DEV Community

Vitor Luiz Rubio
Vitor Luiz Rubio

Posted on

Struct para gerenciar Tags no C# - Parte 2

Então estamos fazendo uma struct para trabalhar com Tags no C#. No primeiro artigo fizemos aquela primeira tentativa que não ficou muito boa e tinha apenas 4 testes. Agora vamos começar as melhorias, mais testes e a descrição do processo.


Iteração 1

Mudamos a List interna para um HashSet porque o hashset já garante a unicidade das tags. Fizemos a renomeação de algumas vairáveis, e mais 9 testes. 3 de criação, 3 de add e 3 de remove.
Deixamos a HashSet como readonly, para não mudarmos sua instância, mas mesmo assim ela (e todo o restante), é mutável.
Deixamos a ordenação só para a saída ToString.
Já podemos criar Tags a partir de strings usando um dos constructores ou convertê-las para strings, mas ainda não podemos simplesmente atribuir um objeto tags a uma string, ou uma string ao Tags. Também não temos o que é recomendável pela microsoft: Override de Equals, GetHashCode, etc.
Igualdade entre tags com o mesmo conteúdo, como se fossem um record, Equal(), GetHashCode(), ==, nada disso está funcionando.

Iteração 2

Adicionamos mais esses testes ao que foi feito na iteração 1 e todos passaram:

 [TestMethod] public void CanCreateTagsFromList() { Tags tags = new Tags(new List<string> { "tag1", "tag2", "tag3" }); tags.AddTags(new List<string> { "tag4", "tag5", "tag3" }); Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString()); } [TestMethod] public void CanCreateTagsFromString() { Tags tags = new Tags("tag1, tag2, tag3"); tags.AddTags("tag4, tag5, tag3"); Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString()); } [TestMethod] public void CanCreateTagsFromTags() { Tags tags = new Tags(); tags.AddTags(new Tags("tag1, tag2, tag3")); tags.AddTags(new Tags("tag4, tag5, tag3")); Tags newTags = new Tags(tags); Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", newTags.ToString()); } [TestMethod] public void CanAddTagsFromList() { Tags tags = new Tags(); tags.AddTags(new List<string> { "tag1", "tag2", "tag3" }); tags.AddTags(new List<string> { "tag4", "tag5", "tag3" }); Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString()); } [TestMethod] public void CanAddTagsFromString() { Tags tags = new Tags(); tags.AddTags("tag1, tag2, tag3"); tags.AddTags("tag4, tag5, tag3"); Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString()); } [TestMethod] public void CanAddTagsFromTags() { Tags tags = new Tags(); tags.AddTags(new Tags("tag1, tag2, tag3")); tags.AddTags(new Tags("tag4, tag5, tag3")); Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString()); } [TestMethod] public void CanRemoveTagsFromList() { Tags tags = new Tags("tag1,tag2,tag3,tag4,tag5"); tags.RemoveTags(new List<string> { "tag1", "tag5"}); Assert.AreEqual("tag2,tag3,tag4", tags.ToString()); } [TestMethod] public void CanRemoveTagsFromString() { Tags tags = new Tags("tag1,tag2,tag3,tag4,tag5"); tags.RemoveTags("tag1, tag5"); Assert.AreEqual("tag2,tag3,tag4", tags.ToString()); } [TestMethod] public void CanRemoveTagsFromTags() { Tags tags = new Tags("tag1,tag2,tag3,tag4,tag5"); tags.RemoveTags(new Tags("tag1, tag5")); Assert.AreEqual("tag2,tag3,tag4", tags.ToString()); } 
Enter fullscreen mode Exit fullscreen mode

Criamos testes que falham com certeza, alguns deles nem compilam por isso a parte que não compila está comentada para vermos os outros falharem:

 [TestMethod] public void TagsWithSameContentsShouldBeEquals() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = new Tags("tag1,tag2,tag3,tag4,tag5"); Assert.AreEqual(tags1, tags2); Assert.IsTrue(tags1.Equals(tags2)); //Assert.IsTrue(tags1 == tags2); //não compila } [TestMethod] public void SameTagsShouldBeEquals() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = tags1; Assert.AreEqual(tags1, tags2); Assert.IsTrue(tags1.Equals(tags2)); //Assert.IsTrue(tags1 == tags2); //não compila } [TestMethod] public void TagsWithSameContentsShouldHaveSameHashcodeEquals() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = new Tags("tag1,tag2,tag3,tag4,tag5"); Assert.AreEqual(tags1.GetHashCode(), tags2.GetHashCode()); } [TestMethod] public void SameTagsShouldHaveSameHashcodeEquals() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = tags1; Assert.AreEqual(tags1.GetHashCode(), tags2.GetHashCode()); } [TestMethod] public void TagsShouldBeEqualsToString() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags1); Assert.IsTrue(tags1.Equals("tag1,tag2,tag3,tag4,tag5")); //Assert.IsTrue(tags1 == "tag1,tag2,tag3,tag4,tag5"); //não compila } 
Enter fullscreen mode Exit fullscreen mode

Iteração 3

Os testes que criamos falharam porque não temos override do Equals, nem do GetHashCode, ou do operador ==

Também precisamos dar uma arrumada na casa, está tudo em um arquivo só porque fizemos no replit, mas está na hora de separar o projeto de teste do restante, a Tags para uma biblioteca e Product para uma suposta aplicação.
Também estou passando o nome de todas as classes de domínio para o português para fins didáticos, e passando a Tags para uma biblioteca chamada SharpTags para simular uma biblioteca de terceiros (que é a maneira como outros a usariam)
Criamos a biblioteca SharpTags
Renomeamos TagStructureTest para SharpTagsTest
Ficamos com a estrutura:

TagStructureTest ├─> Dominio │ ├── Dominio.csproj │ └── Produto.cs ├─> SharpTags │ ├── SharpTags.csproj │ └── Tags.cs ├─> TagStructureTest │ ├── TagsTest.cs │ └── TagStructureTest.csproj ├── TagStructureTest.sln 
Enter fullscreen mode Exit fullscreen mode

Adicionamos Override do Equals e do GetHashCode, que sempre devem ser implementados juntos.
O Override do GetHashCode eu simplesmente aproveitei que já temos um ToString e uso a hashcode que seria gerada para sua string. Não acredito que precisamos de algo melhor que isso por enquanto.
Já o Equals, primeiro ele verifica se dois objetos são o mesmo objeto/instância e retorna true, caso contrário verifica se o objeto sendo comparado é null e retorna false, por último ele vê se as duas strings resultantes são iguais, retornando esse resultado.
Não vamos entrar em detalhes sobre o GetHashCode, ele é um algritmo que gera um número inteiro único para um objeto e é usado para otimizar a performance ao armazenar esse objeto em hashes, como listas do tipo HashSet e Dictionary, fazendo com que sejam armazenados como se fosse em um vetor indexado numericamente (usando esse número gerado como índice) para evitar colisões e aumentar a performance em casoss de listas muito grandes.
A regra mais simples é: se dois objetos são iguais então seus hashes devem ser iguais. Se você fez o override de Equals é obrigado a fazer o override de GetHashCode.
Se você estivesse trabalhando com entidades aqui para serem persistidas em banco de dados com nHibernate ou EF, você faria o GetHashCode ser o próprio Id, e faria o Equals ser baseado no próprio Id também.

Minha implementação:

 public override int GetHashCode() { return this.ToString().GetHashCode(); } public override bool Equals(object? obj) { if (object.ReferenceEquals(this, obj)) { return true; } if (obj == null) { return false; } if ((!(obj is Tags)) && (!(obj is string))) { return false; } return this.ToString().Equals(obj.ToString()); } 
Enter fullscreen mode Exit fullscreen mode

Quase todos passaram exceto os ainda comentados e o teste Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags1);

