Capítulo 4. La obsesión primitiva
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
La obsesión por lo primitivo es una obsesión primitiva.
Rich Hickey
4.0 Introducción
Muchos ingenieros de software piensan que el software consiste en "mover datos"; las escuelas y los libros de texto orientados a objetos se centran en los datos y los atributos cuando enseñan a modelar el mundo real. Éste era un sesgo cultural que se enseñaba en las universidades durante los años 80 y 90. Las tendencias del sector empujaron a los ingenieros a crear diagramas entidad-relación (ERD) y a razonar sobre los datos empresariales en lugar de centrarse en el comportamiento.
Los datos son más relevantes que nunca. La ciencia de datos está creciendo y el mundo gira en torno a los datos. Necesitas crear un simulador para gestionar y proteger los datos y exponer el comportamiento, ocultando al mismo tiempo la información y la representación accidental para evitar el acoplamiento. Las recetas de este capítulo te ayudarán a identificar los objetos pequeños y a ocultar la representación accidental. Descubrirás muchos objetos pequeños cohesionados y los reutilizarás en muchos contextos diferentes.
Cohesión
La cohesión es una medida de el grado en que los elementos de una misma clase o módulo de software trabajan juntos para lograr un propósito único y bien definido. Se refiere a lo estrechamente relacionados que están los objetos entre sí y con el objetivo general del módulo. Puedes considerar que una cohesión alta es una propiedad deseable en el diseño de software, ya que los elementos de un módulo están estrechamente relacionados y trabajan juntos con eficacia para lograr un objetivo específico.
4.1 Crear objetos pequeños
Solución
Busca responsabilidades para objetos pequeños en el MAPPER y reifícalos.
Debate
Desde los primeros días de la informática, los ingenieros mapean todo lo que ven a los familiares tipos de datos primitivos, como String, Entero y Colección. El mapeo a esos tipos de datos viola a veces los principios de abstracción y fail fast. El nombre de una Persona tiene un comportamiento diferente al de una cadena, como puedes ver en el siguiente ejemplo:
public
class
Person
{
private
final
String
name
;
public
Person
(
String
name
)
{
this
.
name
=
name
;
}
}
El concepto de nombre está cosificado:
public
class
Name
{
private
final
String
name
;
public
Name
(
String
name
)
{
this
.
name
=
name
;
// Name has its own creation rules, comparison, etc.
// Might be different than a string
}
}
public
class
Person
{
private
final
Name
name
;
public
Person
(
Name
name
)
{
// Name is created as a valid one,
// you don't need to add validations here
this
.
name
=
name
;
}
}
Toma como ejemplo la palabra de cinco letras del juego Wordle. Una palabra Wordle no tiene las mismas responsabilidades que un char(5)
y no se mapea en la bijección. Si quieres crear un juego Wordle, verás una bijección entre una palabra Wordle distinta de una String
o c
har(5)
, ya que no tienen las mismas responsabilidades. Por ejemplo, no es responsabilidad de un String
averiguar cuántas coincidencias tiene con la palabra Wordle secreta. Y no es responsabilidad de una palabra Wordle concatenar.
Wordle
Wordle es un popular juego online de adivinar palabras en el que tienes seis intentos para adivinar una palabra de cinco letras seleccionada por el juego. Cada vez que adivines una palabra de cinco letras, el juego te indicará qué letras son correctas y están en la posición correcta (marcadas con un cuadrado verde) y qué letras son correctas pero están en la posición incorrecta (marcadas con un cuadrado amarillo).
En un número muy reducido de sistemas de misión crítica, existe un compromiso entre abstracción y rendimiento. Pero para evitar una optimización prematura (véase el Capítulo 16, "Optimización prematura"), debes confiar en los ordenadores modernos y en las optimizaciones de las máquinas virtuales y, como siempre, debes ceñirte a las pruebas en escenarios del mundo real. Encontrar objetos pequeños es una tarea muy difícil, que requiere experiencia para hacer un buen trabajo y evitar el sobrediseño. No hay una bala de plata a la hora de elegir cómo y cuándo mapear algo.
No hay balas de plata
El concepto de "ninguna bala de plata" es una frase acuñada por el informático y pionero de la ingeniería de software Fred Brooks en su ensayo de 1986 "Ninguna bala de plata: Esencia y accidentes de la ingeniería de software". Brooks argumenta que no existe una única solución o enfoque que pueda resolver todos los problemas o mejorar significativamente la productividad y eficacia del desarrollo de software.
4.2 Reificar los datos primitivos
Solución
Utiliza objetos pequeños en lugar de primitivos.
Debate
Supón que estás construyendo un servidor web:
int
port
=
8080
;
InetSocketAddress
in
=
open
(
"example.org"
,
port
);
String
uri
=
urifromPort
(
"example.org"
,
port
);
String
address
=
addressFromPort
(
"example.org"
,
port
);
String
path
=
pathFromPort
(
"example.org"
,
port
);
Este ejemplo ingenuo tiene muchos problemas. Viola el principio "Dilo, no lo preguntes" (véase la Receta 3.3, "Eliminar los definidores de los objetos") y el principio de fallo rápido. Además, no sigue la regla de diseño MAPPER y viola el principio del subconjunto. Hay manipulación de código duplicado por todas partes que se necesita para utilizar estos objetos, ya que no separa claramente el "qué" del "cómo".
La industria es muy perezosa cuando se trata de crear objetos pequeños y también de separar el qué y el cómo, ya que requiere un esfuerzo adicional descubrir tales abstracciones. Es importante fijarse en el protocolo y el comportamiento de los componentes pequeños y olvidarse de intentar comprender las interioridades de cómo funcionan las cosas. Una solución conforme a la biyección podría ser:
Port
server
=
Port
.
parse
(
this
,
"www.example.org:8080"
);
// Port is a small object with responsibilities and protocol
Port
in
=
server
.
open
(
this
);
// returns a port, not a number
URI
uri
=
server
.
asUri
(
this
);
// returns an URI
InetSocketAddress
address
=
server
.
asInetSocketAddress
();
// returns an Address
Path
path
=
server
.
path
(
this
,
"/index.html"
);
// returns a Path
// all of them are validated small bijection objects with very few and precise
// responsibilities
Ver también
4.3 Reificar matrices asociativas
Solución
Utiliza matrices para la creación rápida de prototipos y utiliza objetos para las cosas serias.
Debate
Prototipado rápido
El prototipado rápido se utiliza en el desarrollo de productos para crear rápidamente prototipos que funcionen y validarlos con el usuario final. Esta técnica permite a diseñadores e ingenieros probar y refinar un diseño antes de crear un código limpio coherente, robusto y elegante.
Las matrices asociativas son una forma práctica de representar objetos anémicos. Si los encuentras en el código, esta receta te ayudará a reificar el concepto y sustituirlos. Tener objetos ricos es beneficioso para limpiar el código, de modo que puedas fallar rápido, mantener la integridad, evitar la duplicación de código y ganar cohesión.
Mucha gente sufre de obsesión por lo primitivo y cree que esto es sobrediseño. Diseñar software consiste en tomar decisiones y comparar compensaciones. El argumento del rendimiento no es válido hoy en día, ya que las máquinas virtuales modernas pueden tratar eficazmente pequeños objetos de corta duración.
He aquí un ejemplo de código de obsesión anémico y primitivo:
$coordinate
=
array
(
'latitude'
=>
1000
,
'longitude'
=>
2000
);
// They are just arrays. A bunch of raw data
Esto es más exacto según el concepto de biyección:
final
class
GeographicCoordinate
{
function
__construct
(
$latitudeInDegrees
,
$longitudeInDegrees
)
{
$this
->
longitude
=
$longitudeInDegrees
;
$this
->
latitude
=
$latitudeInDegrees
;
}
}
$coordinate
=
new
GeographicCoordinate
(
1000
,
2000
);
// Should throw an error since these values don’t exist on Earth
Necesitas tener objetos que sean válidos desde el principio:
final
class
GeographicCoordinate
{
function
__construct
(
$latitudeInDegrees
,
$longitudeInDegrees
)
{
$this
->
longitude
=
$longitudeInDegrees
;
$this
->
latitude
=
$latitudeInDegrees
;
}
}
$coordinate
=
new
GeographicCoordinate
(
1000
,
2000
);
// Should throw an error since these values don't exist on Earth
final
class
GeographicCoordinate
{
function
__construct
(
$latitudeInDegrees
,
$longitudeInDegrees
)
{
if
(
!
$this
->
isValidLatitude
(
$latitudeInDegrees
))
{
throw
new
InvalidLatitudeException
(
$latitudeInDegrees
);
}
$this
->
longitude
=
$longitudeInDegrees
;
$this
->
latitude
=
$latitudeInDegrees
;
}
}
}
$coordinate
=
new
GeographicCoordinate
(
1000
,
2000
);
// throws an error since these values don't exist on Earth
Hay un objeto pequeño oscuro (ver Receta 4.1, "Crear objetos pequeños") para modelar la latitud:
final
class
Latitude
{
function
__construct
(
$degrees
)
{
if
(
!
$degrees
->
between
(
-
90
,
90
))
{
throw
new
InvalidLatitudeException
(
$degrees
);
}
}
}
final
class
GeographicCoordinate
{
function
distanceTo
(
GeographicCoordinate
$coordinate
)
{
}
function
pointInPolygon
(
Polygon
$polygon
)
{
}
}
// Now you are in the geometry world (and not in the world of arrays anymore).
// You can safely do many exciting things.
Al crear objetos, no debes pensar en ellos como datos. Se trata de un error muy común. Debes mantenerte fiel al concepto de biyección y descubrir objetos del mundo real.
4.4 Eliminar los abusos de las cadenas
Solución
Utiliza abstracciones reales y objetos reales en lugar de manipulaciones accidentales de cadenas.
Debate
No abuses de las cuerdas. Favorece los objetos reales. Encuentra protocolos ausentes para distinguirlos de las cadenas. Este código hace muchas manipulaciones primitivas de cadenas:
$schoolDescription
=
'College of Springfield'
;
preg_match
(
'/[^ ]*$/'
,
$schoolDescription
,
$results
);
$location
=
$results
[
0
];
// $location = 'Springfield'.
$school
=
preg_split
(
'/[\s,]+/'
,
$schoolDescription
,
3
)[
0
];
//'College'
Puedes convertir el código en una versión más declarativa:
class
School
{
private
$name
;
private
$location
;
function
description
()
{
return
$this
->
name
.
' of '
.
$this
->
location
->
name
;
}
}
Al encontrar objetos presentes en el MAPPER, tu código es más declarativo, más comprobable y puede evolucionar y cambiar más rápidamente. También puedes añadir restricciones a las nuevas abstracciones. Utilizar cadenas para mapear objetos reales es una obsesión primitiva y un síntoma de optimización prematura(véase el Capítulo 16, "Optimización prematura"). A veces, la versión de cadenas es un poco más eficaz. Si tienes que decidir entre aplicar esta receta o hacer manipulaciones de bajo nivel, crea siempre escenarios de uso reales y encuentra mejoras concluyentes y significativas.
4.5 Reificar marcas de tiempo
Debate
Gestionar las marcas de tiempo en distintas zonas horarias y con escenarios de gran concurrencia es un problema bien conocido. A veces, puedes confundir el problema de tener elementos secuenciales y ordenados con la (posible) solución de ponerles marcas de tiempo. Como siempre, necesitas comprender los problemas esenciales que hay que resolver antes de adivinar implementaciones accidentales.
Una posible solución es utilizar una autoridad centralizada o algunos complejos algoritmos de consenso descentralizados. Esta receta cuestiona la necesidad de los sellos de tiempo cuando sólo necesitas una secuencia ordenada. Los sellos de tiempo son muy populares en muchos idiomas y están omnipresentes. Necesitas utilizar timestamps nativos sólo para modelar timestamps si los encuentras en la bijección.
Aquí tienes algunos problemas con las marcas de tiempo:
import
time
# ts1 and ts2 stores the time in seconds
ts1
=
time
.
time
()
ts2
=
time
.
time
()
# might be the same!!
Aquí tienes una solución mejor sin marcas de tiempo, ya que sólo necesitas un comportamiento secuencial:
numbers
=
range
(
1
,
100000
)
# create a sequence of numbers and use them with a hotspot
# or
sequence
=
nextNumber
()
4.6 Reificar subconjuntos como objetos
Soluciones
Crea objetos pequeños y valida un dominio restringido.
Debate
Los subconjuntos son un caso especial de un olor primitivo de obsesión. Los objetos subconjunto están presentes en la biyección, por lo que debes crearlos en tu simulador. Además, cuando intentes crear un objeto no válido, debe romperse inmediatamente, siguiendo el principio de fail fast (ver Capítulo 13, "Fail Fast"). Algunos ejemplos de violaciones de subconjuntos son: los correos electrónicos son un subconjunto de las cadenas, las edades válidas son un subconjunto de los números reales y los puertos son un subconjunto de los números enteros. Los objetos invisibles tienen reglas que debes hacer cumplir en un único punto.
Mira este ejemplo:
validDestination
=
"destination@example.com"
invalidDestination
=
"destination.example.com"
// No error is thrown
Aquí tienes una restricción de dominio mejor:
public
class
EmailAddress
{
public
String
emailAddress
;
public
EmailAddress
(
String
address
)
{
string
expressions
=
@
"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$"
;
if
(
!
Regex
.
IsMatch
(
,
expressions
)
{
throw
new
Exception
(
'
Invalid
address
'
);
}
this
.
emailAddress
=
address
;
}
}
destination
=
new
EmailAddress
(
"destination@example.com"
);
Esta solución no debe confundirse con la versión anémica de Java. Debes ser fiel a la biyección del mundo real.
4.7 Reificar las validaciones de cadenas
Solución
Busca los objetos de dominio que faltan al validar cadenas y reifícalos.
Debate
El software serio tiene muchas validaciones de cadenas. A menudo, no están en los lugares correctos, lo que conduce a un software frágil y corrupto. La solución sencilla es construir sólo abstracciones válidas y del mundo real:
// First Example: Address Validation
class
Address
{
function
__construct
(
string
$emailAddress
)
{
// String validation on Address class violates
// Single Responsibility Principle
$this
->
validateEmail
(
$emailAddress
);
// ...
}
private
function
validateEmail
(
string
$emailAddress
)
{
$regex
=
"/[a-zA-Z0-9_-.+]+@[a-zA-Z0-9-]+.[a-zA-Z]+/"
;
// Regex is a sample / It might be wrong
// Emails and Urls should be first class objects
if
(
!
preg_match
(
$regex
,
$emailAddress
))
{
throw
new
Exception
(
'Invalid email address '
.
emailAddress
);
}
}
}
// Second Example: Wordle
class
Wordle
{
function
validateWord
(
string
$wordleword
)
{
// Wordle word should be a real world entity. Not a subset of Strings
}
}
He aquí una solución mejor:
// First Example: Address Validation
class
Address
{
function
__construct
(
EmailAddress
$emailAddress
)
{
// Email is always valid / Code is cleaner and not duplicated
// ...
}
}
class
EmailAddress
{
// You can reuse this object many times avoiding copy-pasting
string
$address
;
private
function
__construct
(
string
$emailAddress
)
{
$regex
=
"/[a-zA-Z0-9_-.+]+@[a-zA-Z0-9-]+.[a-zA-Z]+/"
;
// Regex is a sample / It might be wrong
// Emails and Urls are first class objects
if
(
!
preg_match
(
$regex
,
$emailAddress
))
{
throw
new
Exception
(
'Invalid email address '
.
emailAddress
);
}
$this
->
address
=
$emailAddress
;
}
}
// Second Example: Wordle
class
Wordle
{
function
validateWord
(
WordleWord
$wordleword
)
{
// Wordle word is a real world entity. Not a subset of string
}
}
class
WordleWord
{
function
__construct
(
string
$word
)
{
// Avoid building invalid Wordle words
// For example length != 5
}
}
Principio de Responsabilidad Única
El principio de responsabilidad única establece que cada módulo o clase de un sistema de software debe tener responsabilidad sobre una única parte de la funcionalidad proporcionada por el software y esa responsabilidad debe estar totalmente encapsulada por la clase. En otras palabras, una clase sólo debe tener una razón para cambiar.
Los objetos pequeños son difíciles de encontrar. Pero siguen el principio de "falla rápido" cuando intentas crear objetos no válidos. El nuevo objeto reificado también sigue el principio de responsabilidad única y el principio de no repetirse. Tener estas abstracciones te obliga a implementar un comportamiento específico que ya está disponible en los objetos que encapsula. Por ejemplo, un WordleWord
no es un String
, pero puede que necesites algunas funciones.
Principio de no repetirse
El principio de no repetirse (DRY) establece que los sistemas de software deben evitar la redundancia y la repetición de código. El objetivo del principio DRY es mejorar la mantenibilidad, flexibilidad y comprensibilidad del software reduciendo la cantidad de conocimientos, código e información duplicados.
Un contraargumento sobre la eficiencia evitando estas nuevas indirecciones es un signo deoptimización prematura, a menos que tengas pruebas concretas de una penalización sustancial con escenarios de uso real de tus clientes. Crear estos pequeños conceptos nuevos mantiene el modelo fiel a la biyección y garantiza que tus modelos estén siempre sanos.
Principios SÓLIDOS
SOLID es un mnemotécnico que significa cinco principios de programación orientada a objetos. Fueron definidos por Robert Martin y son directrices y heurísticos, no reglas rígidas. Se definen en los capítulos relacionados:
-
Principio de responsabilidad única (ver Receta 4.7, "Reificar las validaciones de cadenas")
-
Principio abierto-cerrado (ver Receta 14.3, "Reificar variables booleanas")
-
Principio de sustitución de Liskov (ver Receta 19.1, "Romper la herencia profunda")
-
Interfaz principio de segregación (ver Receta 11.9, "Romper interfaces gordas")
-
Dependencia principio de inversión (ver Receta 12.4, "Eliminar interfaces de un solo uso")
4.8 Eliminar propiedades innecesarias
Solución
Elimina las propiedades accidentales. Añade el comportamiento necesario y luego añade propiedades accidentales para apoyar el comportamiento definido.
Debate
Muchas escuelas de programación te dicen que identifiques rápidamente las partes de los objetos y luego construyas funciones a su alrededor. Tales modelos suelen estar acoplados y son menos mantenibles que los creados en función del comportamiento deseado. Siguiendo la premisa de YAGNI(ver Capítulo 12, "YAGNI"), verás que muchas veces no necesitas estos atributos.
Cada vez que quieren modelar a una persona o a un empleado, los programadores junior o los estudiantes añaden un atributo id o nombre sin pensar si realmente los van a necesitar. Tienes que añadir atributos "bajo demanda" cuando haya suficientes pruebas de comportamiento. Los objetos no son "soportes de datos".
Este es un ejemplo clásico de enseñanza:
class
PersonInQueue
attr_accessor
:name
,
:job
def
initialize
(
name
,
job
)
@name
=
name
@job
=
job
end
end
Si empiezas a centrarte en el comportamiento, podrás construir mejores modelos:
class
PersonInQueue
def
moveForwardOnePosition
# implement protocol
end
end
Una técnica asombrosa para descubrir comportamientos es el desarrollo dirigido por pruebas, en el que te ves obligado a empezar a iterar el comportamiento y el protocolo y a aplazar todo lo que puedas la implementación accidental.
Desarrollo basado en pruebas
El desarrollo dirigido por pruebas (TDD) es un proceso de desarrollo de software que se basa en la repetición de un ciclo de desarrollo muy corto: primero, el desarrollador escribe un caso de prueba automatizado que falla y que define una mejora deseada o un nuevo comportamiento, luego produce un código de producción mínimo para superar esa prueba y, por último, refactoriza el nuevo código para que cumpla unos estándares aceptables. Uno de los principales objetivos de TDD es facilitar el mantenimiento del código asegurándose de que está bien estructurado y sigue unos buenos principios de diseño. También ayuda a detectar defectos en una fase temprana del proceso de desarrollo, ya que cada nuevo fragmento de código se prueba en cuanto se escribe.
4.9 Crear intervalos de fechas
Solución
Reifica este pequeño objeto y respeta la regla MAPPER.
Debate
Esta receta presenta una abstracción muy común que podrías pasar por alto y tiene los mismos problemas que has visto en las otras recetas de este capítulo: abstracciones que faltan, código duplicado, invariante no aplicada (consulta la Receta 13.2, "Aplicar precondiciones"), obsesión primitiva y violación del principio "fail fast". La restricción "la fecha de inicio debe ser inferior a la fecha de finalización" significa que la fecha de inicio de un determinado intervalo debe ser anterior a la fecha de finalización del mismo intervalo.
La "fechadesde" debe ser una fecha anterior en el tiempo a la "fechahasta ". Esta restricción existe para garantizar que el intervalo que se define tiene sentido lógico y que las fechas utilizadas para definirlo están en el orden correcto. Lo sabes, pero te olvidas de crear el objeto Interval
. ¿Crearías un Date
como un par de tres números enteros? Desde luego que no.
He aquí un ejemplo anémico:
val
from
=
LocalDate
.
of
(
2018
,
12
,
9
)
val
to
=
LocalDate
.
of
(
2022
,
12
,
22
)
val
elapsed
=
elapsedDays
(
from
,
to
)
fun
elapsedDays
(
fromDate
:
LocalDate
,
toDate
:
LocalDate
):
Long
{
return
ChronoUnit
.
DAYS
.
between
(
fromDate
,
toDate
)
}
// You need to apply this short function
// or the inline version many times in your code
// You don't check fromDate to be less than toDate
// You can make accounting numbers with a negative value
Después de reificar el objeto Interval
:
data
class
Interval
(
val
fromDate
:
LocalDate
,
val
toDate
:
LocalDate
)
{
init
{
if
(
fromDate
>=
toDate
)
{
throw
IllegalArgumentException
(
"From date must be before to date"
)
}
// Of course the Interval must be immutable
// By using the keyword 'data'
}
fun
elapsedDays
():
Long
{
return
ChronoUnit
.
DAYS
.
between
(
fromDate
,
toDate
)
}
}
val
from
=
LocalDate
.
of
(
2018
,
12
,
9
)
val
to
=
LocalDate
.
of
(
2002
,
12
,
22
)
val
interval
=
Interval
(
from
,
to
)
// Invalid
Este es un olor a obsesión primitiva y está relacionado con la forma en que modelas las cosas. Si encuentras un software al que le faltan validaciones sencillas, sin duda necesita alguna reificación.
Get Libro de cocina de código limpio now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.