2009-09-29 6 views
7

Esta pregunta es, en esencia, una pregunta de diseño. Usaré un ejemplo de Java/Java EE para ilustrar la pregunta.Diseño: cuando la línea entre objetos de dominio y objetos de servicio no está clara

Considere una aplicación de correo web que se genera utilizando JPA para la persistencia y EJB para la capa de servicios. Digamos que tenemos un método de servicio en nuestro EJB como esto:

public void incomingMail(String destination, Message message) { 
    Mailbox mb = findMailBox(destination); // who cares how this works 
    mb.addMessage(message); 
} 

Esto es aparentemente un método de negocio razonable. Presumiblemente, el objeto de buzón seguirá adjuntado y guardará los cambios de nuevo en la base de datos. Después de todo, esa es la promesa de una persistencia transparente.

el objeto de buzón tendría este método:

public void addMessage(Message message) { 
    messages.add(message); 
} 

Aquí es donde se pone complicado - suponemos que queremos tener otros tipos de buzones. Digamos que tenemos un AutoRespondingMailbox que responde automáticamente al remitente, y un HelpDeskMailbox que automáticamente abre un boleto de mesa de ayuda con cada correo electrónico recibido.

Lo natural que hacer sería extender buzón, donde AutoRespondingMailbox tiene este método:

public void addMessage(Message message) { 
    String response = getAutoResponse(); 
    // do something magic here to send the response automatically 
} 

El problema es que nuestro objeto maibox y es subclases son "objetos de dominio" (y en este ejemplo, también Entidades JPA). Los chicos de Hibernate (y muchos otros) predican un modelo de dominio no dependiente, es decir, un modelo de dominio que no depende de los servicios proporcionados por contenedor/tiempo de ejecución. El problema con dicho modelo es que el método AutoRespndingMailbox.addMessage() no puede enviar un correo electrónico porque no puede acceder, por ejemplo, a JavaMail.

El mismo problema ocurriría con HelpDeskMailbox, ya que no podría acceder a WebServices o inyección JNDI para comunicarse con el sistema HelpDesk.

Así que uno se ve obligado a poner esta funcionalidad en la capa de servicio, así:

public void incomingMail(String destination, Message message) { 
    Mailbox mb = findMailBox(destination); // who cares how this works 
    if (mb instanceof AutoRespondingMailbox) { 
     String response = ((AutoRespondingMailbox)mb).getAutoResponse(); 
     // now we can access the container services to send the mail 
    } else if (mb instanceof HelpDeskMailbox) { 
     // ... 
    } else { 
     mb.addMessage(message); 
    } 
} 

tener que utilizar instanceof de esa manera es el primer signo de un problema. Tener que modificar esta clase de servicio cada vez que quiera crear una subclase de Buzón es otro signo de un problema.

¿Alguien tiene las mejores prácticas sobre cómo se manejan estas situaciones? Algunos dirían que el objeto Mailbox debería tener acceso a los servicios del contenedor, y esto puede hacerse con un poco de fudging, pero definitivamente está luchando contra el uso previsto de JPA para hacerlo, ya que el contenedor proporciona inyección de dependencia en todas partes excepto en Entidades, indicando claramente que este no es un caso de uso esperado.

Entonces, ¿qué se espera que hagamos? ¿Llenar nuestros métodos de servicio y renunciar al polimorfismo? Nuestros objetos quedan automáticamente relegados a estructuras de estilo C y perdemos la mayor parte del beneficio de OO.

El equipo de Hibernate diría que debemos dividir nuestra lógica empresarial entre la capa de dominio y la capa de servicio, poniendo toda la lógica que no depende del contenedor en las entidades de dominio, y poniendo toda la lógica que depende de el contenedor en la capa de servicios. Puedo aceptar que, si alguien puede darme un ejemplo de cómo hacerlo sin tener que abandonar por completo el polimorfismo y recurrir a instanceof y otras tales maldad

Respuesta

4

un buzón es un buzón ...

... pero un buzón autoresponding es un buzón con algunas reglas que se le atribuye; esto podría decirse que no es una subclase de buzón de correo, sino que es un Agente de Correo que controla uno o más buzones y un conjunto de reglas.

Advertencia: Tengo experiencia limitada con DDD, pero este ejemplo me parece basado en una suposición falsa, p. que el comportamiento de aplicar reglas pertenece al buzón. Creo que la aplicación de reglas a los mensajes es independiente del buzón, es decir, el buzón del destinatario puede ser solo uno de los criterios utilizados por las reglas de filtrado/enrutamiento. Entonces, un servicio de ApplyRules (mensaje) o ApplyRules (buzón, mensaje) tendría más sentido para mí en este caso.

+0

Creo que se olvidó de la intención de mi pregunta. Olvide los detalles del ejemplo y suponga que algunas subclases de una Entidad necesitaban un comportamiento que dependiera de algún servicio proporcionado por el contenedor. – TTar

+0

Estoy de acuerdo con esto. El problema es que su objeto de dominio ya no es un simple titular de datos, pero ahora tiene un comportamiento adjunto (con impactos en la persistencia). Personalmente, esto se siente como algo que debería manejarse a nivel de servicio. –

+0

Entonces, si un objeto de dominio es un simple titular de datos, ¿no es mi objeto de dominio solo una estructura? ¿No es la definición de diseño orientado a objetos que los datos y el comportamiento se combinan en objetos? – TTar

0

