Descarga la aplicación para disfrutar aún más
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,
Compartir