Struct para gerenciar Tags no C
Ontem eu tentei fazer um tipo no C# para servir como tags para os produtos / serviços da minha empresa, e que pudesse ser usada em qualquer classe que precisasse te tags e que pudesse ser lido/gravado no banco de dados usando EF como se fosse uma string normal.
Queria que atendesse aos seguintes requisitos:
- mantivesse uma lista de strings únicas e em minúscula
- fosse conversível para string (com o ToString mas implementamos também overload de conversão implícita) retornando a lista de tags únicas em minúsculas, separadas por vírgula e ordenadas.
- fosse conversível DE string
- Se comportasse como string de todas as formas
- Se parecesse com um tipo nativo do .Net
- Fosse no meu domínio um value object
- Fosse compatível com o EF
Um disclaimer aqui: essa classe não vai contar as tags para medir relevância ou fazer tag cloud.
Quero deixar claro que meu primeiro código está muito longe de estar correto, na verdade está um lixo, não o use. Tem várias coisas absolutamente erradas e é um bom exemplo de como mesmo devs experientes podem cometer grandes erros em coisas simples.
O meu primeiro código eu fiz a classe Product, a struct Tags (gostaria de insistir em struct por enquanto) e 4 testes unitários.
Dois testes falharam e dois passaram, e eu fiquei intrigado com o motivo que levou dois deles a falharem e propus o desafio ontem no replit
Vou postar aqui o código errado, os primeiros acertos que fiz e o repositório no github com o código correto. No repositório do github teremos várias branches com os nomes iteracao1, iteracao2 e assim por diante para você poder ver a evolução no código com o passar do tempo. Na Main/Master teremos a última versão do código.
Abaixo o código da iteração ZERO.
///ATENÇÃO: CÓDIGO REDONDAMENTE ERRADO PARA FINS DIDÁTICOS
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
namespace TagStructureTest
{
public struct Tags
{
private List<string> tags = new List<string>();
public Tags() { }
public Tags(params string[] t)
{
if (tags != null)
{
this.tags.AddRange(t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower()).OrderBy(x => x).Distinct());
}
}
public override string ToString()
{
return string.Join(",", tags.Select(x => x.Trim().ToLower()).OrderBy(x => x).Distinct());
}
public void AddTags(params string[] t)
{
var tagsToAdd = t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower()).Distinct().ToList();
this.tags = this.tags.Union(tagsToAdd).Select(x => x.Trim().ToLower()).OrderBy(x => x).Distinct().ToList();
}
public void AddTags(Tags t)
{
this.AddTags(t.GetTags());
}
public void RemoveTags(params string[] t)
{
var tagsToRemove = t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower()).Distinct();
tags = tags.Except(tagsToRemove).Select(x => x.Trim().ToLower()).OrderBy(x => x).Distinct().ToList();
}
public void RemoveTags(Tags tags)
{
this.RemoveTags(tags.GetTags());
}
public string[] GetTags()
{
return this.tags.Select(x => x.Trim().ToLower()).OrderBy(x => x).Distinct().ToArray();
}
}
public class Product
{
public Product()
{
Name = string.Empty;
Tags = new Tags();
}
public string Name { get; set; }
public Tags Tags { get; init; }
}
[TestClass]
public class TagsTest
{
[TestMethod]
public void TagsMustHaveCombinationOfUniqueTags()
{
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 CanRemoveTags()
{
Tags tags = new Tags("tag1,tag2,tag3,tag4,tag5");
tags.RemoveTags("tag1");
tags.RemoveTags(new Tags("tag5"));
Assert.AreEqual("tag2,tag3,tag4", tags.ToString());
}
[TestMethod]
public void ProductMustHaveCombinationOfUniqueTags()
{
Product prod = new Product();
prod.Tags.AddTags("tag1, tag2, tag3");
prod.Tags.AddTags("tag4, tag5, tag3");
Assert.AreEqual("tag1,tag2,tag3,tag4,tag5", prod.Tags.ToString());
}
[TestMethod]
public void CanRemoveTagsFromProduct()
{
Product prod = new Product { Tags = new Tags("tag1,tag2,tag3,tag4,tag5") };
prod.Tags.RemoveTags("tag1");
prod.Tags.RemoveTags(new Tags("tag5"));
Assert.AreEqual("tag2,tag3,tag4", prod.Tags.ToString());
}
}
}
Como eu disse, o código está errado. O primeiro erro é conceitual:
Faltou a imutabilidade. Depois de criada uma Tags eu não deveria poder alterar seu estado ou conteúdo através de métodos, mas apenas criando um novo.
Pense nos exemplos string e DateTime do .net:
Quando você tem string nome = "vitor"
não existe um nome.Append(" Rubio"), mas você tem que criar uma nova string concatenando as duas, o jeito mais simples é nome = nome + " Rubio"
. Note que mesmo colocando na mesma variável, criamos uma nova string.
A mesma coisa com datas. Se você tem DateTime amanha = DateTime.Today
e faz amanha.AddDays(1)
a variável amanha continua tendo o valor de hoje. Isso porque seu estado não é alterado. O método AddDays que retorna o dia de amanhã. Então o correto seria amanha = amanha.AddDays(1)
ou DateTime amanha = DateTime.Today.AddDays(1)
.
Pensando nisso a struct Tags deveria ser totalmente repensada.
Mas vamos fingir que está tudo bem, vamos usar ela como está primeiro, sem ser imutável e fazer apenas os testes rodarem.
Primeiro erro:
Meu primeiro erro foi fazer com que Product tivesse uma auto property com autoproperties usando getter public Tags Tags { get; init; }
, mesmo se fosse somente leitura.
Isso porque um getter é somente uma sintax sugar para um método Getxxx (GetTags por exemplo). E como Tags é uma struct e structs são value types ... bem, value types não são passados por referência no retorno de funções, eles são copiados. Então toda vez que eu acesso a propriedade Tags de Product eu estou com um Tags novíssimo.
Isso não seria tão problemático se pelo menos mantivéssemos o Tags apontando para a mesma List tags. Mas este não é o caso. Toda vez que fazíamos um AddTags ou RemoveTags criávamos uma nova lista. Isso fazia com que dentro do objeto Tags antigo uma nova lista com nosso conteúdo fosse criada enquanto que o Tags novo que retornou do getter da property está ainda apontando para a List tags vazia.
Também ficou confuso com essa nomenclatura e toda essa chamada de métodos pra limpar, ordenar e garantir a unicidade.
- Mais sobre Structure Types e Value Types
Segundo erro:
No post origina do replit, nos métodos AddTags da linha 26 e RemoveTags da linha 40 eu criava uma nova instancia List tags. (com new ou ToList()).
Esse é um erro que merece uma atenção especial: ao misturar reference types com value types dentro de structs garanta que seus membros sejam readonly, imutáveis e que não sejam criadas novas instâncias neles. Até em classes esses cuidados devem ser tomados, mas em structs eles se fazem muito mais importantes.
Então o código do AddTags poderia ser assim:
var current = tags.ToArray();
var tagsToAdd = t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower()).Distinct().ToArray();
tags.Clear();
tags.AddRange(current.Union(tagsToAdd).Select(x => x.Trim().ToLower()).Distinct().ToList());
E o método RemoveTags poderia ser:
var tagsToRemove = t.Where(x => !string.IsNullOrWhiteSpace(x)).SelectMany(x => x!.Split(",")).Select(x => x.Trim().ToLower()).Distinct();
_taglist.RemoveAll(x => tagsToRemove.Contains(x));
Mas ainda sim estamos lidando com tags mutáveis. Todo esse aparato deveria ser imutável.
Terceiro erro:
Seria bom que a List tags fosse somente leitura, e isso já seria um passo na direção correta
Quarto erro:
Eu não deveria alterar as tags de um produto com product.Tags.AddTags
.... mas sim através de um método product.AddTags
, essa é a lei de Demeter
É isso
Links e referências:
Top comments (0)