Come può essere modellata la scelta esplicita?
Se hai bisogno di modellare una scelta tra due tipi completamente diversi, la maggior parte dei linguaggi orientati agli oggetti non offre un modo integrato per modellare quella scelta (come i sindacati taggati oi tipi di somma). Invece, puoi usare due variabili nullable di diversi tipi e assicurarti che sia impostato esattamente uno di questi. Ad esempio:
class Message {
private final String text;
private final InternalContact internalContact;
private final ExternalContact externalContact;
private Message(String text, InternalContact internalContact, ExternalContact externalContact) {
// only one of the contacts may be set:
// (the "==" operator behaves as "not xor")
if ((internalContact == null) == (externalContact == null)) {
throw IllegalArgumentException("either internalContact xor externalContact must be set!");
}
this.text = text;
this.internalContact = internalContact;
this.externalContact = externalContact;
}
public static Message toInternalContact(String text, InternalContact contact) {
return new Message(text, contact, null);
}
public static Message toExternalContact(String text, ExternalContact contact) {
return new Message(text, null, contact);
}
...
}
Usato come: Message m = Message.toExternalContact("hello", externalContact)
.
Tale design consente ai due tipi di contatto di essere completamente indipendenti, ma qualsiasi codice che utilizza il tuo messaggio deve essere in grado di gestire entrambi.
In che modo le interfacce possono essere astratte per una scelta?
Se devi solo eseguire azioni specifiche con questi contatti, puoi invece descrivere quelle azioni in un'interfaccia:
interface MessageContact {
String getEmailAddress();
String getDisplayName();
boolean isTrusted();
}
Questo semplifica la nostra classe di messaggio:
class Message {
private final String text;
private final MessageContact contact;
public Message(String text, MessageContact contact) {
this.text = text;
this.contact = contact;
}
...
}
Per creare un messaggio, dobbiamo racchiudere i contatti interni o esterni nell'interfaccia MessageContact, che possiamo fare con il Pattern adattatore adattatore . Ecco le classi di adattatori per l'interfaccia:
class ExternalMessageContact implements MessageContact {
private final ExternalContact contact;
public ExternalMessageContact(ExternalContact c) { this.contact = c; }
@Override
String getEmailAddress() { return contact.getAddress(); }
@Override
/// Display name of external contacts is their email address.
String getDisplayName() { return contact.getAddress(); }
@Override
boolean isTrusted() { return false; }
}
class InternalMessageContact implements MessageContact {
private final InternalContact contact;
public InternalMessageContact(InternalContact c) { this.contact = c; }
@Override
String getEmailAddress() { return contact.getAddress(); }
@Override
String getDisplayName() { return contact.getFullName(); }
@Override
boolean isTrusted() { return true; }
}
Usato come: Message m = new Message("hello", new ExternalMessageContact(externalMessage))
.
Naturalmente, puoi anche scrivere helper statici che offrono un'interfaccia più comoda rispetto a chiamare esplicitamente i costruttori dell'adapter, come con
class Message {
...
public static Message toExternalContact(String text, ExternalContact contact) {
return new Message(text, new ExternalMessageContact(contact));
}
}
Ciò offrirebbe quindi la stessa interfaccia di costruzione del primo esempio.
In alternativa, le classi ExternalContact e InternalContact potrebbero implementare direttamente MessageInterface, senza coinvolgere alcun adattatore. Se fare ciò dipende dal fatto che si desideri che le classi di contatti abbiano una dipendenza diretta dall'interfaccia MessageContact.
Quale approccio alle scelte di modello è "migliore"?
Entrambi questi approcci possono modellare una scelta, ma sono radicalmente diversi. Introducendo un'interfaccia, il codice che utilizza il messaggio non può distinguere tra le classi di contatto concrete, tranne che attraverso i metodi forniti dall'interfaccia MessageContact. Il tipo di calcestruzzo è stato "cancellato".
Questa astrazione dell'interfaccia potrebbe essere o non essere desiderabile. La corretta modellazione della scelta direttamente o l'utilizzo di un'interfaccia dipende da come verranno utilizzate le informazioni di contatto e da come il sistema potrebbe evolvere in futuro. Ad esempio:
-
Quando aggiungi nuove funzionalità alla classe InternalContact, questa sarà immediatamente disponibile per gli utenti del messaggio a scelta esplicita. Ma con il messaggio di interfaccia-astrazione, l'interfaccia nasconde quel comportamento. E non puoi aggiungere un nuovo metodo all'interfaccia senza rompere gli adattatori esistenti.
-
Quando aggiungi un nuovo tipo di contatto, il messaggio di scelta esplicita non tiene conto di questo tipo, quindi il nuovo tipo di contatto non può essere utilizzato direttamente. Dovresti aggiornare la classe Message e qualsiasi codice che utilizza la classe Message per considerare una terza opzione. Al contrario, aggiungere un nuovo tipo di contatto con la variante di messaggio di interfaccia-astrazione è facile come aggiungere un altro adattatore.
Entrambe le varianti implicano una scelta "ho un contatto esterno o interno", ma con l'interfaccia questa è incapsulata nella classe dell'adattatore, e la scelta è resa implicitamente dalla spedizione del metodo dinamico.
La differenza tra questi approcci è legata al Principio Aperto / Chiuso. Per esempio. la variante dell'interfaccia è aperta all'estensione, nel senso che il sistema può essere esteso con nuovi tipi di contatto, senza dover modificare i consumatori della classe Message. Tuttavia, se questo codice non fa parte di un'interfaccia "pubblica" (ad es. Una libreria che viene utilizzata da diversi team), il principio Open / Closed non ha importanza: dovresti scegliere la soluzione più semplice che funzioni. Questa potrebbe essere ancora la variante dell'interfaccia perché offre un'interfaccia molto più comoda con cui lavorare ed è potenzialmente più facile da testare.