Logo Studenta

Recuperatorio parcial 1-PP2

¡Este material tiene más páginas!

Vista previa del material en texto

Value Object:
Un objeto pequeño y simple, como el dinero o un rango de fechas, cuya igualdad no
se basa en la identidad. Con sistemas de objetos de varios tipos, he encontrado útil
distinguir entre objetos de referencia y objetos de valor. De los dos, un objeto de valor suele
ser más pequeño; es similar a los tipos primitivos presentes en muchos lenguajes que no
son puramente orientados a objetos.
Cómo funciona
Definir la diferencia entre un objeto de referencia y un objeto de valor puede ser algo
complicado. En un sentido amplio, nos gusta pensar que los objetos de valor son objetos
pequeños, como un objeto de dinero o una fecha, mientras que los objetos de referencia
son grandes, como un pedido o un cliente. Esta definición es útil pero molesta por su
informalidad.
La diferencia clave entre los objetos de referencia y los objetos de valor radica en
cómo manejan la igualdad. Un objeto de referencia utiliza la identidad como base para la
igualdad, tal vez la identidad dentro del sistema de programación, como la identidad
incorporada de los lenguajes de programación orientados a objetos, o tal vez algún tipo de
número de identificación, como la clave primaria en una base de datos relacional. Un objeto
de valor basa su noción de igualdad en los valores de los campos dentro de la clase. Por lo
tanto, dos objetos de fecha pueden ser iguales si sus valores de día, mes y año son iguales.
Esta diferencia se manifiesta en cómo se manejan. Como los objetos de valor son
pequeños y fáciles de crear, a menudo se pasan por valor en lugar de por referencia.
Realmente no te importa cuántos objetos existen en tu sistema que representen el 18 de
marzo de 2001. Tampoco te importa si dos objetos comparten el mismo objeto de fecha
física o si tienen copias diferentes pero iguales.
La mayoría de los lenguajes no tienen una facilidad especial para los objetos de
valor. Para que los objetos de valor funcionen correctamente en estos casos, es una muy
buena idea hacerlos inmutables, es decir, una vez creados, ninguno de sus campos cambia.
La razón de esto es evitar errores de aliasing. Un error de aliasing ocurre cuando dos
objetos comparten el mismo objeto de valor y uno de los propietarios cambia los valores en
él. Por lo tanto, si Martín tiene una fecha de contratación el 18 de marzo y sabemos que
Cindy fue contratada el mismo día, podemos establecer la fecha de contratación de Cindy
como la misma que la de Martín. Si luego Martín cambia el mes en su fecha de contratación
a mayo, la fecha de contratación de Cindy también cambia. No importa si es correcto o no,
no es lo que la gente espera. Por lo general, con valores pequeños como este, la gente
espera cambiar una fecha de contratación reemplazando el objeto de fecha existente por
uno nuevo. Hacer que los objetos de valor sean inmutables cumple con esa expectativa.
Los objetos de valor no deben persistirse como registros completos. En su lugar, utiliza
Valor Incrustado (268) o LOB Serializado (272). Dado que los objetos de valor son
pequeños, Valor Incrustado (268) suele ser la mejor opción, ya que también permite
consultas SQL utilizando los datos de un objeto de valor.
Si estás realizando mucha serialización binaria, es posible que descubras que optimizar la
serialización de objetos de valor puede mejorar el rendimiento, especialmente en lenguajes
como Java que no tratan los objetos de valor de manera especial. Para un ejemplo de un
objeto de valor, consulta Money (488).
Cuándo usarlo
Trata algo como un objeto de valor cuando estés basando la igualdad en algo que no
sea una identidad. Vale la pena considerarlo para cualquier objeto pequeño que sea fácil de
crear.
Colisiones de nombres
He visto que el término "objeto de valor" se utiliza para este patrón durante bastante
tiempo. Lamentablemente, recientemente he visto a la comunidad de J2EE [Alur et al.] usar
el término "objeto de valor" para referirse al objeto de transferencia de datos (401), lo que
ha causado un revuelo en la comunidad de patrones. Esto es solo uno de esos choques de
nombres que ocurren todo el tiempo en este negocio. Recientemente, [Alur et al.] decidieron
utilizar el término "objeto de transferencia" en su lugar.
Continúo utilizando "objeto de valor" de esta manera en este texto. Si nada más, me
permite ser consistente con mis escritos anteriores.
Special Case
Una subclase que proporciona un comportamiento especial para casos particulares.
Los nulos son elementos incómodos en los programas orientados a objetos porque anulan
el polimorfismo. Por lo general, puedes invocar "foo" libremente en una referencia de
variable de un tipo determinado sin preocuparte si el elemento es del tipo exacto o una
subclase. Con un lenguaje fuertemente tipado, incluso puedes hacer que el compilador
verifique si la llamada es correcta. Sin embargo, dado que una variable puede contener un
valor nulo, puedes encontrarte con un error en tiempo de ejecución al invocar un mensaje
en nulo, lo que te proporcionará una traza de pila agradable y amigable.
Si es posible que una variable sea nula, debes recordar rodearla con código de prueba de
nulo para hacer lo correcto si se encuentra un nulo. A menudo, lo correcto es lo mismo en
muchos contextos, por lo que terminas escribiendo código similar en muchos lugares,
cometiendo el pecado de la duplicación de código.
Los nulos son un ejemplo común de este tipo de problemas y otros surgen con
regularidad. En los sistemas numéricos, debes lidiar con el infinito, que tiene reglas
especiales para cosas como la suma que rompen las invariantes habituales de los números
reales. Una de mis primeras experiencias en software empresarial fue con un cliente de
servicios públicos que no era completamente conocido, al que se referían como "ocupante".
Todos estos implican alterar el comportamiento habitual del tipo.
En lugar de devolver nulo o algún valor extraño, devuelve un Caso Especial que tenga la
misma interfaz que espera el llamador.
Cómo funciona
La idea básica es crear una subclase para manejar el Caso Especial. Así, si tienes
un objeto de cliente y deseas evitar comprobaciones nulas, creas un objeto de cliente nulo.
Toma todos los métodos para el cliente y sobrescríbelos en el Caso Especial para
proporcionar un comportamiento inofensivo. Luego, cada vez que tengas un nulo, inserta
una instancia de cliente nulo en su lugar.
Por lo general, no hay motivo para distinguir entre diferentes instancias de cliente
nulo, por lo que a menudo se puede implementar un Caso Especial con un objeto
compartido (flyweight) [Gang of Four]. No se puede hacer todo el tiempo. Para un cliente de
servicios públicos, puedes acumular cargos contra un cliente ocupante incluso si no puedes
hacer mucho en términos de facturación, por lo que es importante mantener a los ocupantes
separados.
Un nulo puede significar cosas diferentes. Un cliente nulo puede significar que no hay
cliente o que hay un cliente pero no sabemos quién es. En lugar de usar solo un cliente
nulo, considera tener Casos Especiales separados para clientes faltantes y clientes
desconocidos.
Una forma común de que un Caso Especial anule los métodos es devolver otro Caso
Especial, por lo que si le pides a un cliente desconocido su última factura, es probable que
obtengas una factura desconocida.
La aritmética de punto flotante IEEE 754 ofrece buenos ejemplos de Caso Especial con
infinito positivo, infinito negativo y no es un número (NaN). Si divides entre cero, en lugar de
obtener una excepción con la que debas lidiar, el sistema simplemente devuelve NaN, y
NaN participa en la aritmética igual que cualquier otro número de punto flotante.
Cuándo usarlo
Utiliza el Caso Especial siempre que tengas múltiples lugares en el sistema que
tengan el mismo comportamiento después de una comprobación condicional para una
instancia de clase en particular, o el mismo comportamiento después de una comprobación
de nulo.
Lectura adicional
Aún no he visto que se describa el Caso Especial como un patrón, pero se ha
descrito el Objeto Nuloen [Woolf]. Si me perdonas el juego de palabras irresistible, veo al
Objeto Nulo como un caso especial del Caso Especial.
Record Set
Una representación en memoria de datos en formato tabular.En los últimos veinte
años, la forma dominante de representar datos en una base de datos ha sido el formato
relacional tabular. Respaldado por compañías de bases de datos grandes y pequeñas, y un
lenguaje de consulta bastante estándar, casi todos los nuevos desarrollos que veo utilizan
datos relacionales.
A esto se suman numerosas herramientas para construir interfaces de usuario de
manera rápida. Estos frameworks de UI conscientes de los datos se basan en el hecho de
que los datos subyacentes son relacionales, y proporcionan widgets de UI de varios tipos
que facilitan la visualización y manipulación de estos datos con casi ninguna programación.
El lado negativo de estos entornos es que, aunque facilitan en gran medida la
visualización y las actualizaciones simples, no tienen una forma real de alojar la lógica
empresarial. Cualquier validación más allá de "¿es esta una fecha válida?" y cualquier regla
empresarial o cálculo no tienen un buen lugar donde ubicarse. O bien se insertan en la base
de datos como procedimientos almacenados, o se mezclan con el código de la interfaz de
usuario.
La idea del Conjunto de Registros (Record Set) es brindarte lo mejor de ambos mundos, al
proporcionar una estructura en memoria que se ve exactamente como el resultado de una
consulta SQL, pero que puede generarse y manipularse por otras partes del sistema.
Cómo funciona
Un Conjunto de Registros es generalmente algo que no construirás tú mismo, sino
que es proporcionado por el proveedor de la plataforma de software con la que estás
trabajando. Ejemplos incluyen el conjunto de datos de ADO.NET y el conjunto de filas de
JDBC 2.0.
El primer elemento esencial de un Conjunto de Registros es que se ve exactamente
como el resultado de una consulta de base de datos. Esto significa que puedes usar el
enfoque clásico de dos niveles emitiendo una consulta y arrojando los datos directamente
en una UI consciente de los datos con toda la facilidad que ofrecen estas herramientas de
dos niveles. El segundo elemento esencial es que puedes construir fácilmente un Conjunto
de Registros tú mismo o tomar uno que haya resultado de una consulta de base de datos y
manipularlo fácilmente con código de lógica de dominio.
Aunque las plataformas a menudo te dan un Conjunto de Registros, puedes crear
uno tú mismo. El problema es que no tiene mucho sentido sin las herramientas de UI
conscientes de los datos, que también necesitarías crear tú mismo. En cualquier caso, es
justo decir que construir una estructura de Conjunto de Registros como una lista de mapas,
que es común en lenguajes de secuencias de comandos de tipos dinámicos, es un buen
ejemplo de este patrón.
La capacidad de desconectar el Conjunto de Registros de su enlace con la fuente de
datos es muy valiosa. Esto te permite pasar el Conjunto de Registros por una red sin
preocuparte por las conexiones a la base de datos. Además, si puedes serializar fácilmente
el Conjunto de Registros, también puede actuar como un objeto de transferencia de datos
(DTO) para una aplicación.
La desconexión plantea la pregunta de qué sucede cuando actualizas el Conjunto de
Registros. Cada vez más, las plataformas permiten que el Conjunto de Registros sea una
forma de Unidad de Trabajo (Unit of Work), de modo que puedes modificarlo y luego
devolverlo a la fuente de datos para que se confirme. Un origen de datos típicamente puede
utilizar un Bloqueo Offline Optimista (Optimistic Offline Lock) para ver si hay conflictos y, si
no los hay, escribir los cambios en la base de datos.
Interfaz explícita
La mayoría de las implementaciones de Conjunto de Registros utilizan una interfaz
implícita. Esto significa que, para obtener información del Conjunto de Registros, invocas un
método genérico con un argumento que indica qué campo deseas. Por ejemplo, para
obtener el pasajero de una reserva de vuelo, usas una expresión como
aReservation["pasajero"]. Una interfaz explícita requiere una clase de reserva real con
métodos y propiedades definidos. Con una reserva explícita, la expresión para un pasajero
podría ser aReservation.pasajero.
Las interfaces implícitas son flexibles, ya que puedes usar un Conjunto de Registros
genérico para cualquier tipo de datos. Esto evita tener que escribir una nueva clase cada
vez que definas un nuevo Conjunto de Registros. Sin embargo, en general, considero que
las interfaces implícitas son algo negativo. Si estoy programando con una reserva, ¿cómo
sé cómo obtener el pasajero? ¿Es la cadena apropiada "pasajero", "huésped" o "viajero"?
La única forma de saberlo es buscar en el código tratando de encontrar dónde se crean y
usan las reservas. Si tengo una interfaz explícita, puedo ver la definición de la reserva para
ver qué propiedad necesito.
Este problema se ve exacerbado con los lenguajes de tipado estático. Si quiero el
apellido del pasajero, tengo que recurrir a una expresión horrible como
((Person)aReservation["pasajero"]).lastName, pero luego el compilador pierde toda la
información de tipo y tengo que ingresarla manualmente para obtener la información que
deseo. Una interfaz explícita puede mantener la información de tipo para que pueda usar
aReservation.pasajero.apellido. Por estas razones, en general desaconsejo las interfaces
implícitas (y su primo malvado, pasar datos en diccionarios). Tampoco soy muy fanático de
ellos en los Conjuntos de Registros, pero lo que salva la situación aquí es que el Conjunto
de Registros generalmente lleva información sobre las columnas legales en él. Además, los
nombres de columna son definidos por el SQL que crea el Conjunto de Registros, por lo que
no es demasiado difícil encontrar las propiedades cuando las necesitas.
Sin embargo, es agradable ir más allá y tener una interfaz explícita. ADO.NET
proporciona esto con sus conjuntos de datos de tipo fuertemente tipado, que son clases
generadas que brindan una interfaz explícita y completamente tipada para un Conjunto de
Registros. Dado que un conjunto de datos de ADO.NET puede contener muchas tablas y
las relaciones entre ellas, los conjuntos de datos de tipo fuertemente tipados también
proporcionan propiedades que pueden utilizar esa información de relación. Las clases se
generan a partir de la definición del conjunto de datos XSD.
Las interfaces implícitas son más comunes, por lo que he utilizado conjuntos de
datos sin tipo en mis ejemplos para este libro. Sin embargo, para el código de producción en
ADO.NET, sugiero utilizar conjuntos de datos con tipos. En un entorno no ADO.NET, sugiero
utilizar la generación de código para tus propios Conjuntos de Registros explícitos.
Cuándo usarlo
En mi opinión, el valor del Conjunto de Registros radica en tener un entorno que
dependa de él como una forma común de manipular datos. Muchas herramientas de interfaz
de usuario utilizan el Conjunto de Registros, y esa es una razón convincente para usarlos tú
mismo. Si tienes ese tipo de entorno, debes usar el Módulo de Tabla (Table Module) para
organizar tu lógica de dominio: obtén un Conjunto de Registros de la base de datos, pásalo
a un Módulo de Tabla (Table Module) para calcular información derivada, pásalo a una
interfaz de usuario para mostrar y editar, y devuélvelo a un Módulo de Tabla (Table Module)
para validación. Luego, confirma las actualizaciones en la base de datos.
En muchos aspectos, las herramientas que hacen que el Conjunto de Registros sea
tan valioso surgieron debido a la presencia constante de bases de datos relacionales y SQL,
y la ausencia de cualquier estructura y lenguaje de consulta alternativos reales. Ahora, por
supuesto, existe XML, que tiene una estructura ampliamente estandarizada y un lenguaje
de consulta en XPath, y creo que es probable que veamos herramientas que utilicen una
estructura jerárquica de la misma manera que las herramientas actuales utilizan el Conjunto
deRegistros. Quizás esto sea en realidad un caso particular de un patrón más genérico,
algo como una Estructura de Datos Genérica. Pero dejaré que se piense en ese patrón
hasta entonces.
Plugin
Vincula clases durante la configuración en lugar de la compilación. Separated Interface
(476) se utiliza a menudo cuando el código de la aplicación se ejecuta en múltiples entornos
de tiempo de ejecución, cada uno requiriendo diferentes implementaciones de un
comportamiento particular. La mayoría de los desarrolladores suministran la implementación
correcta escribiendo un método de fábrica. Supongamos que defines tu generador de claves
primarias con una Separated Interface (476) para que puedas usar un contador simple en
memoria para las pruebas unitarias, pero una secuencia gestionada por una base de datos
para producción. Es probable que tu método de fábrica contenga una declaración
condicional que examine una variable de entorno local, determine si el sistema está en
modo de prueba y devuelva el generador de claves correcto. Una vez que tienes unas
cuantas fábricas, tendrás un problema en tus manos.
Establecer una nueva configuración de implementación, cómo "ejecutar pruebas
unitarias contra una base de datos en memoria sin control de transacciones" o "ejecutar en
modo de producción contra una base de datos DB2 con control de transacciones completo",
requiere editar declaraciones condicionales en varias fábricas, reconstruir y volver a
implementar. La configuración no debe estar dispersa en toda la aplicación, ni debe requerir
una reconstrucción o una nueva implementación. Plugin resuelve ambos problemas al
proporcionar una configuración centralizada en tiempo de ejecución.
Cómo funciona
Lo primero que debes hacer es definir con una Separated Interface (476) los
comportamientos que tendrán diferentes implementaciones según el entorno de tiempo de
ejecución. Además de eso, utilizamos el patrón básico de la fábrica, pero con algunos
requisitos especiales. La fábrica Plugin requiere que sus instrucciones de vinculación se
declaren en un único punto externo para que la configuración pueda gestionarse fácilmente.
Además, la vinculación a las implementaciones debe ocurrir dinámicamente en tiempo de
ejecución en lugar de durante la compilación, para que la reconfiguración no requiera una
reconstrucción. Un archivo de texto funciona bastante bien como medio para establecer
reglas de vinculación. La fábrica Plugin simplemente leerá el archivo de texto, buscará una
entrada que especifique la implementación de una interfaz solicitada y devolverá esa
implementación.
Plugin funciona mejor en un lenguaje que admite reflexión porque la fábrica puede
construir implementaciones sin dependencias de tiempo de compilación en ellas. Al utilizar
la reflexión, el archivo de configuración debe contener mapeos de nombres de interfaces a
nombres de clases de implementación. La fábrica puede estar independiente en un paquete
de marco y no es necesario cambiarla cuando agregas nuevas implementaciones a las
opciones de configuración.
Incluso cuando no se utiliza un lenguaje que admite reflexión, sigue siendo válido establecer
un punto central de configuración. Incluso puedes usar un archivo de texto para establecer
reglas de vinculación, con la única diferencia de que tu fábrica utilizará lógica condicional
para asignar una interfaz a la implementación deseada. Cada tipo de implementación debe
ser considerado en la fábrica, lo cual no es un gran problema en la práctica. Solo agrega
otra opción dentro del método de fábrica cada vez que agregues una nueva implementación
a la base de código. Para hacer cumplir las dependencias de capa y paquete con una
verificación en tiempo de compilación, coloca esta fábrica en su propio paquete para evitar
romper tu proceso de construcción.
Cuándo usarlo
Usa Plugin siempre que tengas comportamientos que requieran diferentes
implementaciones según el entorno de tiempo de ejecución.
Mapper
Un objeto que establece una comunicación entre dos objetos independientes.
A veces necesitas establecer comunicación entre dos subsistemas que aún necesitan
mantenerse ignorantes entre sí. Esto puede ser porque no puedes modificarlos o puedes
hacerlo, pero no deseas crear dependencias entre ambos o incluso entre ellos y el elemento
de aislamiento.
Cómo funciona
Un mapper es una capa aislante entre subsistemas. Controla los detalles de la
comunicación entre ellos sin que ninguno de los subsistemas sea consciente de ello. Un
mapper a menudo mueve datos de una capa a otra. Una vez activado para este
movimiento, es bastante fácil ver cómo funciona. La parte complicada de usar un mapper es
decidir cómo invocarlo, ya que no puede ser invocado directamente por ninguno de los
subsistemas entre los que realiza el mapeo. A veces, un tercer subsistema impulsa el
mapeo e invoca al mapper también. Otra alternativa es hacer del mapper un observador
[Gang of Four] de uno u otro subsistema. De esta manera, puede ser invocado escuchando
eventos en uno de ellos.
Cómo funciona un mapper depende del tipo de capas que está mapeando. El caso
más común de una capa de mapeo con el que nos encontramos es en un Data Mapper
(165), así que busca allí más detalles sobre cómo se utiliza un Mapper.
Cuándo usarlo
Básicamente, un Mapper desacopla diferentes partes de un sistema. Cuando deseas hacer
esto, tienes la opción entre un Mapper y un Gateway (466). El Gateway (466) es, con
mucho, la opción más común porque es mucho más sencillo usar un Gateway (466) que un
Mapper tanto al escribir el código como al usarlo más adelante.
Como resultado, debes usar un Mapper solo cuando necesites asegurarte de que
ningún subsistema dependa de esta interacción. El único momento en que esto es
realmente importante es cuando la interacción entre los subsistemas es particularmente
complicada y algo independiente del propósito principal de ambos subsistemas. Por lo tanto,
en las aplicaciones empresariales, principalmente encontramos que el Mapper se utiliza
para interactuar con una base de datos, como en el Data Mapper (165).
El Mapper es similar al Mediador [Gang of Four] en el sentido de que se utiliza para
separar elementos diferentes. Sin embargo, los objetos que utilizan un mediador son
conscientes de él, aunque no sean conscientes entre sí; los objetos que separa un Mapper
ni siquiera son conscientes del mapper.
Gateway
Un objeto que encapsula el acceso a un sistema o recurso externo.
El software interesante rara vez vive de forma aislada. Incluso el sistema puramente
orientado a objetos a menudo tiene que lidiar con cosas que no son objetos, como tablas de
bases de datos relacionales, transacciones CICS y estructuras de datos XML.
Cuando se accede a recursos externos de esta manera, generalmente se utilizan API para
interactuar con ellos. Sin embargo, estas API suelen ser complicadas debido a que tienen
en cuenta la naturaleza del recurso. Cualquier persona que necesite comprender un recurso
debe entender su API, ya sea JDBC y SQL para bases de datos relacionales o W3C o
JDOM para XML. Esto no solo dificulta la comprensión del software, sino que también lo
hace mucho más difícil de cambiar en caso de que en el futuro se desee cambiar los datos
de una base de datos relacional a un mensaje XML.
La respuesta es tan común que apenas vale la pena mencionarla. Envuelve todo el
código especial de la API en una clase cuya interfaz se asemeja a la de un objeto regular.
Otros objetos acceden al recurso a través de este Gateway, que traduce las simples
llamadas de método en las correspondientes API especializadas.
Cómo funciona
En realidad, este es un patrón de envoltura muy simple. Toma el recurso externo.
¿Qué necesita hacer la aplicación con él? Crea una API simple para su uso y utiliza el
Gateway para traducir hacia la fuente externa. Uno de los usos clave de un Gateway es
como un buen punto para aplicar un Stub de Servicio (504). A menudo, puedes alterar el
diseño del Gateway para que sea más fácil aplicar un Stub de Servicio (504). Notengas
miedo de hacerlo, los Stub de Servicio (504) bien ubicados pueden facilitar mucho las
pruebas de un sistema y, por lo tanto, su escritura.
Mantén el Gateway lo más simple posible. Concéntrate en los roles esenciales de
adaptar el servicio externo y proporcionar un buen punto para la simulación. El Gateway
debería ser lo más mínimo posible, pero capaz de manejar estas tareas. Cualquier lógica
más compleja debería estar en los clientes del Gateway.
A menudo, es una buena idea utilizar la generación de código para crear Gateways. Al
definir la estructura del recurso externo, puedes generar una clase de Gateway para
envolverlo. Puedes utilizar metadatos relacionales para crear una clase de envoltura para
una tabla relacional, o un esquema XML o DTD para generar código para un Gateway de
XML. Los Gateways resultantes son simples pero cumplen su función. Otros objetos pueden
realizar manipulaciones más complicadas.
A veces, una buena estrategia es construir el Gateway en términos de más de un
objeto. La forma obvia es usar dos objetos: uno de respaldo y otro frontal. El de respaldo
actúa como una superposición mínima del recurso externo y no simplifica en absoluto la API
del recurso. Luego, el frontal transforma la incómoda API en una más conveniente para que
la aplicación la utilice. Este enfoque es útil si el envolvimiento del servicio externo y la
adaptación a tus necesidades son razonablemente complicados, ya que cada
responsabilidad es manejada por una sola clase. Por otro lado, si el envolvimiento del
servicio externo es simple, una sola clase puede manejarlo junto con cualquier adaptación
necesaria.
Cuándo utilizarlo
Debes considerar el uso del patrón Gateway cuando tienes una interfaz incómoda
hacia algo que se siente externo. En lugar de permitir que la incomodidad se extienda por
todo el sistema, utiliza un Gateway para contenerla. Prácticamente no hay desventajas en
crear el Gateway y el código en otras partes del sistema se vuelve mucho más fácil de leer.
El Gateway suele facilitar las pruebas de un sistema al proporcionarte un punto claro en el
cual implementar Stub de Servicio (504). Incluso si la interfaz del sistema externo es
adecuada, un Gateway es útil como un primer paso para aplicar Stub de Servicio (504).
Un claro beneficio del Gateway es que también facilita el reemplazo de un tipo de recurso
por otro. Cualquier cambio en los recursos significa que solo tienes que modificar la clase
Gateway; el cambio no se extenderá por el resto del sistema. Gateway es una forma simple
y poderosa de variación protegida. En muchos casos, el debate sobre el uso de Gateway se
centra en razonar sobre esta flexibilidad. Sin embargo, no olvides que incluso si no crees
que el recurso vaya a cambiar, puedes beneficiarte de la simplicidad y la capacidad de
prueba que te brinda Gateway.
Cuando tienes un par de subsistemas como estos, otra opción para desacoplarlos es
el patrón Mapper (Mapeador) (473). Sin embargo, el patrón Mapper (473) es más
complicado que el Gateway. Como resultado, uso Gateway para la mayoría de los casos de
acceso a recursos externos.
Debo admitir que he luchado bastante con la decisión de si esto debería ser un
nuevo patrón o hacer referencia a patrones existentes como Fachada y Adaptador [Gang of
Four]. Decidí separarlo de estos otros patrones porque creo que hay una distinción útil que
se puede hacer.
- Mientras que la Fachada simplifica una API más compleja, generalmente lo hace el autor
del servicio para uso general. Un Gateway es escrito por el cliente para su uso particular.
Además, una Fachada siempre implica una interfaz diferente a la que está cubriendo,
mientras que un Gateway puede copiar completamente la fachada envuelta, utilizándose
para sustitución o fines de prueba.
- El Adaptador altera la interfaz de implementación para que coincida con otra interfaz con la
que necesitas trabajar. En el caso del Gateway, generalmente no hay una interfaz existente,
aunque podrías usar un adaptador para mapear una implementación a una interfaz de
Gateway. En este caso, el adaptador forma parte de la implementación del Gateway.
- El Mediador generalmente separa múltiples objetos para que no se conozcan entre sí,
pero sí conozcan al mediador. En un Gateway, generalmente solo hay dos objetos
involucrados y el recurso que se envuelve no conoce al Gateway.
Separated Interface
Define una interfaz en un paquete separado de su implementación.
A medida que desarrollas un sistema, puedes mejorar la calidad de su diseño al reducir el
acoplamiento entre las partes del sistema. Una buena forma de hacerlo es agrupar las
clases en paquetes y controlar las dependencias entre ellos. Luego puedes seguir reglas
sobre cómo las clases en un paquete pueden llamar a clases en otro, por ejemplo, una regla
que dice que las clases en la capa de dominio no pueden llamar a clases en el paquete de
presentación.
Sin embargo, es posible que necesites invocar métodos que contradigan la
estructura de dependencia general. Si es así, utiliza Separated Interface para definir una
interfaz en un paquete y luego implementarla en otro. De esta manera, un cliente que
necesite la dependencia de la interfaz puede estar completamente ajeno a la
implementación. El Separated Interface proporciona un buen punto de conexión para
Gateway.
Cómo funciona:
Este patrón es muy simple de aplicar. Básicamente, se aprovecha del hecho de que
una implementación tiene una dependencia de su interfaz, pero no al revés. Esto significa
que puedes poner la interfaz y la implementación en paquetes separados, donde el paquete
de implementación tiene una dependencia del paquete de interfaz. Otros paquetes pueden
depender del paquete de interfaz sin depender del paquete de implementación.
Por supuesto, el software no funcionará en tiempo de ejecución sin alguna
implementación de la interfaz. Esto se puede hacer en tiempo de compilación utilizando un
paquete separado que los relacione o en tiempo de configuración utilizando un Plugin.
Puedes colocar la interfaz en el paquete del cliente o en un tercer paquete. Si solo hay un
cliente para la implementación o todos los clientes están en el mismo paquete, entonces es
conveniente poner la interfaz junto con el cliente. Una buena manera de pensar en esto es
que los desarrolladores del paquete del cliente son responsables de definir la interfaz.
Básicamente, el paquete del cliente indica que funcionará con cualquier otro paquete que
implemente la interfaz que define. Si tienes múltiples paquetes de clientes, es mejor utilizar
un tercer paquete para la interfaz. También es mejor si quieres mostrar que la definición de
la interfaz no es responsabilidad de los desarrolladores del paquete del cliente, sino de los
desarrolladores de la implementación.
Debes considerar qué característica del lenguaje utilizar para la interfaz. En
lenguajes que tienen un constructo de interfaz, como Java y C#, la palabra clave "interface"
es la elección obvia. Sin embargo, puede que no sea la mejor opción. Una clase abstracta
puede ser una buena interfaz porque puedes tener un comportamiento de implementación
común pero opcional en ella.
Una de las cosas incómodas acerca de las interfaces separadas es cómo instanciar
la implementación. Por lo general, requiere conocimiento de la clase de implementación. El
enfoque común es utilizar un objeto de fábrica separado, donde nuevamente hay un
Separated Interface para la fábrica. Aún así, debes vincular una implementación a la fábrica,
y el Plugin es una buena manera de hacerlo. No solo significa que no hay dependencia, sino
que también pospone la decisión sobre la clase de implementación al momento de la
configuración.
Si no quieres llegar hasta el punto de utilizar Plugin, una alternativa más sencilla es
permitir que otro paquete que conozca tanto la interfaz como la implementación instancie
los objetos adecuados al inicio de la aplicación. Cualquier objeto que utilice Separated
Interface puede ser instanciado por sí mismo o tener fábricas instanciadas al inicio.Cuándo utilizarlo:
Utiliza Separated Interface cuando necesites romper una dependencia entre dos
partes del sistema. Aquí tienes algunos ejemplos:
- Has construido código abstracto para casos comunes en un paquete de framework que
necesita llamar a un código de aplicación específico.
- Tienes código en una capa que necesita llamar a código en otra capa sin que debería ver,
como código de dominio que llama a un Data Mapper.
- Necesitas llamar a funciones desarrolladas por otro grupo de desarrollo pero no quieres
tener una dependencia de sus API.
Me encuentro con muchos desarrolladores que tienen interfaces separadas para
cada clase que escriben. Creo que esto es excesivo, especialmente para el desarrollo de
aplicaciones. Mantener interfaces e implementaciones separadas es trabajo adicional,
especialmente porque a menudo se necesitan clases de fábrica (con interfaces e
implementaciones) también. Para aplicaciones, recomiendo usar una interfaz separada solo
si quieres romper una dependencia o si deseas tener múltiples implementaciones
independientes. Si colocas la interfaz y la implementación juntas y luego necesitas
separarlas, esta es una refactorización sencilla que se puede posponer hasta que la
necesites.
Existe un grado en el que la gestión determinada de dependencias de esta manera puede
volverse un poco absurda. Tener solo una dependencia para crear un objeto y utilizar la
interfaz en adelante suele ser suficiente. El problema surge cuando quieres hacer cumplir
reglas de dependencia, como realizar una verificación de dependencia en tiempo de
compilación. Entonces todas las dependencias deben ser eliminadas. Para un sistema más
pequeño, hacer cumplir las reglas de dependencia es menos problemático, pero para
sistemas más grandes es una disciplina muy valiosa.
Remote Fascade
Proporciona una fachada de granularidad gruesa en objetos de granularidad fina
para mejorar la eficiencia sobre una red.
En un modelo orientado a objetos, es mejor utilizar objetos pequeños con métodos
pequeños. Esto brinda muchas oportunidades para controlar y sustituir el comportamiento,
así como utilizar nombres que revelen la intención para facilitar la comprensión de una
aplicación. Una de las consecuencias de este comportamiento de granularidad fina es que
generalmente hay mucha interacción entre objetos, y esa interacción generalmente requiere
muchas invocaciones de métodos.
Dentro de un único espacio de direcciones, la interacción de granularidad fina
funciona bien, pero este estado ideal no existe cuando se realizan llamadas entre procesos.
Las llamadas remotas son mucho más costosas porque hay mucho más que hacer: puede
ser necesario empaquetar los datos, verificar la seguridad, enrutamiento de paquetes a
través de conmutadores. Si los dos procesos se ejecutan en máquinas en lados opuestos
del globo, la velocidad de la luz puede ser un factor. La verdad brutal es que cualquier
llamada entre procesos es órdenes de magnitud más costosa que una llamada dentro de un
proceso, incluso si ambos procesos están en la misma máquina. Este efecto en el
rendimiento no se puede ignorar, incluso para los defensores de la optimización perezosa.
Como resultado, cualquier objeto que se pretenda utilizar como un objeto remoto
necesita una interfaz de granularidad gruesa que minimice la cantidad de llamadas
necesarias para realizar una tarea. Esto afecta no solo a las llamadas de métodos, sino
también a sus objetos. En lugar de solicitar un pedido y sus líneas de pedido
individualmente, necesitas acceder y actualizar el pedido y las líneas de pedido en una sola
llamada. Esto afecta a toda la estructura de tus objetos. Sacrificas la clara intención y el
control de granularidad fina que obtienes con objetos y métodos pequeños. La
programación se vuelve más difícil y tu productividad disminuye.
Una Fachada Remota es una fachada de granularidad gruesa [Gang of Four] sobre
una red de objetos de granularidad fina. Ninguno de los objetos de granularidad fina tiene
una interfaz remota, y la Fachada Remota no contiene lógica de dominio. Todo lo que hace
la Fachada Remota es traducir métodos de granularidad gruesa en los objetos de
granularidad fina subyacentes.
Cómo funciona
La Fachada Remota aborda el problema de distribución que surge al separar las
distintas responsabilidades en objetos diferentes, que es el enfoque estándar de la
programación orientada a objetos. Como resultado, se ha convertido en el patrón estándar
para resolver este problema. Reconozco que los objetos de granularidad fina son la solución
adecuada para la lógica compleja, por lo que aseguro que cualquier lógica compleja se
coloque en objetos de granularidad fina diseñados para colaborar dentro de un solo
proceso. Para permitir un acceso remoto eficiente a ellos, creo un objeto de fachada
separado que actúa como una interfaz remota. Como su nombre lo indica, la fachada es
simplemente una capa delgada que cambia de una interfaz de granularidad gruesa a una
interfaz de granularidad fina.
En un caso simple, como un objeto de dirección, una Fachada Remota reemplaza
todos los métodos de obtención y establecimiento del objeto de dirección regular con un
método de obtención y uno de establecimiento, a menudo llamados accesores en bloque.
Cuando un cliente llama a un establecedor en bloque, la fachada de dirección lee los datos
del método de establecimiento y llama a los accesores individuales en el verdadero objeto
de dirección (ver Figura 15.1) sin hacer nada más. De esta manera, toda la lógica de
validación y cálculo se mantiene en el objeto de dirección, donde se puede estructurar de
manera limpia y se puede utilizar por otros objetos de granularidad fina.
En un caso más complejo, una sola Fachada Remota puede actuar como una puerta
de enlace remota para muchos objetos de granularidad fina. Por ejemplo, una fachada de
pedido se puede utilizar para obtener y actualizar información sobre un pedido, todas sus
líneas de pedido y tal vez algunos datos de cliente también.
Al transferir información en bloque de esta manera, es necesario que esté en una
forma que se pueda mover fácilmente a través de la red. Si tus clases de granularidad fina
están presentes en ambos lados de la conexión y son serializables, puedes transferirlas
directamente mediante una copia. En este caso, un método getAddressData crea una copia
del objeto de dirección original. El método setAddressData recibe un objeto de dirección y lo
utiliza para actualizar los datos del objeto de dirección real. (Esto asume que el objeto de
dirección original necesita preservar su identidad y, por lo tanto, no se puede reemplazar
simplemente con el nuevo objeto de dirección).
Sin embargo, a menudo no puedes hacer esto. Es posible que no desees duplicar
tus clases de dominio en múltiples procesos, o puede ser difícil serializar un segmento de
un modelo de dominio debido a su complicada estructura de relaciones. Es posible que el
cliente no desee el modelo completo, sino solo un subconjunto simplificado de él. En estos
casos, tiene sentido utilizar un Objeto de Transferencia de Datos (401) como base para la
transferencia.
En el esquema que se muestra, se muestra una Fachada Remota que corresponde
a un único objeto de dominio. Esto no es poco común y es fácil de entender, pero no es el
caso más común. Una sola Fachada Remota tendría varios métodos, cada uno diseñado
para pasar información de varios objetos. Por lo tanto, getAddressData y setAddressData
serían métodos definidos en una clase como CustomerService, que también tendría
métodos como getPurchasingHistory y updateCreditData.
La granularidad es uno de los aspectos más complicados de la Fachada Remota. A
algunas personas les gusta hacer Fachadas Remotas bastante pequeñas, como una por
caso de uso. Yo prefiero una estructura de granularidad más gruesa con muchas menos
Fachadas Remotas. Incluso para una aplicación de tamaño moderado, puedo tener solo
una y, incluso para una aplicación grande, puede que solo tenga media docena. Esto
significa que cadaFachada Remota tiene muchos métodos, pero dado que estos métodos
son pequeños, no veo esto como un problema.
Diseñas una Fachada Remota en función de las necesidades de uso de un cliente
en particular, que con mayor frecuencia es la necesidad de ver y actualizar información a
través de una interfaz de usuario. En este caso, puedes tener una sola Fachada Remota
para una familia de pantallas, en cada una de las cuales un método de acceso en bloque
carga y guarda los datos. Al presionar botones en una pantalla, por ejemplo, para cambiar el
estado de un pedido, se invocan métodos de comando en la fachada. Con frecuencia,
tendrás diferentes métodos en la Fachada Remota que hacen prácticamente lo mismo en
los objetos subyacentes. Esto es común y razonable. La fachada está diseñada para
simplificar la vida de los usuarios externos, no para el sistema interno, por lo que si el
proceso del cliente lo considera un comando diferente, es un comando diferente, incluso si
todo se dirige al mismo comando interno.
Una Fachada Remota puede ser sin estado o con estado. Una Fachada Remota sin
estado se puede agrupar, lo que puede mejorar el uso de recursos y la eficiencia,
especialmente en una situación de negocio a consumidor (B2C). Sin embargo, si la
interacción implica estado a lo largo de una sesión, entonces es necesario almacenar el
estado de la sesión en algún lugar utilizando el Estado de Sesión del Cliente (456) o el
Estado de Sesión de la Base de Datos (462), o una implementación del Estado de Sesión
del Servidor (458). Una Fachada Remota con estado puede mantener su propio estado, lo
que facilita la implementación del Estado de Sesión del Servidor (458), pero esto puede
generar problemas de rendimiento cuando tienes miles de usuarios simultáneos.
Además de proporcionar una interfaz de granularidad gruesa, se pueden agregar
varias responsabilidades adicionales a una Fachada Remota. Por ejemplo, sus métodos son
un punto natural para aplicar seguridad. Una lista de control de acceso puede determinar
qué usuarios pueden invocar llamadas a qué métodos. Los métodos de la Fachada Remota
también son un punto natural para aplicar control transaccional. Un método de la Fachada
Remota puede iniciar una transacción, realizar todo el trabajo interno y luego confirmar la
transacción al final. Cada llamada se convierte en una transacción adecuada, ya que no
deseas tener una transacción abierta cuando el retorno regresa al cliente, ya que las
transacciones no están diseñadas para ser eficientes en casos de ejecución prolongada.
Uno de los mayores errores que veo en una Fachada Remota es poner lógica de
dominio en ella. Repite después de mí tres veces: "Una Fachada Remota no tiene lógica de
dominio". Cualquier fachada debe ser una capa delgada que tenga solo responsabilidades
mínimas. Si necesitas lógica de dominio para flujos de trabajo o coordinación, colócala en
tus objetos de granularidad fina o crea un Script de Transacción (Transaction Script)
separado y no remoto (110) para contenerla. Deberías poder ejecutar toda la aplicación
localmente sin utilizar las Fachadas Remotas ni duplicar ningún código.
Fachada Remota y Fachada de Sesión: En los últimos años, el patrón Fachada de
Sesión ha estado apareciendo en la comunidad de J2EE. En mis borradores anteriores,
consideré que la Fachada Remota era el mismo patrón que la Fachada de Sesión y utilicé el
nombre de Fachada de Sesión. Sin embargo, en la práctica, hay una diferencia crucial. La
Fachada Remota se trata de tener una interfaz remota delgada, de ahí mi diatriba contra la
lógica de dominio en ella. En contraste, la mayoría de las descripciones de la Fachada de
Sesión involucran poner lógica en ella, generalmente de tipo flujo de trabajo. Gran parte de
esto se debe al enfoque común de utilizar EJB de sesión de J2EE para envolver EJB de
entidad. Cualquier coordinación de EJB de entidad debe ser realizada por otro objeto, ya
que no pueden ser reentrantes.
Como resultado, veo una Fachada de Sesión como la inclusión de varios Scripts de
Transacción (110) en una interfaz remota. Ese es un enfoque razonable, pero no es lo
mismo que una Fachada Remota. De hecho, argumentaría que, dado que la Fachada de
Sesión contiene lógica de dominio, no debería ser llamada una fachada en absoluto.
Capa de Servicio: Un concepto familiar para las fachadas es una Capa de Servicio
(Service Layer) (133). La diferencia principal es que una capa de servicio no tiene que ser
remota y, por lo tanto, no necesita tener solo métodos de granularidad fina. Al simplificar el
Modelo de Dominio (116), a menudo terminas con métodos de granularidad más gruesa,
pero eso es para mayor claridad, no para eficiencia de red. Además, no es necesario que
una capa de servicio utilice Objetos de Transferencia de Datos (Data Transfer Objects)
(401). Por lo general, puede devolver objetos de dominio reales al cliente.
Si se va a utilizar un Modelo de Dominio (116) tanto dentro de un proceso como de
forma remota, puedes tener una Capa de Servicio (133) y colocar una Fachada Remota
separada encima de ella. Si el proceso solo se utiliza de forma remota, probablemente sea
más fácil combinar la Capa de Servicio (133) en la Fachada Remota, siempre y cuando la
Capa de Servicio (133) no tenga lógica de aplicación. Si hay alguna lógica de aplicación en
ella, entonces haría que la Fachada Remota sea un objeto separado.
Cuándo utilizarlo:
Utiliza la Fachada Remota siempre que necesites acceso remoto a un modelo de
objetos de granularidad fina. Obtendrás las ventajas de una interfaz de granularidad gruesa
y al mismo tiempo mantendrás la ventaja de objetos de granularidad fina, lo que te brinda lo
mejor de ambos mundos.
El uso más común de este patrón es entre una presentación y un Modelo de
Dominio (116), donde ambos pueden ejecutarse en diferentes procesos. Esto se aplica a
una interfaz de usuario Swing y un modelo de dominio en el servidor, o a un servlet y un
modelo de objeto en el servidor si la aplicación y los servidores web son procesos
diferentes.
La mayoría de las veces, esto ocurre con diferentes procesos en diferentes
máquinas, pero resulta que el costo de una llamada entre procesos en la misma máquina es
lo suficientemente grande como para requerir una interfaz de granularidad gruesa para
cualquier comunicación entre procesos, independientemente de dónde residan los
procesos.
Si todo tu acceso está dentro de un solo proceso, no necesitas este tipo de
conversión. Por lo tanto, no utilizaría este patrón para comunicarse entre un Modelo de
Dominio (116) del cliente y su presentación, o entre un script CGI y un Modelo de Dominio
(116) que se ejecuta en un servidor web. No se suele ver el uso de la Fachada Remota con
un Script de Transacción (110), ya que un Script de Transacción (110) es inherentemente de
granularidad más gruesa.
Las Fachadas Remotas implican una distribución síncrona, es decir, una llamada de
procedimiento remoto. A menudo, se puede mejorar en gran medida la capacidad de
respuesta de una aplicación utilizando una comunicación remota asíncrona basada en
mensajes. De hecho, un enfoque asíncrono tiene muchas ventajas convincentes.
Desafortunadamente, la discusión de patrones asíncronos está fuera del alcance de este
libro.
DTO
Data Transfer Object (DTO) es un objeto que lleva datos entre procesos con el fin de
reducir el número de llamadas a métodos.
Cuando se trabaja con una interfaz remota, como Remote Facade (Fachada
Remota), cada llamada a ella es costosa. Como resultado, es necesario reducir el número
de llamadas y eso implica transferir más datos en cada llamada. Una forma de hacer esto
es utilizando muchos parámetros. Sin embargo, esto a menudo resulta incómodo de
programar, de hecho, en lenguajes como Java, que solo devuelven un valor único, es
frecuentemente imposible.
La solución es crear un Data Transfer Object que pueda contener todos los datos
necesarios para la llamada. Este objeto debe ser serializable para poder transmitirse a
través de la conexión. Por lo general,en el lado del servidor se utiliza un ensamblador para
transferir datos entre el DTO y los objetos de dominio.
Muchas personas en la comunidad de Sun utilizan el término "Value Object" (Objeto
de Valor) para este patrón. Yo lo utilizo para referirme a otra cosa. Consulta la discusión en
la página 487.
Cómo funciona:
En muchos aspectos, un Data Transfer Object es uno de esos objetos que nuestras
madres nos dijeron que nunca escribiéramos. A menudo no es más que un conjunto de
campos y sus correspondientes getters y setters. El valor de esta bestia, que generalmente
es odiada, radica en que te permite mover varios datos sobre una red en una sola llamada,
lo cual es esencial para sistemas distribuidos. Cuando un objeto remoto necesita algunos
datos, solicita un Data Transfer Object adecuado. El Data Transfer Object generalmente
contiene más datos de los que el objeto remoto solicitó, pero debe llevar todos los datos que
el objeto remoto necesitará durante un tiempo. Debido a los costos de latencia de las
llamadas remotas, es mejor enviar demasiados datos que tener que realizar múltiples
llamadas.
Un solo Data Transfer Object generalmente contiene más que un solo objeto del
servidor. Agrega datos de todos los objetos del servidor de los que es probable que el objeto
remoto desee datos. Por lo tanto, si un objeto remoto solicita datos sobre un objeto de
orden, el Data Transfer Object devuelto contendrá datos de la orden, el cliente, los ítems de
línea, los productos en los ítems de línea, la información de entrega, y otros datos
relacionados.
Por lo general, no se pueden transferir objetos completos de un Domain Model
(Modelo de Dominio). Esto se debe a que los objetos generalmente están conectados en
una red compleja que es difícil, sino imposible, de serializar. Además, generalmente no se
desean las clases de objetos de dominio en el cliente, ya que esto equivaldría a copiar todo
el Modelo de Dominio en el cliente. En su lugar, se debe transferir una forma simplificada de
los datos de los objetos de dominio.
Los campos de un Data Transfer Object son bastante simples, son primitivas, clases
simples como cadenas y fechas, u otros Data Transfer Objects. Cualquier estructura entre
objetos de transferencia de datos debe ser una estructura de grafo simple, normalmente
una jerarquía, en contraste con las estructuras de grafo más complicadas que se ven en un
Modelo de Dominio. Mantén estos atributos simples porque deben ser serializables y deben
ser comprendidos por ambos lados de la conexión. Como resultado, las clases Data
Transfer Object y las clases a las que hacen referencia deben estar presentes en ambos
lados.
Tiene sentido diseñar el Data Transfer Object en función de las necesidades de un
cliente en particular. Es por eso que a menudo se ven DTOs que corresponden a páginas
web o pantallas de interfaz gráfica. También es posible tener múltiples Data Transfer
Objects para un pedido, dependiendo de la pantalla en particular. Por supuesto, si diferentes
presentaciones requieren datos similares, entonces tiene sentido utilizar un solo Data
Transfer Object para manejarlos a todos.
Una pregunta relacionada a considerar es si usar un solo Data Transfer Object para
toda la interacción o diferentes DTOs para cada solicitud. El uso de diferentes Data Transfer
Objects facilita ver qué datos se transfieren en cada llamada, pero lleva a tener muchos
DTOs. Utilizar uno solo es más fácil de escribir, pero dificulta ver cómo se transfiere la
información en cada llamada. Tiendo a usar uno solo si hay mucha similitud en los datos,
pero no dudo en utilizar diferentes Data Transfer Objects si una solicitud en particular lo
sugiere. Es una de esas cosas en las que no se puede hacer una regla general, así que
podría utilizar un solo Data Transfer Object para la mayor parte de la interacción y usar
diferentes DTOs para un par de solicitudes y respuestas.
Una pregunta similar es si se debe tener un solo Data Transfer Object para ambas
solicitudes y respuestas, o separados para cada uno. Nuevamente, no hay una regla
general. Si los datos en cada caso son bastante similares, se puede usar uno solo. Si son
muy diferentes, utilizo dos.
Algunas personas prefieren que los Data Transfer Objects sean inmutables. En este
esquema, se recibe un Data Transfer Object del cliente y se crea y se envía otro diferente,
incluso si es de la misma clase. Otras personas modifican el Data Transfer Object de
solicitud. No tengo opiniones fuertes en ninguno de los dos sentidos, pero en general
prefiero un Data Transfer Object mutable porque es más fácil ir agregando gradualmente los
datos, incluso si se crea un objeto nuevo para la respuesta. Algunos argumentos a favor de
los Data Transfer Objects inmutables tienen que ver con la confusión de nombres con Value
Object (Objeto de Valor).
Una forma común de Data Transfer Object es la de un conjunto de registros (Record
Set), es decir, un conjunto de registros tabulares, exactamente lo que se obtiene de una
consulta SQL. De hecho, un Record Set es el Data Transfer Object para una base de datos
SQL. A menudo, las arquitecturas lo utilizan en todo el diseño. Un modelo de dominio puede
generar un Record Set de datos para transferirlo a un cliente, el cual lo trata como si
proviniera directamente de SQL. Esto es útil si el cliente tiene herramientas que se vinculan
a estructuras de Record Set. El Record Set puede ser creado completamente por la lógica
del dominio, pero lo más probable es que se genere a partir de una consulta SQL y se
modifique por la lógica del dominio antes de pasarlo a la presentación. Este estilo se presta
al patrón Table Module.
Otra forma de Data Transfer Object es como una estructura de datos de colección
genérica. He visto que se utilizan arrays para esto, pero desaconsejo su uso porque los
índices del array pueden dificultar la comprensión del código. La mejor colección es un
diccionario porque se pueden utilizar cadenas significativas como claves. El problema es
que se pierde la ventaja de una interfaz explícita y un tipado fuerte. Puede valer la pena
usar un diccionario para casos ad hoc cuando no se tiene un generador a mano, ya que es
más fácil manipularlo que escribir un objeto explícito manualmente. Sin embargo, con un
generador, creo que es mejor utilizar una interfaz explícita, especialmente cuando se
considera que se utiliza como protocolo de comunicación entre diferentes componentes.
Serializar el Data Transfer Object, aparte de los simples getters y setters, también
suele ser responsabilidad del Data Transfer Object en sí mismo, en un formato que se
transmita a través de la conexión. El formato depende de lo que haya en cada extremo de la
conexión, de lo que se pueda transmitir a través de la conexión y de lo fácil que sea la
serialización. Muchas plataformas proporcionan serialización incorporada para objetos
simples. Por ejemplo, Java tiene una serialización binaria incorporada y .NET tiene
serializaciones binarias y XML incorporadas. Si hay una serialización incorporada,
generalmente funciona de inmediato porque los Data Transfer Objects son estructuras
simples que no lidian con las complejidades que se encuentran en los objetos de un modelo
de dominio. Por lo tanto, siempre utilizo el mecanismo automático si es posible.
Si no se dispone de un mecanismo automático, generalmente se puede crear uno
propio. He visto varios generadores de código que toman descripciones de registros simples
y generan clases adecuadas para contener los datos, proporcionan accesores y leen y
escriben las serializaciones de datos. Lo importante es hacer que el generador sea tan
complicado como realmente se necesita, y no tratar de incluir características que solo se
cree que se necesitarán. Puede ser una buena idea escribir las primeras clases a mano y
luego utilizarlas para ayudar a escribir el generador.
También se puede utilizar la programación reflexiva para manejar la serialización. De
esta manera, solo se tiene que escribir las rutinas de serialización y deserializaciónuna vez
y colocarlas en una superclase. Puede haber un costo de rendimiento asociado a esto; hay
que medirlo para determinar si el costo es significativo.
Se debe elegir un mecanismo con el que ambos extremos de la conexión puedan
trabajar. Si se controlan ambos extremos, se elige el más fácil; si no se controla, es posible
que se pueda proporcionar un conector en el extremo que no se posee. Entonces se puede
utilizar un simple Data Transfer Object en ambos extremos de la conexión y utilizar el
conector para adaptarse al componente externo.
Uno de los problemas más comunes que se enfrenta con los Data Transfer Objects
es si se debe utilizar una forma de serialización de texto o binaria. Las serializaciones de
texto son fáciles de leer para comprender qué se está comunicando. XML es popular porque
se pueden obtener fácilmente herramientas para crear y analizar documentos XML. Las
grandes desventajas del texto son que requieren más ancho de banda para enviar los
mismos datos (lo cual es especialmente cierto en el caso de XML) y a menudo hay una
penalización en el rendimiento, que puede ser bastante significativa.
Un factor importante para la serialización es la sincronización del Data Transfer
Object en cada extremo de la conexión. En teoría, cada vez que el servidor cambia la
definición del Data Transfer Object, el cliente también se actualiza, pero en la práctica esto
puede no suceder. Acceder a un servidor con un cliente desactualizado siempre conduce a
problemas, pero el mecanismo de serialización puede hacer que los problemas sean más o
menos dolorosos. Con una serialización binaria pura de un Data Transfer Object, se perderá
por completo su comunicación, ya que cualquier cambio en su estructura generalmente
provoca un error en la deserialización. Incluso un cambio inocuo, como agregar un campo
opcional, tendrá este efecto. Como resultado, la serialización binaria directa puede introducir
mucha fragilidad en las líneas de comunicación.
Otros esquemas de serialización pueden evitar esto. Uno de ellos es la serialización XML,
que generalmente se puede escribir de manera que las clases sean más tolerantes a los
cambios. Otro enfoque más tolerante es utilizar una serialización binaria, como serializar los
datos utilizando un diccionario. Aunque no me gusta utilizar un diccionario como el objeto de
transferencia de datos, puede ser una forma útil de realizar una serialización binaria de los
datos, ya que esto introduce cierta tolerancia en la sincronización.
Ensamblar un objeto de transferencia de datos a partir de objetos de dominio: Un
objeto de transferencia de datos no conoce cómo conectarse con objetos de dominio. Esto
se debe a que debería implementarse en ambos lados de la conexión. Por esa razón, no
quiero que el objeto de transferencia de datos dependa del objeto de dominio. Tampoco
quiero que los objetos de dominio dependan del objeto de transferencia de datos, ya que la
estructura del objeto de transferencia de datos cambiará cuando modifique los formatos de
interfaz. Como regla general, quiero mantener el modelo de dominio independiente de las
interfaces externas. Como resultado, me gusta crear un objeto ensamblador separado
responsable de crear un objeto de transferencia de datos a partir del modelo de dominio y
actualizar el modelo a partir de él (Figura 15.4). El ensamblador es un ejemplo de un
mapeador (Mapper) en el sentido de que mapea entre el objeto de transferencia de datos y
los objetos de dominio. También puedo tener varios ensambladores que compartan el
mismo objeto de transferencia de datos. Un caso común para esto es tener diferentes
semánticas de actualización en diferentes escenarios utilizando los mismos datos. Otra
razón para separar el ensamblador es que el objeto de transferencia de datos se puede
generar fácilmente automáticamente a partir de una descripción simple de datos. Generar el
ensamblador es más difícil e incluso a menudo imposible.
Cuándo usarlo:
Utilice un objeto de transferencia de datos siempre que necesite transferir múltiples
elementos de datos entre dos procesos en una única llamada de método.
Existen algunas alternativas al objeto de transferencia de datos, aunque no soy fan
de ellas. Una es no utilizar un objeto en absoluto, sino simplemente utilizar un método de
configuración con muchos argumentos o un método de obtención con varios argumentos
pasados por referencia. El problema es que muchos lenguajes, como Java, solo permiten
un objeto como valor de retorno, por lo que, aunque se puede utilizar para actualizaciones,
no se puede utilizar para recuperar información sin jugar con devoluciones de llamada.
Otra alternativa es utilizar alguna forma de representación de cadena directamente,
sin un objeto que actúe como interfaz. Aquí el problema es que todo lo demás está
acoplado a la representación de cadena. Es bueno ocultar la representación precisa detrás
de una interfaz explícita; de esta manera, si desea cambiar la cadena o reemplazarla por
una estructura binaria, no tiene que cambiar nada más.
En particular, vale la pena crear un objeto de transferencia de datos cuando se
desea comunicar entre componentes utilizando XML. El DOM XML es una molestia para
manipular, y es mucho mejor utilizar un objeto de transferencia de datos que lo encapsule,
especialmente porque el objeto de transferencia de datos es muy fácil de generar.
Otro propósito común de un objeto de transferencia de datos es actuar como una
fuente común de datos para varios componentes en diferentes capas. Cada componente
realiza algunos cambios en el objeto de transferencia de datos y luego lo pasa a la siguiente
capa. El uso de conjuntos de registros (Record Set) en COM y .NET es un buen ejemplo de
esto, donde cada capa sabe cómo manipular datos basados en conjuntos de registros, ya
sea que provengan directamente de una base de datos SQL o hayan sido modificados por
otras capas. .NET amplía esto al proporcionar un mecanismo incorporado para serializar
conjuntos de registros en XML.
Aunque este libro se centra en sistemas síncronos, hay un interesante uso asíncrono
para el objeto de transferencia de datos. Esto ocurre cuando se desea utilizar una interfaz
tanto de forma síncrona como asíncrona. Devuelva un objeto de transferencia de datos
como de costumbre para el caso síncrono; para el caso asíncrono, cree una carga perezosa
(Lazy Load) del objeto de transferencia de datos y devuélvala. Conecte la carga perezosa
(Lazy Load) a donde deberían aparecer los resultados de la llamada asíncrona. El usuario
del objeto de transferencia de datos solo se bloqueará cuando intente acceder a los
resultados de la llamada.
Layer Supertype
Layer Supertype (Supertipo de Capa) es un patrón que consiste en tener un tipo que
actúa como supertipo para todos los tipos en una capa determinada. Es común que todos
los objetos en una capa tengan métodos que no deseas duplicar en todo el sistema. Puedes
mover todo este comportamiento a un supertipo común de capa.
Cómo funciona
Layer Supertype es una idea simple que conduce a un patrón muy breve. Lo único
que necesitas es una superclase para todos los objetos en una capa, por ejemplo, una
superclase de objetos de dominio para todos los objetos de dominio en un Modelo de
Dominio. Las características comunes, como el almacenamiento y manejo de campos de
identidad, pueden ubicarse allí. De manera similar, todos los Mapeadores de Datos en la
capa de mapeo pueden tener una superclase que se base en el hecho de que todos los
objetos de dominio tienen un supertipo común.Si tienes más de un tipo de objeto en una
capa, es útil tener más de un supertipo de capa.
Cuándo usarlo
Utiliza Layer Supertype cuando tengas características comunes en todos los objetos
de una capa. A menudo, aplico este patrón automáticamente porque hago mucho uso de
características comunes.
Service Stub (Módulo simulado)
Elimina la dependencia de servicios problemáticos durante las pruebas.
Los sistemas empresariales a menudo dependen de servicios de terceros, como la
puntuacióncrediticia, la búsqueda de tasas de impuestos y los motores de precios.
Cualquier desarrollador que haya construido un sistema de este tipo puede hablar de la
frustración de depender de recursos completamente fuera de su control. La entrega de
funcionalidades es impredecible y, como estos servicios suelen ser remotos, la confiabilidad
y el rendimiento también pueden verse afectados.
Al menos, estos problemas ralentizan el proceso de desarrollo. Los desarrolladores
se quedan esperando a que el servicio vuelva a estar en línea o tal vez introducen algunos
parches en el código para compensar características que aún no se han entregado. Mucho
peor, y bastante probablemente, estas dependencias darán lugar a momentos en los que
las pruebas no se pueden ejecutar. Cuando las pruebas no se pueden ejecutar, el proceso
de desarrollo está roto.
Reemplazar el servicio durante las pruebas con un Service Stub que se ejecute localmente,
rápido y en memoria mejora tu experiencia de desarrollo.
Cómo funciona
El primer paso es definir el acceso al servicio con un Gateway (Puerta de enlace). El
Gateway no debe ser una clase, sino una Interfaz Separada para que puedas tener una
implementación que llame al servicio real y al menos una que sea solo un Service Stub
(Módulo simulado). La implementación deseada del Gateway se debe cargar utilizando un
Plugin (Complemento). La clave para escribir un Service Stub es mantenerlo lo más simple
posible, ya que la complejidad derrotará su propósito.
Vamos a recorrer el proceso de simular un servicio de impuestos sobre las ventas
que proporciona el monto y la tasa de impuesto estatal, dado una dirección, tipo de producto
y monto de venta. La forma más sencilla de proporcionar un Service Stub es escribir dos o
tres líneas de código que utilicen una tasa de impuesto fija para satisfacer todas las
solicitudes.
Por supuesto, las leyes fiscales no son tan simples. Ciertos productos están exentos
de impuestos en ciertos estados, por lo que confiamos en nuestro servicio de impuestos real
para conocer las combinaciones de productos y estados que están exentos de impuestos.
Sin embargo, gran parte de la funcionalidad de nuestra aplicación depende de si se cobran
impuestos, por lo que debemos acomodar la exención de impuestos en nuestro Service
Stub (Módulo simulado). El medio más sencillo de agregar este comportamiento al stub es
mediante una declaración condicional que exime una combinación específica de dirección y
producto, y luego utiliza esos mismos datos en cualquier caso de prueba relevante. El
número de líneas de código en nuestro stub aún se puede contar con una mano.
Un Service Stub más dinámico mantiene una lista de combinaciones de productos y
estados exentos, lo que permite a los casos de prueba agregar elementos a ella. Incluso
aquí, estamos hablando de aproximadamente 10 líneas de código. Mantenemos las cosas
simples dada nuestra intención de acelerar el proceso de desarrollo.
El Service Stub dinámico plantea una pregunta interesante sobre la dependencia
entre este y los casos de prueba. El Service Stub depende de un método de configuración
para agregar exenciones que no está en la interfaz original del Gateway (Puerta de enlace)
del servicio de impuestos. Para aprovechar un Plugin (Complemento) para cargar el Service
Stub, este método debe agregarse al Gateway (Puerta de enlace), lo cual está bien, ya que
no agrega mucho ruido a tu código y se hace en nombre de las pruebas. Asegúrate de que
la implementación del Gateway que llama al servicio real genere fallas de aserción dentro
de cualquier método de prueba.
Cuándo usarlo
Utiliza Service Stub siempre que encuentres que la dependencia de un servicio en
particular dificulta tu desarrollo y pruebas.
Muchos practicantes de Extreme Programming utilizan el término "Mock Object" para
referirse a un Service Stub. Nosotros hemos optado por utilizar "Service Stub" porque ha
estado presente durante más tiempo.
Registry
Un registro es un objeto ampliamente conocido que otros objetos pueden utilizar
para encontrar objetos y servicios comunes. Cuando deseas encontrar un objeto,
generalmente comienzas con otro objeto que tiene una asociación con él y utilizas la
asociación para navegar hasta él. Sin embargo, en algunos casos, es posible que no tengas
un objeto adecuado para comenzar. Puede que conozcas el número de identificación (ID)
del cliente pero no tengas una referencia a él. En este caso, necesitas algún tipo de método
de búsqueda, pero la pregunta es: ¿Cómo accedes al buscador?
Un Registro es esencialmente un objeto global, o al menos parece uno, aunque no
sea tan global como parece.
Cómo funciona:
Al igual que con cualquier objeto, debes pensar en el diseño de un Registro en
términos de interfaz e implementación. Y, como ocurre con muchos objetos, ambos son
bastante diferentes, aunque a menudo se comete el error de pensar que deberían ser
iguales.
Lo primero en pensar es la interfaz, y para los Registros, mi interfaz preferida son los
métodos estáticos. Un método estático en una clase es fácil de encontrar en cualquier lugar
de una aplicación. Además, puedes encapsular cualquier lógica que desees dentro del
método estático, incluida la delegación a otros métodos, ya sean estáticos o de instancia.
Sin embargo, solo porque tus métodos sean estáticos no significa que tus datos
deban estar en campos estáticos. De hecho, casi nunca uso campos estáticos modificables
a menos que sean constantes.
Antes de decidir cómo almacenar tus datos, piensa en el alcance de los datos. Los
datos de un Registro pueden variar según los diferentes contextos de ejecución. Algunos
son globales en todo el proceso, otros globales en un hilo y otros globales en una sesión.
Diferentes alcances requieren implementaciones diferentes, pero no requieren interfaces
diferentes. El programador de la aplicación no necesita saber si una llamada a un método
estático proporciona datos con alcance de proceso o alcance de hilo. Puedes tener
diferentes Registros para diferentes alcances, pero también puedes tener un solo Registro
en el que diferentes métodos tengan diferentes alcances.
Si tus datos son comunes a todo un proceso, un campo estático es una opción. Sin
embargo, rara vez uso campos estáticos modificables porque no permiten la sustitución.
Puede ser extremadamente útil poder sustituir un Registro por un propósito particular,
especialmente para pruebas (un Plugin es una buena manera de hacerlo).
Para un Registro con alcance de proceso, la opción habitual es un singleton [Gang
of Four]. La clase Registro contiene un solo campo estático que almacena una instancia del
Registro. Cuando las personas utilizan un singleton, a menudo hacen que el llamador
acceda explícitamente a los datos subyacentes (Registro.instanciaUnica.obtenerFoo()), pero
yo prefiero un método estático que oculte el objeto singleton (Registro.obtenerFoo()). Esto
funciona especialmente bien porque los lenguajes basados en C permiten que los métodos
estáticos accedan a datos de instancia privados.
Los singletons se utilizan ampliamente en aplicaciones de un solo hilo, pero pueden
ser un problema en aplicaciones de múltiples hilos. Esto se debe a que es demasiado fácil
para múltiples hilos manipular el mismo objeto de formas impredecibles. Puede que puedas
solucionarlo con sincronización, pero la dificultad de escribir el código de sincronización
probablemente te llevará a un manicomio antes de solucionar todos los errores. Por esa
razón, no recomiendo utilizar un singleton para datos modificables en un entorno de
múltiples hilos. Sin embargo, funciona bien para datos inmutables, ya que cualquier cosa
que no pueda cambiar no tendrá problemas de conflicto de hilos. Por lo tanto, algo como
una lista de todos los estados de los Estados Unidos es un buen candidato para un Registro
con alcance de proceso. Estos datos se pueden cargar cuando el proceso se inicia y nunca
necesitan modificarse, o se pueden actualizar raramente con alguna interrupción del
proceso.
Un tipo comúnde datos de Registro tiene alcance de hilo. Un buen ejemplo es una
conexión de base de datos. En este caso, muchos entornos te brindan alguna forma de
almacenamiento específico del hilo, como la variable local de Java. Otra técnica es un
diccionario indexado por hilo, cuyo valor es un objeto de datos adecuado. Una solicitud de
conexión implica una búsqueda en ese diccionario por el hilo actual.
Lo importante de recordar sobre los datos con alcance de hilo es que no se ven
diferentes a los datos con alcance de proceso. Aún puedo usar un método como
Registro.obtenerConexionBaseDatos(), que tiene la misma forma cuando estoy accediendo
a datos con alcance de proceso.
La búsqueda en un diccionario también es una técnica que se puede utilizar para
datos con alcance de sesión. Aquí necesitas un ID de sesión, pero se puede colocar en un
registro con alcance de hilo cuando comienza una solicitud. Cualquier acceso posterior a los
datos de la sesión puede buscar los datos en un mapa indexado por sesión utilizando el ID
de sesión que se guarda en el almacenamiento específico del hilo.
Si estás utilizando un Registro con alcance de hilo y métodos estáticos, es posible
que te encuentres con un problema de rendimiento cuando varios hilos los utilicen. En ese
caso, el acceso directo a la instancia del hilo evitará el cuello de botella.
Algunas aplicaciones pueden tener un solo Registro, otras pueden tener varios. Los
Registros generalmente se dividen por capa del sistema o por contexto de ejecución. Mi
preferencia es dividirlos según su uso, en lugar de por su implementación.
Cuándo usarlo:
A pesar de la encapsulación de un método, un Registro sigue siendo un dato global
y, como tal, es algo que me resulta incómodo utilizar. Casi siempre veo alguna forma de
Registro en una aplicación, pero siempre trato de acceder a objetos a través de referencias
regulares entre objetos. Básicamente, debes usar un Registro sólo como último recurso.
Existen alternativas al uso de un Registro. Una de ellas es pasar los datos
ampliamente necesarios como parámetros. El problema con esto es que se agregan
parámetros a las llamadas de método donde no los necesita el método llamado, sino algún
otro método que se llama en varias capas profundas del árbol de llamadas. Pasar un
parámetro cuando no se necesita el 90% del tiempo es lo que me lleva a usar un Registro
en su lugar.
Otra alternativa que he visto en lugar de un Registro es agregar una referencia a los
datos comunes a los objetos cuando se crean. Aunque esto agrega un parámetro adicional
en un constructor, al menos solo se usa en ese constructor. A menudo es más molestia de
lo que vale, pero si tienes datos que solo se utilizan en un subconjunto de clases, esta
técnica te permite restringir las cosas de esa manera.
Uno de los problemas con un Registro es que debes modificarlo cada vez que
agregas un nuevo dato. Por eso, algunas personas prefieren utilizar un mapa como
contenedor de datos globales. Prefiero la clase explícita porque mantiene los métodos
explícitos, por lo que no hay confusión sobre qué clave usar para encontrar algo. Con una
clase explícita, solo necesitas mirar el código fuente o la documentación generada para ver
qué está disponible. Con un mapa, debes buscar lugares en el sistema donde se lee o se
escribe en el mapa para descubrir qué clave se utiliza o confiar en una documentación que
rápidamente queda obsoleta. Una clase explícita también te permite mantener la seguridad
de tipos en un lenguaje de tipado estático, así como encapsular la estructura del Registro
para poder refactorizarlo a medida que crece el sistema. Un mapa desnudo también carece
de encapsulación, lo que dificulta ocultar la implementación. Esto es particularmente
incómodo si debes cambiar el alcance de ejecución de los datos. Así que hay momentos en
los que es correcto usar un Registro, pero recuerda que cualquier dato global siempre es
sospechoso hasta que se demuestre su inocencia.
Money:
Una gran proporción de las computadoras en este mundo manipulan dinero, por lo
que siempre me ha intrigado que el dinero no sea en realidad un tipo de dato de primera
clase en ningún lenguaje de programación popular. La falta de un tipo de dato ocasiona
problemas, especialmente en lo que respecta a las monedas. Si todas tus operaciones se
realizan en una sola moneda, esto no representa un problema grave, pero una vez que
involucras múltiples monedas, quieres evitar sumar tus dólares a tus yenes sin tener en
cuenta las diferencias de moneda. El problema más sutil está relacionado con el redondeo.
Las operaciones monetarias a menudo se redondean a la unidad monetaria más pequeña.
Cuando haces esto, es fácil perder centavos (o su equivalente local) debido a errores de
redondeo.
Lo bueno de la programación orientada a objetos es que puedes solucionar estos
problemas creando una clase llamada Money (Dinero) que los maneje. Sin embargo, resulta
sorprendente que ninguna de las bibliotecas de clases base principales lo haga en realidad.
Cómo funciona
La idea básica es tener una clase Money con campos para la cantidad numérica y la
moneda. Puedes almacenar la cantidad como un tipo integral o un tipo decimal fijo. El tipo
decimal es más sencillo para algunas manipulaciones, mientras que el tipo integral es mejor
para otras. Debes evitar cualquier tipo de dato de punto flotante, ya que esto introducirá los
tipos de problemas de redondeo que se pretenden evitar con Money. La mayoría de las
veces, las personas desean que los valores monetarios se redondeen a la unidad más
pequeña completa, como los centavos en el dólar. Sin embargo, a veces se necesitan
unidades fraccionarias. Es importante dejar claro con qué tipo de dinero estás trabajando,
especialmente en una aplicación que utiliza ambos tipos. Tiene sentido tener diferentes
tipos para los dos casos, ya que se comportan de manera bastante diferente en
operaciones aritméticas. Money es un objeto de valor (Value Object, 486), por lo que se
deben anular las operaciones de igualdad y código hash en función de la moneda y la
cantidad.
Money necesita operaciones aritméticas para que puedas utilizar objetos de dinero
tan fácilmente como utilizas números. Pero las operaciones aritméticas para el dinero tienen
algunas diferencias importantes con las operaciones de dinero en los números. Lo más
obvio es que cualquier suma o resta debe tener en cuenta la moneda para poder reaccionar
si intentas sumar dinero de diferentes monedas. La respuesta más simple y común es
considerar un error la suma de diferentes monedas. En situaciones más sofisticadas,
puedes usar la idea de una bolsa de dinero de Ward Cunningham. Esta es un objeto que
contiene dinero de múltiples monedas en un solo objeto. Este objeto luego puede participar
en cálculos al igual que cualquier objeto de dinero. También se puede evaluar su valor en
una moneda.
La multiplicación y la división resultan más complicadas debido a los problemas de
redondeo. Cuando multiplicas dinero, lo haces con un escalar. Si quieres agregar un
impuesto del 5% a una factura, multiplicas por 0.05, por lo que ves multiplicación por tipos
numéricos regulares.
La complicación incómoda viene con el redondeo, especialmente al asignar dinero
entre diferentes lugares. Aquí hay un simple dilema planteado por Matt Foemmel.
Supongamos que tengo una regla empresarial que dice que debo asignar la cantidad total
de una suma de dinero a dos cuentas: 70% a una cuenta y 30% a otra. Tengo 5 centavos
para asignar. Si hago el cálculo, termino con 3.5 centavos y 1.5 centavos. No importa cómo
redondee estos valores, tendré problemas. Si hago el redondeo habitual al número más
cercano, 1.5 se convierte en 2 y 3.5 se convierte en 4. Así que termino ganando un centavo.
Redondear hacia abajo me da 4 centavos y redondear hacia arriba me da 6 centavos. No
hay un esquema general de redondeo que pueda aplicar a ambos casos para evitar perder
o ganar un centavo. He visto varias soluciones a este problema.
• Tal vez la más común sea ignorarlo,

Continuar navegando