DEV Community

Vitor Luiz Rubio
Vitor Luiz Rubio

Posted on

Struct para gerenciar Tags no C# - Parte 3

Continuando com as alterações na struct tags, temos mais duas iterações. Depois é apenas documentar, melhorar os testes e arrumar o layout do código. Dê uma olhada depois no projeto no github para ver como ficou.


Iteração 5

Adicionei overlod para os operadores de conversão implícita string => tags e tags => string.

        public static implicit operator string(Tags t) => t.ToString();

        public static implicit operator Tags(string s) => new Tags(s);
Enter fullscreen mode Exit fullscreen mode

E Deu pau boniiiiito.

Muito cuidado ao brincar com overload de operadores.

Primeiro de tudo, o que são conversões implícitas? São conversões sem perda de dados, de tipos compatíveis, que podem ser feitas diretamente sem colocar o tipo na frente.
Por exemplo:

byte a = 5;
int b = a;
Enter fullscreen mode Exit fullscreen mode

Essa é uma conversão implícita. Você pode jogar um byte em um int, porque ele cabe, e ele também é um "tipo do mesmo tipo". Ele é um número inteiro. Só que menor. Ele pode ser convertido direto, é o mesmo tipo de dado.
Tá, mas e uma conversão explícita? É o tipo de conversão que você tem que forçar com o nome do outro tipo na frente, porque os tipos são quase incompatíveis, ou compatíveis até certo ponto, e você vai perder dados. Por exemplo na conversão abaixo:

double a = 5.5;
int b = (int)a;
Enter fullscreen mode Exit fullscreen mode

Nessa conversão eu tenho que forçar o tipo double a "caber" no int e pra isso eu perco informação, eu perco o .5.

No nosso caso, eu quero que Tags seja conversível para string. Assim de uma forma que se eu jogar uma string em uma tag ele crie a tag automaticamente sem dar new, e se eu jogar uma tag em uma string ele converta automaticamente sem eu ter que chamar o .ToString().

Muito ousado?

O que eu quero é fazer isso ser legal e compilável:

Tags a = "vitor,teste";
string b = a;
Enter fullscreen mode Exit fullscreen mode

E porque está dando vários erros do tipo

Error CS0121 The call is ambiguous between the following methods or properties: 'Tags.RemoveTags(Tags?)' and 'Tags.RemoveTags(params string[]?)' TagStructureTest C:\Users\vitor\OneDrive\Desktop\Labs\TagStructureTest\TagStructureTest\TagsTest.cs 32 N/A

Onde está a ambiguidade? Em todos os métodos que eu aceito Tags como parâmetro de entrada mas que também tem um overload que aceite string está dando ambiguidade. Por quê? Porque o compilador não sabe se RemoveTags(new Tags("tag5"));é pra chamar o RemoveTags de string ou o RemoveTags de Tags convertento pra string .... já que agora Tags é legalmente conversível pra string ...

Complicado? Muito.

Vamos retirar todos os métodos que aceitam Tags como argumento, e seus testes. Vamos também renomear AddTags para Add e RemoveTags para Remove.

Magicamente removendo esses métodos ele compila, e agora temos menos métodos para dar manutenção, testar e documentar.

Rodamos os testes e todos os testes rodaram exceto o InequalityOperatorSameVarTest, que já esperávamos, pois Tags ainda não é imutável, e o teste

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

Falha na linha Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags1);

Estrano isso, mas vamos descomentar linha Assert.IsTrue(tags1 == "tag1,tag2,tag3,tag4,tag5") e ver se ela passa, vamos também olhar a documentação do Assert.AreEqual e debugar. Vamos também dividir isso aí em 3 testes.

        [TestMethod]
        public void TagsShouldBeEqualsToString()
        {
            Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5");
            Assert.IsTrue(tags1.Equals("tag1,tag2,tag3,tag4,tag5"));
        }

        [TestMethod]
        public void TagsShouldBeEqualsToStringUsingEqualityOperators()
        {
            Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5");
            Assert.IsTrue(tags1 == "tag1,tag2,tag3,tag4,tag5"); 
        }

        [TestMethod]
        public void ATagsVarShouldBeEqualsToASameContentString()
        {
            Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5");
            Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags1);
        }
Enter fullscreen mode Exit fullscreen mode

Compila e Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags1); continua falhando.

Inspecionando o método Assert.AreEqual encontramos a resposta:

        //
        // Summary:
        //     Tests whether the specified objects are equal and throws an exception if the
        //     two objects are not equal. *Different numeric types are treated as unequal even
        //     if the logical values are equal. 42L is not equal to 42.*
        //
        // Parameters:
        //   expected:
        //     The first object to compare. This is the object the tests expects.
        //
        //   actual:
        //     The second object to compare. This is the object produced by the code under test.
        //
        // Exceptions:
        //   T:Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException:
        //     Thrown if expected is not equal to actual.
        public static void AreEqual(object expected, object actual)
