Usando a verificação de tipo estático para proteger contra erros de negócios


13

Eu sou um grande fã de verificação de tipo estático. Impede que você cometa erros estúpidos como este:

// java code
Adult a = new Adult();
a.setAge("Roger"); //static type checker would complain
a.setName(42); //and here too

Mas isso não impede que você cometa erros estúpidos como este:

Adult a = new Adult();
// obviously you've mixed up these fields, but type checker won't complain
a.setAge(150); // nobody's ever lived this old
a.setWeight(42); // a 42lb adult would have serious health issues

O problema ocorre quando você está usando o mesmo tipo para representar obviamente diferentes tipos de informações. Eu estava pensando que uma boa solução para isso seria estender a Integerclasse, apenas para evitar erros de lógica de negócios, mas não para adicionar funcionalidade. Por exemplo:

class Age extends Integer{};
class Pounds extends Integer{};

class Adult{
    ...
    public void setAge(Age age){..}
    public void setWeight(Pounds pounds){...}
}

Adult a = new Adult();
a.setAge(new Age(42));
a.setWeight(new Pounds(150));

Isso é considerado uma boa prática? Ou existem problemas imprevistos de engenharia no caminho com um design tão restritivo?


5
a.SetAge( new Age(150) )Ainda não será compilado?
John Wu

1
O tipo int do Java tem um intervalo fixo. Claramente, você gostaria de ter números inteiros com intervalos personalizados, por exemplo, Número inteiro <18, 110>. Você está procurando por tipos de refinamento ou tipos dependentes, que alguns idiomas (não convencionais) oferecem.
Theodoros Chatzigiannakis

3
O que você está falando é uma ótima maneira de fazer design! Infelizmente, o Java tem um sistema de tipo tão primitivo que é difícil fazer o certo. E mesmo se você tentar fazer isso, poderá resultar em efeitos colaterais, como desempenho inferior ou produtividade do desenvolvedor.
Euphoric

@ JohnWu sim, seu exemplo ainda será compilado, mas esse é o único ponto de falha dessa lógica. Depois de declarar seu new Age(...)objeto, você não poderá atribuí-lo incorretamente a uma variável do tipo Weightem nenhum outro local. Reduz o número de lugares onde erros podem acontecer.
J-prumo

Respostas:


12

Você está essencialmente solicitando um sistema de unidades (não, não testes de unidade, "unidade" como em "unidade física", como medidores, volts etc.).

No seu código Agerepresenta tempo e Poundsrepresenta massa. Isso leva a coisas como conversão de unidades, unidades básicas, precisão etc.


Houve / há tentativas de inserir algo assim em Java, por exemplo:

Os dois últimos parecem estar vivendo nessa coisa do github: https://github.com/unitsofmeasurement


C ++ possui unidades via Boost


O LabView vem com várias unidades .


Existem outros exemplos em outros idiomas. (edições são bem-vindas)


Isso é considerado uma boa prática?

Como você pode ver acima, quanto mais um idioma for usado para manipular valores com unidades, mais nativamente ele suporta unidades. O LabView é frequentemente usado para interagir com dispositivos de medição. Como tal, faz sentido ter esse recurso no idioma e usá-lo certamente seria considerado uma boa prática.

Mas em qualquer linguagem de alto nível de uso geral, onde a demanda é baixa por uma quantidade tão rigorosa, provavelmente é inesperada.

Ou existem problemas imprevistos de engenharia no caminho com um design tão restritivo?

Meu palpite seria: desempenho / memória. Se você lida com muitos valores, a sobrecarga de um objeto por valor pode se tornar um problema. Mas como sempre: a otimização prematura é a raiz de todo mal .

Eu acho que o maior "problema" é acostumar as pessoas, pois a unidade geralmente é definida implicitamente da seguinte forma:

class Adult
{
    ...
    public void setAge(int ageInYears){..}

As pessoas ficarão confusas quando tiverem que passar um objeto como um valor para algo que aparentemente poderia ser descrito de uma forma simples int, quando não estiverem familiarizados com os sistemas de unidades.


"As pessoas ficarão confusas quando tiverem que passar um objeto como um valor para algo que aparentemente pode ser descrito com um simples int..." --- para o qual temos aqui o diretor da menor surpresa. Boa pegada.
22818 Greg Greghardt

1
Acho que intervalos personalizados movem isso para fora do domínio de uma biblioteca de unidades e para os tipos dependentes
jk.

6

Ao contrário da resposta de null, definir um tipo para uma "unidade" pode ser benéfico se um número inteiro não for suficiente para descrever a medida. Por exemplo, o peso é frequentemente medido em várias unidades dentro do mesmo sistema de medição. Pense em "libras" e "onças" ou "quilogramas" e "gramas".

Se você precisar de um nível de medição mais granular, definir um tipo para a unidade é benéfico:

public struct Weight {
    private int pounds;
    private int ounces;

    public Weight(int pounds, int ounces) {
        // Value range checks go here
        // Throw exception if ounces is greater than 16?
    }

    // Getters go here
}

Para algo como "idade", recomendo calcular em tempo de execução com base na data de nascimento da pessoa:

public class Adult {
    private Date birthDate;

    public Interval getCurrentAge() {
        return calculateAge(Date.now());
    }

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}

2

O que você parece estar procurando é conhecido como tipos com tags . Eles são uma maneira de dizer "este é um número inteiro que representa a idade", enquanto "este também é um número inteiro, mas representa o peso" e "você não pode atribuir um ao outro". Observe que isso vai além das unidades físicas, como metros ou quilogramas: no meu programa eu posso ter "alturas de pessoas" e "distâncias entre pontos de um mapa", ambos medidos em metros, mas não compatíveis entre si desde a atribuição de um. o outro não faz sentido da perspectiva da lógica de negócios.

Alguns idiomas, como o Scala, suportam tipos de tags com bastante facilidade (veja o link acima). Em outros, você pode criar suas próprias classes de wrapper, mas isso é menos conveniente.

Validação, por exemplo, verificar se a altura de uma pessoa é "razoável" é outra questão. Você pode colocar esse código em sua Adultclasse (construtor ou setters) ou dentro de suas classes de tipo / wrapper marcadas. De certa forma, classes internas como URLou UUIDcumprem essa função (entre outras, por exemplo, fornecendo métodos utilitários).

Se o uso de tipos marcados ou classes de wrapper realmente ajudará a melhorar seu código, isso dependerá de vários fatores. Se seus objetos são simples e têm poucos campos, o risco de atribuí-los incorretamente é baixo e o código adicional necessário para usar tipos marcados pode não valer o esforço. Em sistemas complexos com estruturas complexas e muitos campos (especialmente se muitos deles compartilham o mesmo tipo primitivo), pode ser realmente útil.

No código que escrevo, geralmente crio classes de invólucro se passar por mapas. Tipos como eles Map<String, String>são muito opacos por si mesmos, então envolvê-los em classes com nomes significativos, como NameToAddressajuda muito. Obviamente, com tipos marcados, você pode escrever Map<Name, Address>e não precisar do wrapper para todo o mapa.

No entanto, para tipos simples, como Strings ou Inteiros, eu achei as classes de wrapper (em Java) muito incômodas. A lógica comercial regular não era tão ruim, mas surgiram vários problemas com a serialização desses tipos para JSON, mapeando-os para objetos de banco de dados etc. Você pode escrever mapeadores e ganchos para todas as grandes estruturas (por exemplo, Jackson e Spring Data), mas o trabalho extra e a manutenção relacionados a esse código compensarão o que você ganha com o uso desses invólucros. Claro, YMMV e em outro sistema, o saldo pode ser diferente.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.