leanmind logo leanmind text logo

Blog

BDD

Behaviour-driven Development es una técnica para tomar mejores requisitos de producto.

Combatiendo la obsesión de primitivos con objetos valor

Por Carlos Blé Jurado

La capa de código que contiene la lógica de dominio, debería hacer un uso limitado de los tipos primitivos del lenguaje, reemplazando la mayoría de ellos por nuestros propios tipos, con más semántica y con la capacidad de atraer comportamiento, permitiendo así programar paso de mensajes entre objetos.

OOP - Orientación a objetos

La orientación a objetos consiste en el paso de mensajes entre objetos. Se trata de pedirle a los objetos que nos resuelvan consultas o realicen operaciones (principio “tell don’t ask”), en lugar de limitarnos a pedirles los valores de sus atributos para luego operar con ellos.

Aunque lenguajes como Java están orientados a objetos, la mayoría de los proyectos que he visto en mi carrera usaban programación estructurada más que programación orientada a objetos. Esto lo vemos cuando hay clases que solo tienen campos de datos, pero no funciones, mientras que las clases que sí tienen funciones, no tienen datos propios, sino que los leen y los escriben en otros objetos. Este enfoque típico, ciertamente puede resultar modular, pero tiene más de programación estructurada que de orientación a objetos.

Los libros de Sandi Metz sobre orientación a objetos, son muy ilustrativos en este sentido y además se leen muy rápido.

Si nuestras clases encapsulan código y datos juntos, es más posible que estemos realizando un diseño orientado a objetos.

Value Object - Objetos valor

Los objetos valor o Value Object son aquellos que representan valores. Un objeto valor difiere de un objeto entidad en que no tiene un campo de identificador o clave única. A la hora de comparar dos objetos valor, usaremos su contenido para saber si son iguales. Por ejemplo, si quiero beber agua y me ofrecen dos vasos exactamente iguales y con la misma cantidad de agua, me da igual tomar de uno que del otro, para mí son iguales. Cuando modelamos objetos valor, debemos tener en cuenta que deben ser:

A veces, el uso de herramientas como un ORM lleva a modelar de manera forzada un objeto valor como entidad, solo porque en la base de datos las filas de la tabla necesitan un identificador único. Podría pasar esto con objetos como Color, Moneda, Role, Kind… Para evitar que esto suceda, mi recomendación es que los objetos creados para trabajar con el ORM se queden dentro de la capa de persistencia y no polucionen la capa de dominio. Así, podremos modelar orientado a objetos y representar cada concepto como valor o como entidad, atendiendo a su significado en el dominio, sin vernos afectados por las limitaciones expresivas del sistema de persistencia.

Primitive Obsession - Obsesión por los primitivos

Los tipos de datos primitivos son imprescindibles para el intercambio de datos con sistemas de terceros, como puede ser una capa GUI, una capa de Rest o la capa de datos. Son universales y fácilmente interoperables entre distintos proveedores. Sin embargo, son muy poco expresivos para programar reglas de negocio, es decir, para modelar conceptos de negocio. A veces, pueden tener valores incoherentes para el negocio. Otras veces, acabamos por codificar conceptos con valores, donde cada valor de una variable representa un concepto distinto. Y esto es difícil de descifrar cuando leemos el código pasados unos meses.

Abusamos de los tipos primitivos, usándolos para mucho más de lo que fueron pensados. A este mal olor del código, se le llama Primitive Obsession

Dentro de la capa de lógica, podremos expresar mejor nuestra intención programando si usamos nuestros propios tipos. Una forma de hacerlo es encapsular estos valores especiales en objetos valor, haciendo una envoltura (wrapper) de los primitivos.

Veamos un ejemplo de un objeto utilizado para especificar una cantidad de columnas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ColumnWidth {
    private final int columnWidth;

    private ColumnWidth(int columnWidth) {
        this.columnWidth = columnWidth;
    }

    static ColumnWidth createColumnWidth(int columnWidth) {
        if (columnWidth < 1){
            throw new IllegalArgumentException("The column width must be at least 1");
        }
        return new ColumnWidth(columnWidth);
    }

    public int value() {
        return columnWidth;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ColumnWidth)) return false;
        ColumnWidth that = (ColumnWidth) o;
        return columnWidth == that.columnWidth;
    }

    @Override
    public int hashCode() {
        return Objects.hash(columnWidth);
    }
}

La creación utiliza un método de factoría, porque realiza una validación que nos asegura que una vez construida una instancia del objeto, tiene un valor válido. Los constructores de los objetos no deben realizar validaciones ni operaciones de ningún tipo más que la asignación de campos, por eso usamos un método de factoría para la creación.

Cuando compare dos instancias de ColumnWidth, sabré si son iguales por su valor y nada más.

La extracción de objetos valor en un código existente puede llevarse a cabo mediante refactorización. En esta demostración grabada en vídeo doy ejemplos de cómo hacerlo y de paso explico los conceptos de este artículo de forma práctica y algunos otros principios más:

Aprovecho para dar las gracias a los amigos de Valencia Software Crafters por la oportunidad de compartir esta sesión, especialmente a Meri y a Zero.

La clase Text del vídeo quedó tal que así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Text {
    private final String text;

    private Text(String text) {
        this.text = text;
    }

    static Text createText(String text) {
        if (text == null){
            return new Text("");
        }

        return new Text(text);
    }

    public boolean fitsIn(ColumnWidth columnsWidth) {
        return text.length() <= columnsWidth.value();
    }

    public Text wrapLine(ColumnWidth columnsWidth) {
        return createText(text.substring(0, columnsWidth.value()) + "\n");
    }

    public Text removeLine(ColumnWidth columnsWidth) {
        return createText(text.substring(columnsWidth.value()));
    }

    public Text concat(Text otherText) {
        return createText(text + otherText.text);
    }

    public Text wordWrap(ColumnWidth columnsWidth){
        if (fitsIn(columnsWidth)){
            return this;
        }
        Text wrappedLine = wrapLine(columnsWidth);
        Text remainingText = removeLine(columnsWidth);
        return wrappedLine.concat(remainingText.wordWrap(columnsWidth));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Text)) return false;
        Text text1 = (Text) o;
        return Objects.equals(text, text1.text);
    }

    @Override
    public int hashCode() {
        return Objects.hash(text);
    }

    @Override
    public String toString() {
        return text;
    }
}

Cuidado con abstracciones inadecuadas

Uno de los peligros de extraer objetos que pretenden representar conceptos, es que realmente no consigamos modelar bien dichos conceptos y nos queden abstracciones marcianas, clases difíciles de entender dado que no terminan de encajar de verdad con el dominio.

Como precaución para evitar abstracciones dañinas, podemos empezar usando tipos primitivos al comienzo del desarrollo de una funcionalidad y cuando vamos teniendo más claridad sobre el dominio, podemos hacer refactor hacia tipos propios. Para eso, será clave que hayamos escrito test automáticos, para asegurarnos que podemos hacer cambios sin romper nada.

Espero que este artículo te ayude a identificar cómo podrías hacer explícitos algunos conceptos en tu código creando tus propios tipos.

Publicado el 20/04/2020 por

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers