De acordo com Quando a obsessão primitiva não é um cheiro de código? , Devo criar um objeto ZipCode para representar um CEP em vez de um objeto String.
No entanto, na minha experiência, eu prefiro ver
public class Address{
public String zipCode;
}
ao invés de
public class Address{
public ZipCode zipCode;
}
porque acho que o último requer que eu mude para a classe ZipCode para entender o programa.
E acredito que preciso passar entre muitas classes para ver a definição se todos os campos de dados primitivos fossem substituídos por uma classe, que parece estar sofrendo do problema do ioiô (um antipadrão).
Então, eu gostaria de mover os métodos ZipCode para uma nova classe, por exemplo:
Velho:
public class ZipCode{
public boolean validate(String zipCode){
}
}
Novo:
public class ZipCodeHelper{
public static boolean validate(String zipCode){
}
}
para que apenas quem precise validar o código postal dependa da classe ZipCodeHelper. E encontrei outro "benefício" de manter a obsessão primitiva: mantém a classe parecida com sua forma serializada, se houver, por exemplo: uma tabela de endereços com a coluna de string zipCode.
Minha pergunta é: "evitar o problema do ioiô" (alternar entre definições de classe) é uma razão válida para permitir a "obsessão primitiva"?
fonte
Respostas:
A suposição é que você não precisa fazer ioiô na classe ZipCode para entender a classe Address. Se o ZipCode for bem projetado, deve ser óbvio o que ele faz apenas lendo a classe Address.
Os programas não são lidos de ponta a ponta - normalmente os programas são complexos demais para tornar isso possível. Você não pode manter todo o código em um programa em sua mente ao mesmo tempo. Portanto, usamos abstrações e encapsulamentos para "dividir" o programa em unidades significativas, para que você possa ver uma parte do programa (por exemplo, a classe Address) sem precisar ler todo o código do qual depende.
Por exemplo, tenho certeza de que você não deve ler o código-fonte para String sempre que encontrar String no código.
Renomear a classe de ZipCode para ZipCodeHelper sugere que agora existem dois conceitos separados: um código postal e um auxiliar de código postal. Tão duas vezes mais complexo. E agora o sistema de tipos não pode ajudá-lo a distinguir entre uma sequência arbitrária e um CEP válido, pois eles têm o mesmo tipo. É aqui que a "obsessão" é apropriada: você está sugerindo uma alternativa mais complexa e menos segura, apenas porque deseja evitar um tipo simples de invólucro em torno de um primitivo.
O uso de uma primitiva é IMHO justificado nos casos em que não há validação ou outra lógica, dependendo desse tipo específico. Mas assim que você adiciona qualquer lógica, é muito mais simples se essa lógica for encapsulada com o tipo
Quanto à serialização, acho que parece uma limitação na estrutura que você está usando. Certamente você deve conseguir serializar um ZipCode em uma string ou mapeá-lo para uma coluna em um banco de dados.
fonte
ZipCodeHelper
(que eu prefiro chamarZipCodeValidator
) pode muito bem estabelecer uma conexão com um serviço da Web para fazer seu trabalho. Isso não faria parte da responsabilidade única "manter os dados do código postal". Tornar o sistema de tipos desaprovar códigos postais inválidos ainda pode ser alcançado, tornando oZipCode
construtor o equivalente ao pacote privado do Java e chamando-o com umZipCodeFactory
que sempre chama o validador.ZipCode
seria uma "estrutura de dados" eZipCodeHelper
um "objeto". De qualquer forma, acho que concordamos que não devemos passar as conexões da Web ao construtor ZipCode.int
como ID permite multiplicar um ID por um ID ...)Foo
eFooValidator
aulas separadas . Nós poderia ter umaZipCode
classe que valida o formato e umZipCodeValidator
que bate em algum serviço Web para verificar se um formatado corretamenteZipCode
é realmente atual. Sabemos que os códigos postais mudam. Mas, na prática, teremos uma lista de códigos postais válidos encapsulados emZipCode
ou em algum banco de dados local.Se pode fazer:
E o construtor para ZipCode faz:
Então você quebrou o encapsulamento e adicionou uma dependência bastante boba à classe ZipCode. Se o construtor não chamar
ZipCodeHelper.validate(...)
, você terá uma lógica isolada em sua própria ilha sem realmente aplicá-la. Você pode criar códigos postais inválidos.O
validate
método deve ser um método estático na classe ZipCode. Agora, o conhecimento de um código postal "válido" é agrupado com a classe ZipCode. Como seus exemplos de código se parecem com Java, o construtor de ZipCode deve lançar uma exceção se um formato incorreto for fornecido:O construtor verifica o formato e lança uma exceção, impedindo a criação de códigos postais inválidos, e o
validate
método estático está disponível para outro código, para que a lógica de verificar o formato seja encapsulada na classe ZipCode.Não há "ioiô" nessa variante da classe ZipCode. É apenas chamado de Programação Orientada a Objetos adequada.
Também vamos ignorar a internacionalização aqui, que pode exigir outra classe chamada ZipCodeFormat ou PostalService (por exemplo, PostalService.isValidPostalCode (...), PostalService.parsePostalCode (...), etc.).
fonte
ZipCode.validate
método é a pré-verificação que pode ser realizada antes de chamar um construtor que lança uma exceção.Se você luta muito com essa pergunta, talvez o idioma que você usa não seja a ferramenta certa para o trabalho? Esse tipo de "primitivas do tipo domínio" é trivialmente fácil de expressar, por exemplo, em F #.
Lá você pode, por exemplo, escrever:
Isso é realmente útil para evitar erros comuns, como comparar IDs de diferentes entidades. E como essas primitivas digitadas são muito mais leves que uma classe C # ou Java, você realmente as usará.
fonte
ZipCode
?A resposta depende inteiramente do que você realmente deseja fazer com os códigos postais. Aqui estão duas possibilidades extremas:
(1) Todos os endereços são garantidos em um único país. Sem exceções. (Por exemplo, nenhum cliente estrangeiro ou funcionário cujo endereço particular esteja no exterior enquanto estiver trabalhando para um cliente estrangeiro.) Este país possui CEPs e espera-se que eles nunca sejam seriamente problemáticos (ou seja, não requerem entrada de forma livre) como "atualmente o D4B 6N2, mas isso muda a cada 2 semanas"). Os códigos postais são usados não apenas para endereçamento, mas para validação de informações de pagamento ou finalidades semelhantes. - Nessas circunstâncias, uma classe de código postal faz muito sentido.
(2) Os endereços podem estar em quase todos os países; portanto, dezenas ou centenas de esquemas de endereçamento com ou sem CEP (e com milhares de exceções estranhas e casos especiais) são relevantes. Um código "ZIP" é realmente solicitado apenas para lembrar as pessoas de países onde os códigos postais são usados para não esquecer de fornecer os seus. Os endereços são usados apenas para que, se alguém perder o acesso à sua conta e puder provar seu nome e endereço, o acesso seja restaurado. - Nessas circunstâncias, as classes de CEP para todos os países relevantes seriam um esforço enorme. Felizmente eles não são necessários.
fonte
As outras respostas falaram sobre modelagem de domínio OO e uso de um tipo mais rico para representar seu valor.
Não discordo, especialmente considerando o código de exemplo que você postou.
Mas também me pergunto se isso realmente responde ao título da sua pergunta.
Considere o seguinte cenário (extraído de um projeto real no qual estou trabalhando):
Você tem um aplicativo remoto em um dispositivo de campo que fala com o servidor central. Um dos campos do banco de dados para a entrada do dispositivo é um CEP para o endereço em que o dispositivo de campo está. Você não se importa com o CEP (ou qualquer outro endereço do resto). Todas as pessoas que se preocupam com isso estão do outro lado de um limite HTTP: você é a única fonte de verdade para os dados. Não tem lugar na sua modelagem de domínio. Você apenas grava, valida, armazena e, a pedido, embaralha em um blob JSON para pontos em outros lugares.
Nesse cenário, fazer muito além de validar a inserção com uma restrição de regex SQL (ou seu equivalente ORM) provavelmente é um exagero da variedade YAGNI.
fonte
RegexValidatedString
, contendo a própria sequência e o regex usado para validá-la. Mas, a menos que cada instância tenha um regex único (o que é possível, mas improvável), isso parece um pouco tolo e desperdiçador de memória (e possivelmente tempo de compilação do regex). Então, você coloca o regex em uma tabela separada e deixa para trás uma chave de pesquisa em cada instância para encontrá-lo (o que é possivelmente pior devido a indireção) ou encontra uma maneira de armazená-lo uma vez para cada tipo comum de compartilhamento de valor que regra - - por exemplo. um campo estático em um tipo de domínio ou método equivalente, como disse o IMSoP.A
ZipCode
abstração só faria sentido se suaAddress
classe também não tivesse umaTownName
propriedade. Caso contrário, você terá meia abstração: o código postal designa a cidade, mas esses dois bits de informação relacionados são encontrados em classes diferentes. Não faz muito sentido.No entanto, mesmo assim, ainda não é uma aplicação correta (ou melhor, uma solução para) obsessão primitiva; que, na minha opinião, se concentra principalmente em duas coisas:
Seu caso também não é. Um endereço é um conceito bem definido com propriedades claramente necessárias (rua, número, CEP, cidade, estado, país, ...). Há pouco ou nenhum motivo para quebrar esses dados, pois eles têm uma única responsabilidade: designar um local na Terra. Um endereço requer todos esses campos para ser significativo. Metade de um endereço é inútil.
É assim que você sabe que não precisa mais subdividir-se: decompô-lo ainda mais prejudicaria a intenção funcional da
Address
classe. Da mesma forma, você não precisa que umaName
subclasse seja usada naPerson
classe, a menos queName
(sem uma pessoa conectada) seja um conceito significativo em seu domínio. O que (geralmente) não é. Os nomes são usados para identificar pessoas, elas geralmente não têm valor por si próprias.fonte
Name
subclasse para ser usada naPerson
classe, a menos que Nome (sem uma pessoa conectada) seja um conceito significativo em seu domínio ." Quando você tem validação personalizada para os nomes, o nome se torna um conceito significativo em seu domínio; que mencionei explicitamente como um caso de uso válido para o uso de um subtipo. Em segundo lugar, para a validação do CEP, você está introduzindo suposições adicionais, como códigos postais que precisam seguir o formato de um determinado país. Você está abordando um tópico muito mais amplo do que a intenção da pergunta do OP.Do artigo:
O código fonte é lido com muito mais frequência do que está escrito. Portanto, o problema do ioiô, de ter que alternar entre muitos arquivos, é uma preocupação.
No entanto, não , o problema do ioiô parece muito mais relevante ao lidar com módulos ou classes profundamente interdependentes (que se alternam entre si). Esse é um tipo especial de pesadelo para ler, e provavelmente é o que o criador do problema do ioiô tinha em mente.
No entanto - sim , é importante evitar muitas camadas de abstração!
Por exemplo, discordo da suposição feita na resposta da mmmaaa de que "você não precisa fazer ioiô para [(visitar)] a classe ZipCode para entender a classe Address". Minha experiência tem sido que fazer - pelo menos nas primeiras vezes que você ler o código. No entanto, como os outros, não são momentos em que uma
ZipCode
classe mais adequada.YAGNI (Você não vai precisar disso) é um padrão melhor a seguir para evitar o código de lasanha (código com muitas camadas) - abstrações, como tipos e classes, estão lá para ajudar o programador e não devem ser usadas a menos que sejam um auxílio.
Pessoalmente, pretendo "salvar linhas de código" (e, claro, os relacionados "salvar arquivos / módulos / classes", etc). Estou confiante de que há quem me aplique o epíteto de "obsessivo primitivo" - acho mais importante ter um código fácil de raciocinar do que se preocupar com rótulos, padrões e antipadrões. A escolha correta de quando criar uma função, um módulo / arquivo / classe ou colocar uma função em um local comum é muito situacional. Eu aponto aproximadamente para 3-100 funções de linha, 80-500 arquivos de linha e "1, 2, n" para código de biblioteca reutilizável ( SLOC - sem incluir comentários ou clichê; normalmente eu quero pelo menos um SLOC adicional adicional por linha obrigatória padrão).
A maioria dos padrões positivos surgiu dos desenvolvedores fazendo exatamente isso, quando precisavam deles . É muito mais importante aprender a escrever código legível do que tentar aplicar padrões sem o mesmo problema a ser resolvido. Qualquer bom desenvolvedor pode implementar o padrão de fábrica sem o ter visto antes, no caso incomum em que é o ajuste certo para o problema. Eu usei o padrão de fábrica, o padrão de observador e, provavelmente, centenas além, sem saber seu nome (ou seja, existe um "padrão de atribuição de variáveis"?). Para um experimento divertido - veja quantos padrões GoF estão embutidos na linguagem JS - parei de contar após 12 a 15 anos em 2009. O padrão Factory é tão simples quanto retornar um objeto de um construtor JS, por exemplo - não há necessidade de um WidgetFactory.
Então - sim , às vezes
ZipCode
é uma boa aula. No entanto, não , o problema do ioiô não é estritamente relevante.fonte
O problema do ioiô é relevante apenas se você precisar se virar e voltar. Isso é causado por uma ou duas coisas (às vezes ambas):
Se você pode olhar o nome e ter uma idéia razoável do que ele faz, e os métodos e propriedades fazem coisas razoavelmente óbvias, você não precisa olhar o código, basta usá-lo. Esse é o objetivo das classes em primeiro lugar - elas são trechos de código modulares que podem ser usados e desenvolvidos isoladamente. Se você precisar procurar algo diferente da API da classe para ver o que ela faz, é, na melhor das hipóteses, uma falha parcial.
fonte
Lembre-se, não há bala de prata. Se você estiver escrevendo um aplicativo extremamente simples que precisa ser rastreado rapidamente, uma sequência simples pode fazer o trabalho. No entanto, em 98% das vezes, um Objeto de Valor, conforme descrito por Eric Evans no DDD, seria o ajuste perfeito. Você pode ver facilmente todos os benefícios que os objetos Value oferecem lendo.
fonte