Iteração 4

Para iniciar a iteração 4 vamos fazer o operator overloading dos sinais == e !=

Operator overloading é um tipo de método que sobrecarrega ou motifica o comportamento de operadores. São úteis para quando precisamos que uma classe ou até uma struct se comporte como um tipo de dado especial até nos momentos em que usamos == ou !=. Por exemplo, nos objetos Produto os operadores == e != só comparam as referências e não o conteúdo. Nós alteraremos esse comportamento em Tags porque queremos que o conteúdo das tags seja considerado.
Veja também

Algumas mudanças foram feitas porque estamos tratando de igualdade e override de operators em um Value Type e não em um Reference Type.
Isso torna algumas coisas mais simples embora outras precisem de mais cuidados.
O trecho de código abaixo podemos tirar:

 if (object.ReferenceEquals(this, obj)) { return true; } 
Enter fullscreen mode Exit fullscreen mode

Com Value Types não precisamos lidar com Reference Equals nem com nulidade.

Os operadores == e != implementados. Veja que o != é facil, porque ele é a negação do ==.

 public static bool operator ==(Tags esquerda, Tags direita) { return esquerda.Equals(direita); } public static bool operator !=(Tags esquerda, Tags direita) => !(esquerda == direita); 
Enter fullscreen mode Exit fullscreen mode

Agora podemos descomentar as linhas:

Assert.IsTrue(tags1 == tags2); 
Enter fullscreen mode Exit fullscreen mode

mas a Assert.IsTrue(tags1 == "tag1,tag2,tag3,tag4,tag5"); ainda não compila, segura ela comentada.
Podemos colocar esses métodos em outros testes também.
O teste TagsShouldBeEqualsToString continua não passando, porque compara com string, ainda não implementamos isso.

Acrescentei mais 4 testes:

 [TestMethod] public void EqualityOperatorSameVarTest() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = tags1; Assert.IsTrue(tags1 == tags2); } [TestMethod] public void EqualityOperatorSameContentsTest() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = new Tags("tag1,tag2,tag3,tag4,tag5"); Assert.IsTrue(tags1 == tags2); } [TestMethod] public void InequalityOperatorSameVarTest() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = tags1; tags2.AddTags("Teste"); Assert.IsTrue(tags1 != tags2); } [TestMethod] public void IneEqualityOperatorSameContentsTest() { Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5"); Tags tags2 = new Tags("tag1,tag2,tag3,tag4,tag6"); Assert.IsTrue(tags1 != tags2); } 
Enter fullscreen mode Exit fullscreen mode

E não estranhamente o InequalityOperatorSameVarTest não passou. Porque? Porque ambos estão compartilhando a mesma HashSet _taglist, por referência, que está sendo mudada pelo método AddTags. Podemos mudar esse método para criar uma nova, mas isso fará novamente com que outros testes, principalmente de retorno de funções e getters retornando Tags (como no caso de Produto), falhem.

A solução é transformar a classe em imutável de vez. Isso vai envolver bastante energia por isso deixaremos para a iteração 5.

Top comments (0)