2009-09-29 8 views
7

Questa domanda è, nella sua essenza, una domanda di progettazione. Userò un esempio EE Java/Java per illustrare la domanda.Design: quando la linea tra oggetti dominio e oggetti servizio non è chiara

Si consideri un'applicazione di posta elettronica creata utilizzando JPA per la persistenza e EJB per il livello di servizi. Diciamo che abbiamo un metodo di servizio nel nostro EJB come questo:

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

Questo è apparentemente un metodo di business ragionevole. Presumibilmente, l'oggetto Mailbox verrà comunque collegato e ripristinerà senza problemi le modifiche sul database. Dopotutto, questa è la promessa di una persistenza trasparente.

l'oggetto cassetta postale avrebbe questo metodo:

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

Ecco dove diventa complicato - supponiamo che vogliamo avere altri tipi di cassette postali. Supponiamo di avere una casella di risposta automatica che risponda automaticamente al mittente e un HelpDeskMailbox che apra automaticamente un ticket helpdesk con ogni email ricevuta.

La cosa più naturale da fare sarebbe quella di estendere la Posta elettronica, in cui AutoRespondingMailbox ha questo metodo:

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

Il problema è che il nostro oggetto maibox ed è sottoclassi sono "oggetti di dominio" (e in questo esempio, anche Entità JPA). I ragazzi di Hibernate (e molti altri) predicano un modello di dominio non dipendente, ovvero un modello di dominio che non dipende dai servizi forniti dal contenitore/runtime. Il problema con questo modello è che il metodo AutoRespndingMailbox.addMessage() non può inviare un messaggio di posta elettronica perché non può accedere, ad esempio, a JavaMail.

Lo stesso problema si verificava con HelpDeskMailbox, poiché non poteva accedere ai servizi Web o all'iniezione JNDI per comunicare con il sistema HelpDesk.

Così si è costretti a mettere questa funzionalità nel livello di servizio, come questo:

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); 
    } 
} 

dover utilizzare instanceof in questo modo è il primo segno di un problema. La modifica di questa classe di servizio ogni volta che si desidera creare una sottoclasse di cassette postali è un altro segno di un problema.

Qualcuno ha le migliori pratiche su come vengono gestite queste situazioni? Alcuni direbbero che l'oggetto Mailbox dovrebbe avere accesso ai servizi del contenitore, e questo può essere fatto con qualche confusione, ma sta sicuramente combattendo l'uso previsto di JPA per farlo, poiché il contenitore fornisce l'iniezione delle dipendenze ovunque tranne che nelle Entità, indicando chiaramente che questo non è un caso d'uso previsto.

Quindi, cosa dovremmo fare invece? Litigare i nostri metodi di servizio e il polimorfismo di rinuncia? I nostri oggetti vengono automaticamente relegati a strutture in stile C e perdiamo la maggior parte del vantaggio di OO.

Il team di Hibernate direbbe che dovremmo dividere la nostra logica di business tra il livello di dominio e il livello di servizio, mettendo tutta la logica che non dipende dal contenitore nelle entità di dominio e mettendo tutta la logica che dipende da il contenitore nel livello di servizi. Posso accettarlo, se qualcuno può darmi un esempio di come farlo senza dover rinunciare completamente al polimorfismo e ricorrere a instanceof e ad altri tali cattiveria

risposta

4

una cassetta postale è una cassetta postale ...

... ma una cassetta postale autoresponding è una cassetta postale con alcune regole ad esso; questa non è probabilmente una sottoclasse di cassette postali, ma è invece un MailAgent che controlla una o più cassette postali e un insieme di regole.

Avvertenza: Ho esperienza limitata con DDD, ma questo esempio mi sembra basarsi su una falsa ipotesi, ad es. che il comportamento dell'applicazione delle regole appartiene alla casella di posta. Penso che applicare le regole ai messaggi sia indipendente dalla casella di posta, ad esempio la cassetta postale del destinatario potrebbe essere solo uno dei criteri utilizzati dalle regole di filtro/routing. Quindi un servizio ApplyRules (messaggio) o ApplyRules (cassetta postale, messaggio) avrebbe più senso per me in questo caso.

+0

Penso che tu abbia perso l'intento della mia domanda. Dimentica le specifiche dell'esempio e presume che alcune sottoclassi di un determinato comportamento di Entity si basino su alcuni servizi forniti dal contenitore. – TTar

+0

Sono d'accordo con questo. Il problema è che il tuo oggetto dominio non è più un semplice titolare di dati, ma ora hai un comportamento collegato (con impatti sulla persistenza) ad esso. Personalmente questo sembra qualcosa che dovrebbe essere gestito a livello di servizio. –

+0

Quindi, se un oggetto dominio è un semplice titolare di dati, il mio oggetto dominio non è solo una struttura? Non è la definizione di progettazione orientata agli oggetti che i dati e il comportamento sono combinati in oggetti? – TTar