Una opción (y tal vez no la mejor opción) sería envuelva el objeto dentro de un objeto "ejecutor".El objeto ejecutor contendría la información de capa de servicio, y el objeto de datos internalizado contendría la información de dominio. A continuación, podría utilizar una fábrica para crear estos objetos, limitando así el alcance de los métodos "instancia de" o elementos similares, y luego los diferentes objetos tendrían algún tipo de interfaz común para usar para poder ejecutar en sus objetos de datos. Es una especie de mezcla entre el patrón de comando - usted tiene el objeto de comando como el ejecutor - y un patrón de estado - el estado es el estado actual del objeto de datos - aunque ninguno es un ajuste exacto.

+0

Pero hacemos todo esto a fin de mantener las dependencias de contenedores fuera de nuestro modelo de dominio. ¿A que final? ¿Solo para hacer las cosas más difíciles? – TTar

+0

Para permitir que diferentes servicios de front-end utilicen el mismo sistema de back-end, entiendo – aperkins

6

Le falta algo: es completamente sensato que el objeto Mailbox dependa de una interfaz que se proporciona en tiempo de ejecución. El "no depender de los servicios de tiempo de ejecución" es correcto, ya que no debería tener dependencias en tiempo de compilación.

Con la única dependencia de ser una interfaz, se puede utilizar un contenedor IoC como StructureMap, Unity, etc., para alimentar a su objeto de una instancia de prueba en lugar de una instancia de tiempo de ejecución.

Al final, el código para un AutoRespondingMailbox podría tener este aspecto:

public class AutoRespondingMailbox { 
    private IEmailSender _sender; 

    public AutoRespondingMailbox(IEmailSender sender){ 
     _sender = sender; 
    } 

    public void addMessage(Message message){ 
     String response = getAutoResponse(); 
     _sender.Send(response); 
} 

Tenga en cuenta que esta clase no depende de algo, pero no es necesariamente proporcionada por el tiempo de ejecución - para una unidad de prueba, fácilmente puede proporcionar un IEmailSender ficticio que escribe en la consola, etc. Además, si su plataforma cambia o los requisitos cambian, puede proporcionar fácilmente un IEmailSender diferente en la construcción que utiliza una metodología diferente a la original. Esa es la razón de la actitud de "límite de dependencias".

+0

Creo que los chicos de Hibernate dirían lo contrario. Realmente creen que la lógica comercial puede/debe dividirse. No veo cómo se puede dividir sin hacer que todo se convierta en un desastre. – TTar

+0

¿Cómo es que esto no divide la lógica comercial? La única lógica en AutoRespondingMailbox es que debería obtener una respuesta de alguna parte y enviarla automáticamente. No depende de otra cosa que la existencia de una interfaz particular. Esa interfaz se puede proporcionar de muchas maneras, muchas de las cuales no están relacionadas en absoluto con el código compilado. –

1

No tengo mucha experiencia con DDD, pero tengo una sugerencia sobre cómo se puede solucionar esto.

Hubiera hecho el resumen de la clase MailBox y luego hice las 3 implementaciones con MailBox como su superclase.

Creo que la denominación del método addMessage (...) podría hacerse mejor. Este nombre - agregar sugiere que el mensaje proporcionado debería simplemente agregarse al buzón, al igual que un setter, pero en lugar de reemplazar un valor existente, simplemente agrega el mensaje proporcionado a algún tipo de almacenamiento.

Pero lo que estás buscando es más bien un comportamiento. ¿Qué ocurre si el buzón abstracto obliga a todas las subclases a implementar el método public void handleIncommingMessage(Message message);?

Entonces su método findMailBox(destination); decide de alguna manera qué instancia de buzón se debe recuperar, que ya es su responsabilidad.

Al instanciar las diferentes subclases del buzón, cada subclase podría tener diferentes necesidades sobre cómo manejar un mensaje entrante.Pero esto puede ser separada haciendo lo siguiente:

Interfaz funcional:

public interface MessageHandler { 
    void handleMessage(Message message); 
} 

clase abstracta:

public abstract MailBox{ 
    private MessageHandler handler; 

    protected MailBox(MessageHandler handler){ 
     this.handler = handler; 
    } 

instanciation:

MailBox mb1 = new MailStorage(new DefaultMessageHandler()); 
MailBox mb2 = new AutoreplyingMailBox(new AutoReplyingMessageHandler()); 
MailBox mb3 = new HelpDeskMailBox(new HelpDeskMessageHandler()); 

Y si lo desea, incluso se podría deshacerse de todas las diferentes subclases del MailBox y en su lugar solo hacer diferentes implementaciones de la interfaz MessageHandler.

Dependiendo del destino que se proporcione al método findMailBox, solo necesitaría instanciar un MailBox (no abstracto en este caso) y proporcionarle la implementación MessageHandler correcta.

Eso haría que el MailBox.handleIncommingMessage(...) solamente hacer una cosa (o dos):

public class MailBox { 

    private MessageHandler messageHandler; 

    public MailBox(MessageHandler messageHandler){ 
     this.messageHandler = messageHandler; 
    } 

    public void handleIncommingMessage(Message message){ 
     addMessage(message); 
     this.messageHandler.handleMessage(message); 
    } 
} 

El código final en su ejemplo sería entonces algo como esto

public void incomingMail(String destination, Message message) { 
    Mailbox mb = findMailBox(destination); // who cares how this works 
    mb.handleIncommingMessage(message); 
} 

Este método no tendrá que se editará cuando se introduzca un nuevo tipo de MailBox o MessageHandler. La lógica está separada de los datos, la lógica de lo que sucede cuando se agrega un mensaje (addMessage/handleIncommingMessage) se mantiene en la implementación de MailHandler.

Cuestiones relacionadas