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 interfacetipi, anziché i classtipi, tramite i metodi e le proprietà.
Nel caso del Poster originale (OP), encoder->WaitEncoderFrame()restituirebbe IEncoderFrameinvece di a Frame, e definirebbe quali operazioni sono consentite.
SOLUZIONE 1
Nel caso più semplice, Framee le Encoderclassi sono entrambe sotto il tuo controllo, IEncoderFrameè un sottoinsieme di metodi che Frame già espone pubblicamente e alla Encoderclasse 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 Framedefinizione non è sotto il tuo controllo o non sarebbe appropriato aggiungere IEncoderFramei 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 . Encoderdeve 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.TheFrameviene 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 Encodere 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 Encoderritenesse che l'esposizione Frame framefosse 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 IEncoderFrameda .FrameEncoder
Si noti inoltre che se non hai il permesso di aggiungere IEncoderFramea Frame, o i metodi necessari non si adattano bene al generale Framedi 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 Encodere 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 Framefunzionalità di. Nel secondo, IEncoderFrameè un adattatore. Nel terzo, IEncoderFrameè una partizione nella Encoderfunzionalità di s. Non importa se le tue esigenze cambiano tra queste tre situazioni: l'API rimane la stessa.