Enter fullscreen mode Exit fullscreen mode

Ou seja, Assert.AreEqual considera o mesmo tipo e mesmo valor. Ele falha pra um int e um byte por exemplo. Isso está na documentação.

Então vamos trocar essa linha para Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags1.ToString()); porque nós queremos ver se o valor das duas strings geradas é o mesmo.

Funcionou. Agora vamos para a iteração 6 onde deixamos isso tudo verdadeiramente imutável.

Iteração 6

Lembrando que cada interação minha é um ajuste ou refactoring para fazer funcionar um dos testes (e as vezes testes adicionais) e que eu estou colocando cada uma em uma branch.
Eu não colocaria cada um em uma branch num projeto da vida real mas com certeza teria um commit bem explicado para cada um.
Vamos começar fazendo o seguinte: Mudar o HashSet pra um ImmutableHashSet. Isso faz com que não possamos mais usar os métodos UnionWith e ExceptWith. Então teriamos que criar novos em vez de mudar seu conteúdo. Mas lemre-se que não devemos criar novos reference types dentro de um field de um calue type.
Então teremos que fazer com que os métodos Add e Remove retornem um novo Tags com seu conteúdo já preparado (o resultado da fusão dos hashsets). E teremos que mudar a classe Produto também.
Muita coisa terá que mudar.
O parameterless constructor (construtor padrão) tem que criar um ImmutableHashSet vazio. O construtor que aceita params string[] deve criar um ImmutableHashSet com essas strings ou vazio.
Os métodos add e remove devem aproveitar o método new pra criar um novo. O jeito de criar um ImmutableHashSet é meio diferente, você verá.

Todos os tags.Add e tags.Remove tiveram que mudar para tags = tags.Add e tags = tags.Remove.

using System.Collections.Immutable;

namespace SharpTags
{
    public struct Tags
    {
        private readonly ImmutableHashSet<string> _taglist;

        public Tags() : this(null)
        {

        }

        public Tags(IEnumerable<string>? t) : this(t?.ToArray())
        {

        }

        public Tags(params string[]? t)
        {
            if (t != null && t.Count() > 0)
            {
                var tagsToAdd = t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower()).ToArray();
                _taglist = ImmutableHashSet.Create<string>(tagsToAdd);
            }
            else
            {
                _taglist = ImmutableHashSet.Create<string>();
            }
        }

        public override string ToString()
        {
            return string.Join(",", _taglist.Select(x => x.Trim().ToLower()).OrderBy(x => x).Distinct());
        }


        public Tags Add(IEnumerable<string>? t)
        {
            return this.Add(t?.ToArray());
        }

        public Tags Add(params string[]? t)
        {
            if (t != null && t.Length > 0)
            {
                var tagsToAdd = t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower());
                return new Tags(_taglist.Union(tagsToAdd));
            }

            return new Tags(_taglist);
        }


        public Tags Remove(IEnumerable<string>? t)
        {
            return this.Remove(t?.ToArray());
        }






        public Tags Remove(params string[]? t)
        {
            if (t != null && t.Length > 0)
            {
                var tagsToRemove = t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower());
                return new Tags(_taglist.Except(tagsToRemove));
            }
            return new Tags(_taglist);
        }


        public string[] GetTags()
        {
            return this._taglist.Select(x => x.Trim().ToLower()).OrderBy(x => x).Distinct().ToArray();
        }


        public override int GetHashCode()
        {
            return this.ToString().GetHashCode();
        }

        public override bool Equals(object? obj)
        {

            if (obj == null)
            {
                return false;
            }

            if ((!(obj is Tags)) && (!(obj is string)))
            {
                return false;
            }

            return this.ToString().Equals(obj.ToString());
        }



        public static bool operator ==(Tags esquerda, Tags direita)
        {
            return esquerda.Equals(direita);
        }

        public static bool operator !=(Tags esquerda, Tags direita) => !(esquerda == direita);


        public static implicit operator string(Tags t) => t.ToString();

        public static implicit operator Tags(string s) => new Tags(s);
    }
}
Enter fullscreen mode Exit fullscreen mode

Alteramos e rodamos os testes e funcionou

using Dominio;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SharpTags;
using System.Numerics;

namespace TagStructureTest
{

    [TestClass]
    public class TagsTest
    {

        #region testes básicos passando na iteração1

        [TestMethod]
        public void TagsMustHaveCombinationOfUniqueTags()
        {

            Tags tags = new Tags();
            tags = tags.Add("tag1, tag2, tag3");
            tags = tags.Add("tag4, tag5, tag3");
            Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString());
        }