0

Un'opzione (e forse non l'opzione migliore) sarebbe avvolgere l'oggetto all'interno di un oggetto "executor".L'oggetto executor conterrebbe le informazioni del livello di servizio e l'oggetto di dati internalizzato conterrà le informazioni sul dominio. È quindi possibile utilizzare una factory per creare questi oggetti, limitando in tal modo l'ambito dei metodi "instanceof" o elementi simili, quindi i diversi oggetti avranno una sorta di interfaccia comune da utilizzare per poterli eseguire sui propri oggetti dati. È una sorta di mix tra lo schema di comando - si ha l'oggetto comando come l'esecutore - e uno schema di stato - lo stato è lo stato corrente dell'oggetto dati - sebbene nessuno dei due sia esattamente adatto.

+0

Ma facciamo tutto questo in nome del mantenimento delle dipendenze del contenitore dal nostro modello di dominio. A che scopo? Solo per rendere le cose più difficili? – TTar

+0

Per consentire a diversi servizi front-end di utilizzare lo stesso sistema di back-end, sono a conoscenza – aperkins

6

Ti manca qualcosa: è completamente sensato che l'oggetto Mailbox dipenda da un'interfaccia fornita in fase di runtime. Il "non dipendere dai servizi di runtime" è corretto, in quanto non dovresti avere dipendenze in fase di compilazione.

Con l'unica dipendenza che è un'interfaccia, è possibile utilizzare un contenitore IoC come StructureMap, Unity, ecc. Per alimentare l'oggetto come un'istanza di test anziché un'istanza di runtime.

Alla fine, il codice per un AutoRespondingMailbox potrebbe assomigliare a questo:

public class AutoRespondingMailbox { 
    private IEmailSender _sender; 

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

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

Si noti che questa classe non dipende da qualcosa, ma non è necessariamente fornito dal runtime - per un test di unità, potresti facilmente fornire un fittizio IEmailSender che scrive sulla console, ecc. Inoltre, se la tua piattaforma cambia o cambiano i requisiti, puoi facilmente fornire un diverso IEmailSender su una costruzione che utilizza una metodologia diversa da quella originale. Che il è la ragione per l'atteggiamento di "dipendenza dai limiti".

+0

Penso che i ragazzi di Hibernate sostengano il contrario. Credono davvero che la logica aziendale possa/dovrebbe essere suddivisa. Non vedo come possa essere diviso senza rendere tutto un casino. – TTar

+0

In che modo non si suddivide la logica aziendale? L'unica logica in AutoRespondingMailbox è che dovrebbe ricevere una risposta da qualche parte e inviarla automaticamente. Non dipende da nient'altro che dall'esistenza di una particolare interfaccia. Tale interfaccia può essere fornita in vari modi, molti dei quali non sono affatto correlati al codice compilato. –

1

Non ho molta esperienza con DDD, ma ho un suggerimento su come questo potrebbe essere risolto.

Avrei fatto l'abstract della classe MailBox e quindi ho realizzato le 3 implementazioni con MailBox come superclasse.

Penso che la denominazione del metodo addMessage (...) potrebbe essere eseguita meglio. Questo nome - aggiungi sta suggerendo che il messaggio fornito dovrebbe essere semplicemente aggiunto alla casella di posta, proprio come un setter, ma invece di sostituire un valore esistente aggiunge semplicemente il messaggio fornito a qualche tipo di memoria.

Ma quello che stai cercando è piuttosto un comportamento. Cosa accadrebbe se la casella di posta astratta costringesse tutte le sottoclassi a implementare il metodo public void handleIncommingMessage(Message message);?

Quindi il metodo findMailBox(destination); decide in qualche modo quale istanza di cassetta da recuperare, che è già di sua competenza.

Quando si instanzano le diverse sottoclassi della cassetta postale, ciascuna sottoclasse potrebbe avere esigenze diverse su come gestire un messaggio in entrata.Ma questo può essere separato nel seguente modo:

Interfaccia funzionale:

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

classe astratta:

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()); 

E se si vuole, si potrebbe anche sbarazzarsi di tutte le diverse sottoclassi della MailBox e invece fare solo implementa diversi funzioni dell'interfaccia MessageHandler.

A seconda della destinazione fornita al metodo findMailBox, è sufficiente installare un MailBox (non astratto in questo caso) e fornire l'implementazione corretta MessageHandler.

che renderebbero il MailBox.handleIncommingMessage(...) fare solo una cosa (o due):

public class MailBox { 

    private MessageHandler messageHandler; 

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

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

Il codice finale nel tuo esempio sarebbe allora qualcosa di simile

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

Questo metodo non avrà mai a essere modificato quando viene introdotto un nuovo tipo di MailBox o MessageHandler. La logica è separata dai dati, la logica per ciò che accade quando viene aggiunto un messaggio (addMessage/handleIncommingMessage) viene mantenuta nell'implementazione di MailHandler.