Uso de la verificación de tipo estático para protegerse contra errores comerciales

13

Soy un gran fanático de la verificación de tipos estáticos. Le impide cometer errores estúpidos como este:

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

Pero no te impide cometer errores 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

El problema surge cuando usa el mismo tipo para representar evidentemente diferentes tipos de información. Estaba pensando que una buena solución para esto sería extender la Integerclase, solo para evitar errores de lógica de negocios, pero no para agregar funcionalidad. Por ejemplo:

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

¿Se considera esto una buena práctica? ¿O hay problemas de ingeniería imprevistos en el futuro con un diseño tan restrictivo?

J-bob
fuente
55
¿ a.SetAge( new Age(150) )Todavía no se compilará?
John Wu
1
El tipo int de Java tiene un rango fijo. Claramente, le gustaría tener enteros con rangos personalizados, por ejemplo, Entero <18, 110>. Está buscando tipos de refinamiento o tipos dependientes, que ofrecen algunos idiomas (no convencionales).
Theodoros Chatzigiannakis
3
¡Lo que estás hablando es una excelente manera de hacer diseño! Lamentablemente, Java tiene un sistema de tipos tan primitivo que es difícil hacerlo bien. E incluso si trata de hacerlo, puede provocar efectos secundarios como un menor rendimiento o una menor productividad del desarrollador.
Eufórico el
@JohnWu sí, su ejemplo aún se compilará, pero ese es el único punto de falla para esa lógica. Una vez que declara su new Age(...)objeto, no puede asignarlo incorrectamente a una variable de tipo Weighten ningún otro lugar. Reduce la cantidad de lugares donde pueden ocurrir errores.
J-bob

Respuestas:

12

Básicamente está solicitando un sistema de unidad (no, no pruebas de unidad, "unidad" como en "unidad física", como metros, voltios, etc.).

En su código Agerepresenta el tiempo y Poundsrepresenta la masa. Esto lleva a cosas como la conversión de unidades, unidades base, precisión, etc.


Hubo / hay intentos de introducir algo así en Java, por ejemplo:

Los dos posteriores parecen estar viviendo en esta cosa github: https://github.com/unitsofmeasurement


C ++ tiene unidades a través de Boost


LabView viene con un montón de unidades .


Hay otros ejemplos en otros idiomas. (las ediciones son bienvenidas)


¿Se considera esto una buena práctica?

Como puede ver arriba, cuanto más probable es que se use un lenguaje para manejar valores con unidades, más nativamente admite unidades. LabView se usa a menudo para interactuar con dispositivos de medición. Como tal, tiene sentido tener una característica de este tipo en el lenguaje y usarlo sin duda se consideraría una buena práctica.

Pero en cualquier lenguaje de alto nivel de propósito general, donde la demanda es baja para tal cantidad de rigor, probablemente sea inesperado.

¿O hay problemas de ingeniería imprevistos en el futuro con un diseño tan restrictivo?

Mi conjetura sería: rendimiento / memoria. Si maneja muchos valores, la sobrecarga de un objeto por valor puede convertirse en un problema. Pero como siempre: la optimización prematura es la raíz de todo mal .

Creo que el "problema" más grande es hacer que la gente se acostumbre, ya que la unidad generalmente se define implícitamente así:

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

Las personas se confundirán cuando tengan que pasar un objeto como valor de algo que aparentemente podría describirse con un simple int, cuando no estén familiarizados con los sistemas de unidades.

nulo
fuente
"Las personas se confundirán cuando tengan que pasar un objeto como valor de algo que aparentemente podría describirse con un simple int..." --- para lo cual tenemos al Director de Menos Sorpresa como guía aquí. Buena atrapada.
Greg Burghardt
1
Creo que los rangos personalizados mueven esto fuera del ámbito de una biblioteca de unidades a los tipos dependientes
jk.
6

En contraste con la respuesta nula, definir un tipo para una "unidad" puede ser beneficioso si un entero no es suficiente para describir la medición. Por ejemplo, el peso a menudo se mide en múltiples unidades dentro del mismo sistema de medición. Piense en "libras" y "onzas" o "kilogramos" y "gramos".

Si necesita un nivel de medición más granular, es beneficioso definir un tipo para la unidad:

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 "edad", recomiendo calcular eso en tiempo de ejecución en función de la fecha de nacimiento de la persona:

public class Adult {
    private Date birthDate;

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

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}
Greg Burghardt
fuente
2

Lo que parece estar buscando se conoce como tipos etiquetados . Son una forma de decir "este es un número entero que representa la edad", mientras que "también es un número entero pero representa el peso" y "no se puede asignar uno al otro". Tenga en cuenta que esto va más allá de las unidades físicas, como metros o kilogramos: es posible que tenga en mi programa "alturas de personas" y "distancias entre puntos en un mapa", ambas medidas en metros, pero no son compatibles entre sí desde que asigné una a el otro no tiene sentido desde la perspectiva de la lógica empresarial.

Algunos idiomas, como Scala, admiten tipos etiquetados con bastante facilidad (consulte el enlace anterior). En otros, puede crear sus propias clases de contenedor, pero esto es menos conveniente.

La validación, por ejemplo, verificar que la altura de una persona sea "razonable" es otro problema. Puede poner dicho código en su Adultclase (constructor o establecedores), o dentro de sus clases de tipo / contenedor etiquetados. En cierto modo, las clases integradas tales como URLo UUIDcumplen tal función (entre otras, por ejemplo, proporcionar métodos de utilidad).

Si usar tipos etiquetados o clases de envoltura realmente ayudará a mejorar su código, dependerá de varios factores. Si sus objetos son simples y tienen pocos campos, el riesgo de asignarlos incorrectamente es bajo y el código adicional necesario para usar tipos etiquetados puede no valer la pena. En sistemas complejos con estructuras complejas y muchos campos (especialmente si muchos de ellos comparten el mismo tipo primitivo), en realidad puede ser útil.

En el código que escribo, a menudo creo clases de contenedor si paso mapas. Los tipos como Map<String, String>son muy opacos por sí mismos, por lo que es muy útil incluirlos en clases con nombres significativos como NameToAddress. Por supuesto, con los tipos etiquetados, podría escribir Map<Name, Address>y no necesitar el contenedor para todo el mapa.

Sin embargo, para tipos simples como Strings o Integers, he encontrado que las clases de contenedor (en Java) son demasiado molestas. La lógica comercial regular no era tan mala, pero surgieron una serie de problemas al serializar estos tipos a JSON, asignarlos a objetos de base de datos, etc. Puede escribir mapeadores y ganchos para todos los grandes marcos (por ejemplo, Jackson y Spring Data), pero el trabajo y el mantenimiento adicionales relacionados con este código compensarán lo que gane al usar estos contenedores. Por supuesto, YMMV y en otro sistema, el balance puede ser diferente.

Michał Kosmulski
fuente