La risposta di Doc Brown mostra una classica implementazione da manuale di Law of Demeter - e il fastidio / disorganizzato-code-bloat di aggiungere dozzine di metodi in quel modo è probabilmente il motivo per cui i programmatori, incluso me stesso, spesso non si preoccupano di farlo, anche se dovrebbero.
Esiste un modo alternativo per disaccoppiare la gerarchia degli oggetti:
Esporre i interface
tipi, anziché i class
tipi, tramite i metodi e le proprietà.
Nel caso del Poster originale (OP), encoder->WaitEncoderFrame()
restituirebbe IEncoderFrame
invece di a Frame
, e definirebbe quali operazioni sono consentite.
SOLUZIONE 1
Nel caso più semplice, Frame
e le Encoder
classi sono entrambe sotto il tuo controllo, IEncoderFrame
è un sottoinsieme di metodi che Frame già espone pubblicamente e alla Encoder
classe in realtà non importa cosa fai a quell'oggetto. Quindi, l'implementazione è banale ( codice in c # ):
interface IEncoderFrame {
void DoOrGetSomething();
}
class Frame : IEncoderFrame {
// A method that already exists in Frame.
public void DoOrGetSomething() { ... }
}
class Encoder {
private Frame _frame;
public IEncoderFrame TheFrame { get { return _frame; } }
...
}
SOLUZIONE 2
In un caso intermedio, in cui la Frame
definizione non è sotto il tuo controllo o non sarebbe appropriato aggiungere IEncoderFrame
i metodi a Frame
, una buona soluzione è un adattatore . Questo è ciò che la risposta di CandiedOrange discute, come new FrameHandler( frame )
. IMPORTANTE: se lo fai, è più flessibile se lo esponi come interfaccia , non come classe . Encoder
deve saperlo class FrameHandler
, ma i clienti devono solo saperlo interface IFrameHandler
. O come l'ho chiamato, interface IEncoderFrame
- per indicare che è specificamente Frame visto dal POV di Encoder :
interface IEncoderFrame {
void DoOrGetSomething();
}
// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
Frame _frame;
public EncoderFrameWrapper( Frame frame ) {
_frame = frame;
}
public void DoOrGetSomething() {
_frame....;
}
}
class Encoder {
private Frame _frame;
// Adapter pattern. Appropriate if no access needed to Encoder.
public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }
...
}
COSTO: Allocazione e GC di un nuovo oggetto, EncoderFrameWrapper, encoder.TheFrame
viene chiamato ogni volta . (È possibile memorizzare nella cache quel wrapper, ma questo aggiunge più codice. Ed è facile da programmare in modo affidabile solo se il campo del frame dell'encoder non può essere sostituito con un nuovo frame.)
SOLUZIONE 3
Nel caso più difficile, il nuovo wrapper dovrebbe conoscere entrambi Encoder
e Frame
. Quell'oggetto stesso violerebbe LoD - sta manipolando una relazione tra Encoder e Frame che dovrebbe essere la responsabilità di Encoder - e probabilmente sarà una seccatura per avere ragione. Ecco cosa può succedere se inizi su quella strada:
interface IEncoderFrame {
void DoOrGetSomething();
}
// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
Encoder _owner;
Frame _frame;
public EncoderFrameWrapper( Encoder owner, Frame frame ) {
_owner = owner; _frame = frame;
}
public void DoOrGetSomething() {
_frame.DoOrGetSomething();
// Hmm, maybe this wrapper class should be nested inside Encoder...
_owner... some work inside owner; maybe should be owner-internal details ...
}
}
class Encoder {
private Frame _frame;
...
}
È diventato brutto. Esiste un'implementazione meno complicata, quando il wrapper deve toccare i dettagli del suo creatore / proprietario (Encoder):
interface IEncoderFrame {
void DoOrGetSomething();
}
class Encoder : IEncoderFrame {
private Frame _frame;
// HA! Client gets to think of this as "the frame object",
// but its really me, intercepting it.
public IEncoderFrame TheFrame { get { return this; } }
// This is the method that the LoD approach suggests writing,
// except that we are exposing it only when the instance is accessed as an IEncoderFrame,
// to avoid extending Encoder's already large API surface.
public void IEncoderFrame.DoOrGetSomething() {
_frame.DoOrGetSomething();
... make some change within current Encoder instance ...
}
...
}
Certo, se sapessi che sarei finito qui, potrei non farlo. Potrei semplicemente scrivere i metodi LoD e finirlo. Non è necessario definire un'interfaccia. D'altra parte, mi piace che l'interfaccia racchiuda i relativi metodi. Mi piace come ci si sente a fare le "operazioni tipo frame" a ciò che sembra un frame.
OSSERVAZIONI FINALI
Considera questo: se l'implementatore Encoder
ritenesse che l'esposizione Frame frame
fosse appropriata per la sua architettura complessiva o fosse "molto più semplice dell'implementazione del LoD", sarebbe stato molto più sicuro se invece avesse fatto il primo frammento che mostro - esporre un sottoinsieme limitato di Frame, come interfaccia. Nella mia esperienza, questa è spesso una soluzione completamente praticabile. Basta aggiungere metodi all'interfaccia, se necessario. (Sto parlando di uno scenario in cui "sappiamo" che Frame ha già i metodi necessari, o che sarebbero facili e non controversi da aggiungere. Il lavoro di "implementazione" per ogni metodo sta aggiungendo una riga alla definizione dell'interfaccia.) E sappi che anche nel peggiore scenario futuro, è possibile far funzionare l'API, rimuovendola IEncoderFrame
da .Frame
Encoder
Si noti inoltre che se non hai il permesso di aggiungere IEncoderFrame
a Frame
, o i metodi necessari non si adattano bene al generale Frame
di classe, e la soluzione # 2 non ti si addice, forse perché l'oggetto in più di creazione e distruzione, la soluzione n. 3 può essere vista semplicemente come un modo per organizzare i metodi Encoder
e realizzare il LoD. Non passare attraverso dozzine di metodi. Avvolgili in un'interfaccia e usa "implementazione esplicita dell'interfaccia" (se sei in c #), in modo che sia possibile accedervi solo quando l'oggetto viene visualizzato attraverso quell'interfaccia.
Un altro punto che voglio sottolineare è che la decisione di esporre la funzionalità come interfaccia ha gestito tutte e 3 le situazioni sopra descritte. Nel primo, IEncoderFrame
è semplicemente un sottoinsieme della Frame
funzionalità di. Nel secondo, IEncoderFrame
è un adattatore. Nel terzo, IEncoderFrame
è una partizione nella Encoder
funzionalità di s. Non importa se le tue esigenze cambiano tra queste tre situazioni: l'API rimane la stessa.