Recentemente, tive um problema com a legibilidade do meu código.
Eu tinha uma função que fazia uma operação e retornava uma string representando o ID dessa operação para referência futura (um pouco como o OpenFile no Windows retornando um identificador). O usuário usaria esse ID posteriormente para iniciar a operação e monitorar seu término.
O ID tinha que ser uma sequência aleatória devido a preocupações de interoperabilidade. Isso criou um método que tinha uma assinatura muito pouco clara:
public string CreateNewThing()
Isso torna clara a intenção do tipo de retorno. Eu pensei em quebrar essa string em outro tipo que torna seu significado mais claro assim:
public OperationIdentifier CreateNewThing()
O tipo conterá apenas a sequência e será usado sempre que essa sequência for usada.
É óbvio que a vantagem desse modo de operação é mais segurança de tipo e intenção mais clara, mas também cria muito mais código e código que não é muito idiomático. Por um lado, gosto da segurança adicional, mas também cria muita confusão.
Você acha que é uma boa prática agrupar tipos simples em uma classe por motivos de segurança?
fonte
Respostas:
Primitivas, como
string
ouint
, não têm significado em um domínio comercial. Uma conseqüência direta disso é que você pode usar um URL por engano quando é esperado um ID do produto ou usar quantidade ao esperar o preço .É também por isso que o desafio Object Calisthenics apresenta empacotamento de primitivas como uma de suas regras:
O mesmo documento explica que há um benefício adicional:
De fato, quando as primitivas são usadas, geralmente é extremamente difícil rastrear a localização exata do código relacionado a esses tipos, geralmente levando à duplicação grave do código . Se houver
Price: Money
classe, é natural encontrar a faixa de verificação dentro. Se, em vez disso, é usado umint
(pior, adouble
) para armazenar os preços dos produtos, quem deve validar o intervalo? O produto? O desconto? O carrinho?Finalmente, um terceiro benefício não mencionado no documento é a capacidade de alterar relativamente fácil o tipo subjacente. Se hoje meu
ProductId
temshort
como seu tipo subjacente e, posteriormente, eu preciso usá-loint
, é provável que o código a ser alterado não abranja toda a base de código.A desvantagem - e o mesmo argumento se aplica a todas as regras do exercício Object Calisthenics - é que, se rapidamente se torna esmagador demais para criar uma classe para tudo . Se
Product
contém oProductPrice
que herda doPositivePrice
qual herda oPrice
qual, por sua vezMoney
, não é uma arquitetura limpa, mas uma bagunça completa em que, para encontrar uma única coisa, um mantenedor deve abrir algumas dezenas de arquivos todas as vezes.Outro ponto a considerar é o custo (em termos de linhas de código) da criação de classes adicionais. Se os invólucros forem imutáveis (como deveriam ser normalmente), significa que, se usarmos C #, você precisará ter, dentro do invólucro, pelo menos:
ToString()
,Equals
e aGetHashCode
substitui (também muito LOC).e eventualmente, quando relevante:
==
e!=
operadores,Cem LOC para um invólucro simples o tornam bastante proibitivo, e é também por isso que você pode ter certeza absoluta da lucratividade a longo prazo desse invólucro. A noção de escopo explicada por Thomas Junk é particularmente relevante aqui. Escrever cem LOCs para representar um
ProductId
usado em toda a sua base de código parece bastante útil. Escrever uma classe desse tamanho para um pedaço de código que cria três linhas em um único método é muito mais questionável.Conclusão:
Agrupe primitivas em classes que tenham significado no domínio comercial do aplicativo quando (1) ajudar a reduzir erros, (2) reduzir o risco de duplicação de código ou (3) ajudar a alterar o tipo subjacente posteriormente.
Não agrupe automaticamente todos os primitivos encontrados no seu código: há muitos casos em que o uso
string
ouint
é perfeitamente adequado.Na prática,
public string CreateNewThing()
retornar uma instância daThingId
classe em vez destring
pode ajudar, mas você também pode:Retorne uma instância da
Id<string>
classe, que é um objeto do tipo genérico, indicando que o tipo subjacente é uma sequência. Você tem o benefício da legibilidade, sem a desvantagem de ter que manter muitos tipos.Retorne uma instância da
Thing
classe. Se o usuário precisar apenas do ID, isso pode ser feito facilmente com:fonte
Eu usaria o escopo como regra geral: quanto mais restrito for o escopo de gerar e consumir
values
, menor será a probabilidade de você criar um objeto que represente esse valor.Digamos que você tenha o seguinte pseudocódigo
então o escopo é muito limitado e eu não faria sentido em torná-lo um tipo. Mas digamos que você gere esse valor em uma camada e o passe para outra camada (ou até outro objeto), faria sentido criar um tipo para isso.
fonte
doFancyOtherstuff()
uma sub-rotina, você pode pensar que ajob.do(id)
referência não é local o suficiente para ser mantida como uma sequência simples.Os sistemas do tipo estático têm tudo a ver com a prevenção de usos incorretos dos dados.
Existem exemplos óbvios de tipos que fazem isso:
Existem exemplos mais sutis
Podemos ficar tentados a usar
double
tanto por preço quanto por comprimento ou usar umstring
para um nome e um URL. Mas fazer isso subverte nosso incrível sistema de tipos e permite que esses abusos passem nas verificações estáticas do idioma.Confundir segundos de libra com segundos de Newton pode ter resultados ruins em tempo de execução .
Isso é particularmente um problema com seqüências de caracteres. Eles freqüentemente se tornam o "tipo de dados universal".
Estamos acostumados principalmente a interfaces de texto com computadores e geralmente estendemos essas interfaces humanas (UIs) para interfaces de programação (APIs). Pensamos em 34.25 como os caracteres 34.25 . Pensamos em uma data como os personagens 05-03-2015 . Pensamos em um UUID como os caracteres 75e945ee-f1e9-11e4-b9b2-1697f925ec7b .
Mas esse modelo mental prejudica a abstração da API.
Da mesma forma, as representações textuais não devem desempenhar nenhum papel no design de tipos e APIs. Cuidado com o
string
! (e outros tipos "primitivos" excessivamente genéricos)Tipos comunicam "quais operações fazem sentido".
Por exemplo, uma vez trabalhei em um cliente para uma API REST HTTP. O REST, feito corretamente, usa entidades hipermídia, que possuem hiperlinks apontando para entidades relacionadas. Nesse cliente, não apenas as entidades foram digitadas (por exemplo, Usuário, Conta, Assinatura), mas também os links para essas entidades (UserLink, AccountLink, SubscriptionLink). Os links eram pouco mais do que invólucros
Uri
, mas os tipos separados impossibilitavam o uso de um AccountLink para buscar um usuário. Se tudo tivesse sido simplesUri
- ou pior aindastring
- , esses erros só seriam encontrados em tempo de execução.Da mesma forma, na sua situação, você tem dados que são usados para apenas uma finalidade: identificar um
Operation
. Não deve ser usado para mais nada, e não devemos tentar identificarOperation
s com sequências aleatórias que criamos. Criar uma classe separada adiciona legibilidade e segurança ao seu código.Obviamente, todas as coisas boas podem ser usadas em excesso. Considerar
quanta clareza acrescenta ao seu código
com que frequência é usado
Se um "tipo" de dados (no sentido abstrato) é usado com frequência, para propósitos distintos e entre interfaces de código, é um candidato muito bom por ser uma classe separada, à custa da verbosidade.
fonte
05-03-2015
significa quando interpretado como uma data ?As vezes.
Este é um daqueles casos em que você precisa avaliar os problemas que podem surgir ao usar um em
string
vez de um mais concretoOperationIdentifier
. Qual é a gravidade deles? Qual é a probabilidade deles?Então você precisa considerar o custo de usar o outro tipo. Quão doloroso é usar? Quanto trabalho é feito?
Em alguns casos, você economiza tempo e esforço com um bom tipo de concreto para trabalhar. Em outros, não valerá a pena.
Em geral, acho que esse tipo de coisa deve ser feito mais do que é hoje. Se você tem uma entidade que significa algo em seu domínio, é bom tê-lo como seu próprio tipo, pois é mais provável que essa entidade mude / cresça com a empresa.
fonte
Eu geralmente concordo que muitas vezes você deve criar um tipo para primitivas e seqüências de caracteres, mas como as respostas acima recomendam a criação de um tipo na maioria dos casos, listarei algumas razões pelas quais / quando não:
fonte
short
(por exemplo) terá o mesmo custo envolto ou desembrulhado. (?)Não, você não deve definir tipos (classes) para "tudo".
Mas, como outras respostas indicam, geralmente é útil fazê-lo. Você deve desenvolver - conscientemente, se possível - um sentimento de atrito excessivo devido à ausência de um tipo ou classe adequada, ao escrever, testar e manter seu código. Para mim, o início do atrito excessivo é quando quero consolidar vários valores primitivos como um valor único ou quando preciso validar os valores (isto é, determinar quais de todos os valores possíveis do tipo primitivo correspondem aos valores válidos do 'tipo implícito').
Descobri que considerações como a que você colocou na sua pergunta são responsáveis por muito design no meu código. Eu desenvolvi o hábito de evitar deliberadamente escrever mais código do que o necessário. Espero que você esteja escrevendo bons testes (automatizados) para o seu código - se estiver, poderá refatorá-lo facilmente e adicionar tipos ou classes, se isso fornecer benefícios líquidos para o desenvolvimento e manutenção contínuos do seu código .
A resposta de Telastyn e resposta de Thomas Junk ambos fazem um bom ponto sobre o contexto eo uso do código relevante. Se você estiver usando um valor dentro de um único bloco de código (por exemplo, método, loop,
using
bloco), não há problema em usar apenas um tipo primitivo. É bom usar um tipo primitivo se você estiver usando um conjunto de valores repetidamente e em muitos outros lugares. Mas quanto mais frequente e amplamente você estiver usando um conjunto de valores, e quanto menos esse conjunto de valores corresponder aos valores representados pelo tipo primitivo, mais você deverá considerar encapsular os valores em uma classe ou tipo.fonte
Aparentemente, tudo o que você precisa fazer é identificar uma operação.
Além disso, você diz o que uma operação deve fazer:
Você diz isso de uma maneira como se fosse "exatamente como a identificação é usada", mas eu diria que essas são propriedades que descrevem uma operação. Isso soa como uma definição de tipo para mim, há até um padrão chamado "padrão de comando" que se encaixa muito bem. .
Diz
Eu acho que isso é muito semelhante comparado ao que você quer fazer com suas operações. (Compare as frases que coloquei em negrito nas duas aspas). Em vez de retornar uma string, retorne um identificador para um
Operation
no sentido abstrato, um ponteiro para um objeto dessa classe no oop, por exemplo.Em relação ao comentário
Não, não seria. Lembre-se de que os padrões são muito abstratos , tão abstratos de fato que são um tanto meta. Ou seja, eles geralmente abstraem a própria programação e não algum conceito do mundo real. O padrão de comando é uma abstração de uma chamada de função. (ou método) Como se alguém apertasse o botão de pausa logo após passar os valores dos parâmetros e logo antes da execução, para retomar mais tarde.
A seguir, está considerando oop, mas a motivação por trás disso deve ser verdadeira para qualquer paradigma. Eu gosto de dar algumas razões pelas quais colocar a lógica no comando pode ser considerada uma coisa ruim.
Para recapitular: o padrão de comando permite agrupar a funcionalidade em um objeto, para ser executado posteriormente. Por uma questão de modularidade (a funcionalidade existe independentemente de ser executada via comando ou não), testabilidade (a funcionalidade deve ser testável sem comando) e todas as outras palavras que expressam essencialmente a demanda para escrever um bom código, você não colocaria a lógica real no comando.
Como os padrões são abstratos, pode ser difícil criar boas metáforas do mundo real. Aqui está uma tentativa:
"Ei, vovó, você poderia apertar o botão de gravação na TV às 12, para não perder os simpsons no canal 1?"
Minha avó não sabe o que acontece tecnicamente quando aperta o botão de gravação. A lógica está em outro lugar (na TV). E isso é uma coisa boa. A funcionalidade é encapsulada e as informações são ocultas do comando, é um usuário da API, não necessariamente parte da lógica ... oh caramba, estou balbuciando palavras de novo, é melhor terminar esta edição agora.
fonte
Idéia por trás do agrupamento de tipos primitivos,
Obviamente, seria muito difícil e impraticável fazê-lo em qualquer lugar, mas é importante agrupar os tipos quando necessário,
Por exemplo, se você tiver a classe Order,
As propriedades importantes para procurar um Pedido são principalmente OrderId e InvoiceNumber. E o Valor e o Código da Moeda estão intimamente relacionados, onde se alguém alterar o Código da Moeda sem alterar o Valor, o Pedido não poderá mais ser considerado válido.
Portanto, apenas agrupar OrderId, InvoiceNumber e introduzir um composto para moeda faz sentido nesse cenário e provavelmente a descrição do agrupamento não faz sentido. Portanto, o resultado preferido pode parecer,
Portanto, não há razão para embrulhar tudo, mas coisas que realmente importam.
fonte
Opinião impopular:
Você normalmente não deve definir um novo tipo!
Definir um novo tipo para quebrar uma classe primitiva ou básica é algumas vezes chamado de definição de um pseudo-tipo. A IBM descreve isso como uma má prática aqui (eles se concentram especificamente no uso indevido de genéricos nesse caso).
Os pseudo tipos tornam inútil a funcionalidade comum da biblioteca.
As funções matemáticas do Java podem funcionar com todas as primitivas numéricas. Mas se você definir uma nova classe Porcentagem (agrupar um dobro que pode estar no intervalo de 0 a 1), todas essas funções serão inúteis e precisarão ser agrupadas por classes que (ainda pior) precisam saber sobre a representação interna da classe Porcentagem .
Pseudo-tipos se tornam virais
Ao criar várias bibliotecas, você frequentemente descobrirá que esses pseudotipos se tornam virais. Se você usar a classe Porcentagem acima mencionada em uma biblioteca, será necessário convertê-la no limite da biblioteca (perdendo toda a segurança / significado / lógica / outro motivo que você tinha para criar esse tipo) ou terá que fazer essas classes acessível para a outra biblioteca também. Infectando a nova biblioteca com seus tipos, onde uma simples dupla pode ter sido suficiente.
Tirar mensagem
Desde que o tipo que você agrupe não precise de muita lógica de negócios, eu recomendaria não agrupá-lo em uma pseudo classe. Você só deve agrupar uma classe se houver sérias restrições de negócios. Em outros casos, nomear suas variáveis corretamente deve contribuir bastante para transmitir significado.
Um exemplo:
A
uint
pode representar perfeitamente um UserId, podemos continuar usando operadores incorporados do Java parauint
s (como ==) e não precisamos de lógica de negócios para proteger o 'estado interno' do ID do usuário.fonte
Como em todas as dicas, saber quando aplicar as regras é a habilidade. Se você criar seus próprios tipos em um idioma controlado por tipo, receberá a verificação de tipo. Então, geralmente, esse será um bom plano.
NamingConvention é a chave para a legibilidade. Os dois combinados podem transmitir intenção claramente.
Mas /// ainda é útil.
Então, sim, eu diria que crie muitos tipos próprios quando a vida deles estiver além de um limite de classe. Considere também o uso de ambos
Struct
eClass
, nem sempre, de Classe.fonte
///
significa isso ?