        [TestMethod]
        public void CanRemoveTags()
        {

            Tags tags = new Tags("tag1,tag2,tag3,tag4,tag5");
            tags = tags.Remove("tag1");
            tags = tags.Remove(new Tags("tag5"));
            Assert.AreEqual("tag2,tag3,tag4", tags.ToString());
        }


        [TestMethod]
        public void ProductMustHaveCombinationOfUniqueTags()
        {


            Produto prod = new Produto();
            prod.Tags = prod.Tags.Add("tag1, tag2, tag3");
            prod.Tags = prod.Tags.Add("tag4, tag5, tag3");
            Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", prod.Tags.ToString());
        }


        [TestMethod]
        public void CanRemoveTagsFromProduct()
        {

            Produto prod = new Produto();
            prod.Tags = prod.Tags.Add("tag1,tag2,tag3,tag4,tag5");
            prod.Tags = prod.Tags.Remove("tag1");
            prod.Tags = prod.Tags.Remove(new Tags("tag5"));
            Assert.AreEqual("tag2,tag3,tag4", prod.Tags.ToString());
        }

        #endregion


        #region testes novos criados na iteracao 1 (constructor, add, remove)
        [TestMethod]
        public void CanCreateTagsFromList()
        {
            Tags tags = new Tags(new List<string> { "tag1", "tag2", "tag3" });
            tags = tags.Add(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 = tags.Add("tag4, tag5, tag3");
            Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString());
        }

        [TestMethod]
        public void CanCreateTagsFromTags()
        {
            Tags tags = new Tags();
            tags = tags.Add(new Tags("tag1, tag2, tag3"));
            tags = tags.Add(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 = tags.Add(new List<string> { "tag1", "tag2", "tag3" });
            tags = tags.Add(new List<string> { "tag4", "tag5", "tag3" });
            Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString());
        }

        [TestMethod]
        public void CanAddTagsFromString()
        {
            Tags tags = new Tags();
            tags = tags.Add("tag1, tag2, tag3");
            tags = tags.Add("tag4, tag5, tag3");
            Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags.ToString());
        }

        [TestMethod]
        public void CanAddTagsFromTags()
        {
            Tags tags = new Tags();
            tags = tags.Add(new Tags("tag1, tag2, tag3"));
            tags = tags.Add(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 = tags.Remove(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 = tags.Remove("tag1, tag5");
            Assert.AreEqual("tag2,tag3,tag4", tags.ToString());
        }

        [TestMethod]
        public void CanRemoveTagsFromTags()
        {
            Tags tags = new Tags("tag1,tag2,tag3,tag4,tag5");
            tags = tags.Remove(new Tags("tag1, tag5"));
            Assert.AreEqual("tag2,tag3,tag4", tags.ToString());
        }

        #endregion


        #region testes de igualdade 

        [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); 
        }

        [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); 
        }



        [TestMethod]
        public void TagsWithSameContentsShouldHaveSameHashcode()
        {
            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 SameTagsShouldHaveSameHashcode()
        {
            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.IsTrue(tags1.Equals("tag1,tag2,tag3,tag4,tag5"));
        }

        [TestMethod]
        public void TagsShouldBeEqualsToStringUsingEqualityOperators()
        {
            Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5");
            Assert.IsTrue(tags1 == "tag1,tag2,tag3,tag4,tag5"); 
        }

        [TestMethod]
        public void ATagsVarShouldBeEqualsToASameContentString()
        {
            Tags tags1 = new Tags("tag1,tag2,tag3,tag4,tag5");
            Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", tags1.ToString());
        }


        [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;
            tags1 = tags2.Add("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); 
        }

        #endregion

    }
}
Enter fullscreen mode Exit fullscreen mode

Mas ainda falta testar bem esses dois implicit operator.
Além disso, e se a gente transformasse cada operação de tags=tags.Add pra tags+= ? E tags.Remove pra tags-= ?
Também precisamos fazer uma limpeza e dividir esses testes talvez em 5 arquivos: Testes de criação, Add, Remove e Gerais. O Arquivo de testes está ficando muito grande.
Além disso está faltando um teste para o método GetTags (que retorna um array de string[]). Esse método não foi nem utilizado e não sei se GetTags é um nome apropriado pra ele. ToArray parece mais apropriado mas eu gostaria de evitar para não dar a impressão de que estamos implementando IEnumerable.
Como nosso conteúdo é imutável, poderíamos guardar ele direto em uma string em vez de um ImmutableHashSet. Assim as consultas a ele, comparações e o ToString poderiam ficar mais performáticos.
Mas se fizermos isso ficaria mais difícil implementar IEnumerable (para navegar entre as tags). Mas será que queremos fazer isso?
Também precisamos decidir se vamos implementar IComparable e IEquatable.

Top comments (0)