Il principio della minima conoscenza


32

Comprendo il motivo alla base del principio della minima conoscenza , ma trovo alcuni svantaggi se provo ad applicarlo nel mio progetto.

Uno degli esempi di questo principio (in realtà come non usarlo), che ho trovato nel libro Head First Design Patterns specifica che è sbagliato chiamare un metodo su oggetti che sono stati restituiti dal chiamare altri metodi, in termini di questo principio .

Ma sembra che a volte sia molto necessario utilizzare tale capacità.

Ad esempio: ho diverse classi: classe di acquisizione video, classe encoder, classe streamer e tutte usano qualche altra classe di base, VideoFrame, e poiché interagiscono tra loro, possono fare ad esempio qualcosa del genere:

streamer codice di classe

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

Come puoi vedere, questo principio non è applicato qui. Questo principio può essere applicato qui, o è che questo principio non può sempre essere applicato in un progetto come questo?




4
Questo è probabilmente un duplicato più vicino.
Robert Harvey,

1
Correlati: codice come questo è un "disastro ferroviario" (in violazione della Legge di Demetra)? (divulgazione completa: questa è la mia domanda)
un CVn del

Risposte:


21

Il principio di cui stai parlando (meglio noto come Legge di Demetra ) per le funzioni può essere applicato aggiungendo un altro metodo di supporto alla tua classe streamer come

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

Ora, ogni funzione "parla solo agli amici", non agli "amici degli amici".

IMHO è una linea guida approssimativa che può aiutare a creare metodi che seguono più rigorosamente il principio della singola responsabilità. In un caso semplice come quello sopra è probabilmente molto supposto se questo vale davvero la seccatura e se il codice risultante è davvero "più pulito", o se espanderà il tuo codice formalmente senza alcun guadagno notevole.


Questo approccio ha il vantaggio di rendere molto più semplice il test unitario, il debug e il motivo del codice.
gntskn,

+1. Tuttavia, c'è un'ottima ragione per non apportare tale modifica al codice è che l'interfaccia di classe potrebbe gonfiarsi con metodi il cui lavoro consiste semplicemente nel fare una o poche chiamate in molti altri metodi (e senza logica aggiuntiva). In alcuni tipi di ambiente di programmazione, ad esempio C ++ COM (in particolare WIC e DirectX), vi è un costo elevato associato all'aggiunta di ogni singolo metodo a un'interfaccia COM, rispetto ad altri linguaggi.
rwong

1
Nella progettazione dell'interfaccia COM in C ++, la scomposizione di grandi classi in piccole classi (il che significa che hai maggiori possibilità di dover parlare con più oggetti) e la riduzione al minimo del numero di metodi di interfaccia sono due obiettivi di progettazione con vantaggi reali (riduzione dei costi) basati su una profonda comprensione della meccanica interna (tabelle virtuali, riusabilità del codice, molte altre cose). Pertanto i programmatori COM C ++ in genere devono ignorare LoD.
rwong

10
+1: La Legge di Demetra dovrebbe davvero essere chiamata The Suggestion of Demeter: YMMV
Binary Worrier

La legge di demeter è un consiglio per fare scelte architettoniche, mentre tu suggerisci un cambiamento estetico, quindi sembra che tu rispetti quella "legge". Sarebbe un'idea sbagliata, perché un cambiamento sintattico non significa improvvisamente che tutto vada bene. Per quanto ne so, la legge di demeter significava sostanzialmente: se vuoi fare OOP, allora smetti di scrivere codice prodecurale con funzioni getter ovunque.
user2180613

39

Il Principio della minima conoscenza o la Legge di Demetra è un avvertimento contro il coinvolgimento della tua classe con dettagli di altre classi che attraversano strato dopo strato. Ti dice che è meglio parlare solo con i tuoi "amici" e non con "amici di amici".

Immagina che ti sia stato chiesto di saldare uno scudo su una statua di un cavaliere in armatura a piastra lucida. Posiziona con cura lo scudo sul braccio sinistro in modo che appaia naturale. Noti che ci sono tre piccoli punti sull'avambraccio, sul gomito e sulla parte superiore del braccio in cui lo scudo tocca l'armatura. Saldi tutti e tre i punti perché vuoi essere sicuro che la connessione sia forte. Ora immagina che il tuo capo sia pazzo perché non può muovere il gomito della sua armatura. Hai pensato che l'armatura non si sarebbe mai mossa e quindi hai creato una connessione immobile tra avambraccio e parte superiore del braccio. Lo scudo dovrebbe connettersi solo al suo amico, avambraccio. Non per avambracci amici. Anche se devi aggiungere un pezzo di metallo per farli toccare.

Le metafore sono belle, ma cosa intendiamo veramente per amico? Qualsiasi cosa un oggetto sappia come creare o trovare è un amico. In alternativa, un oggetto può semplicemente chiedere di ricevere altri oggetti , di cui conosce solo l'interfaccia. Questi non contano come amici perché non viene imposta alcuna aspettativa su come ottenerli. Se l'oggetto non sa da dove provengono perché è passato / iniettato qualcos'altro, allora non è un amico di un amico, non è nemmeno un amico. È qualcosa che l'oggetto sa solo come usare. È una buona cosa.

Quando cerchi di applicare principi come questo è importante capire che non ti vietano mai di realizzare qualcosa. Sono un avvertimento che potresti trascurare di fare più lavoro per ottenere un design migliore che realizzi la stessa cosa.

Nessuno vuole lavorare senza motivo, quindi è importante capire cosa stai uscendo da questo. In questo caso mantiene flessibile il tuo codice. È possibile apportare modifiche e avere un minor numero di altre classi interessate dalle modifiche di cui preoccuparsi. Suona bene ma non ti aiuta a decidere cosa fare a meno che tu non lo prenda come una sorta di dottrina religiosa.

Invece di seguire ciecamente questo principio, prendi una versione semplice di questo problema. Scrivi una soluzione che non segue il principio e una che lo fa. Ora che hai due soluzioni, puoi confrontare quanto ricettivo sia ciascuno ai cambiamenti cercando di farli in entrambi.

Se NON PUOI risolvere un problema seguendo questo principio, probabilmente c'è un'altra abilità che ti manca.

Una soluzione al tuo problema particolare è quella di iniettare il file frame qualcosa (una classe o un metodo) che sappia parlare con i frame in modo da non dover diffondere tutti quei dettagli di chat di frame nella tua classe, che ora sa solo come e quando prendi una cornice.

Questo in realtà segue un altro principio: uso separato dalla costruzione.

frame = encoder->WaitEncoderFrame()

Usando questo codice ti sei preso la responsabilità di acquisire in qualche modo un Frame. Non hai ancora assunto alcuna responsabilità per parlare con a Frame.

frame->DoOrGetSomething(); 

Ora devi sapere come parlare con un Frame, ma sostituiscilo con:

new FrameHandler(frame)->DoOrGetSomething();

E ora devi solo sapere come parlare con il tuo amico FrameHandler.

Ci sono molti modi per raggiungere questo obiettivo e questo non è forse il migliore, ma mostra come seguire il principio non renda il problema irrisolvibile. Richiede solo più lavoro da te.

Ogni buona regola ha un'eccezione. I migliori esempi che conosco sono le lingue specifiche del dominio interno . A DSL s catena di metodosembra abusare continuamente della Legge di Demetra perché chiami costantemente metodi che restituiscono tipi diversi e li usi direttamente. Perché va bene? Perché in una DSL tutto ciò che viene restituito è un amico attentamente progettato con cui devi parlare direttamente. In base alla progettazione, ti è stato dato il diritto di aspettarti che la catena di metodi di un DSL non cambi. Non hai questo diritto se ti addentri casualmente nella base di codice concatenando tutto ciò che trovi. I migliori DSL sono rappresentazioni o interfacce molto sottili con altri oggetti che probabilmente non dovresti approfondire. Lo dico solo perché ho scoperto di aver capito molto meglio la legge di Demeter una volta appreso perché i DSL sono un buon design. Alcuni arrivano al punto di dire che i DSL non violano nemmeno la vera legge di demeter.

Un'altra soluzione è quella di far iniettare qualcos'altro framein te. Se frameproviene da un setter o preferibilmente da un costruttore, non ti assumerai alcuna responsabilità per la costruzione o l'acquisizione di un frame. Questo significa che il tuo ruolo qui è molto più simile a come FrameHandlerssarebbe stato. Invece ora sei colui che è loquace Framee sta facendo qualcos'altro per capire come ottenere un Frame In un certo senso questa è la stessa soluzione con un semplice cambio di prospettiva.

I principi SOLID sono quelli grandi che cerco di seguire. Due rispetto qui sono i principi di inversione di responsabilità singola e dipendenza. È davvero difficile rispettare questi due e finire ancora per violare la Legge di Demetra.

La mentalità che va a violare Demetra è come mangiare in un ristorante a buffet dove prendi quello che vuoi. Con un po 'di lavoro in anticipo puoi fornirti un menu e un server che ti offriranno tutto ciò che ti piace. Siediti, rilassati e punta bene.


2
"Ti dice che è meglio parlare solo con i tuoi" amici "e non con" amici di amici "" ++
RubberDuck

1
Il secondo paragrafo, è stato rubato da qualche parte o l'hai inventato? Questo mi ha fatto comprendere il concetto interamente . Ciò ha reso palese ciò che sono "amici di amici" e quali sono gli svantaggi di intrecciare troppe classi (articolazioni). Una citazione +.
die maus,

2
@diemaus Se ho rubato quel paragrafo di scudo da qualche parte, la sua fonte mi è fuoriuscita dalla testa. All'epoca il mio cervello pensava solo che fosse intelligente. Quello che ricordo è che l'ho aggiunto dopo molti dei voti, quindi è bello vederlo convalidato. Sono contento che abbia aiutato.
candied_orange,

19

Il design funzionale è migliore del design orientato agli oggetti? Dipende.

MVVM è meglio di MVC? Dipende.

Amos & Andy o Martin e Lewis? Dipende.

Da cosa dipende? Le scelte che fai dipendono dal modo in cui ogni tecnica o tecnologia soddisfa i requisiti funzionali e non funzionali del tuo software, soddisfacendo adeguatamente gli obiettivi di progettazione, prestazioni e manutenibilità.

[Alcuni libri] dicono che [alcune cose] sono sbagliate.

Quando lo leggi in un libro o in un blog, valuta il reclamo in base al merito; cioè, chiedi perché. Non esiste una tecnica giusta o sbagliata nello sviluppo del software, c'è solo "quanto bene questa tecnica raggiunge i miei obiettivi? È efficace o inefficace? Risolve un problema ma ne crea uno nuovo? Può essere ben compreso dall'intero team di sviluppo, o è troppo oscuro? "

In questo caso particolare - l'atto di chiamare un metodo su un oggetto restituito da un altro metodo - poiché esiste un modello di progettazione reale che codifica questa pratica (Factory), è difficile immaginare come si possa affermare che è categoricamente sbagliato.

Il motivo per cui è chiamato il "Principio della conoscenza minima" è che "Low Coupling" è una qualità desiderabile di un sistema. Gli oggetti che non sono strettamente collegati tra loro funzionano in modo più indipendente e sono quindi più facili da mantenere e modificare individualmente. Ma come mostra il tuo esempio, ci sono momenti in cui l'accoppiamento elevato è più desiderabile, in modo che gli oggetti possano coordinare in modo più efficace i loro sforzi.


2

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.


Associare la tua classe a un'interfaccia restituita da uno dei suoi collaboratori, piuttosto che una classe concreta, mentre un miglioramento, è ancora una fonte di accoppiamento. Se l'interfaccia deve cambiare, significa che la tua classe dovrà cambiare. Il punto importante da eliminare è che l' accoppiamento non è intrinsecamente negativo purché si assicuri di evitare coppie inutili con oggetti instabili o strutture interne . Questo è il motivo per cui la Legge di Demetra deve essere presa con un grande pizzico di sale; ti dice di evitare sempre qualcosa che può o non può essere un problema, a seconda delle circostanze.
Periata Breatta,

@PeriataBreatta - Certamente non posso e non sono d'accordo. Ma vorrei sottolineare una cosa: un'interfaccia , per definizione, rappresenta ciò che deve essere conosciuto al confine tra le due classi. Se "ha bisogno di cambiare", questo è fondamentale : nessun approccio alternativo avrebbe potuto in qualche modo magicamente evitare la codifica necessaria. Contrastate il fatto che facendo una delle tre situazioni che descrivo, ma non usando un'interfaccia, restituisce invece una classe concreta. In 1, Frame, in 2, EncoderFrameWrapper, in 3, Encoder. Ti blocchi in questo approccio. L'interfaccia può adattarsi a tutti.
ToolmakerSteve

@PeriataBreatta ... che dimostra il vantaggio di definirlo esplicitamente come interfaccia. Spero alla fine di migliorare un IDE per renderlo così conveniente, che le interfacce saranno utilizzate molto più pesantemente. La maggior parte degli accessi a più livelli avverrebbe tramite qualche interfaccia, quindi sarebbe molto più facile gestire le modifiche. (Se questo è "eccessivo" per alcuni casi, l'analisi del codice, combinata con le annotazioni su dove siamo disposti a correre il rischio di non avere un'interfaccia, in cambio del piccolo incremento delle prestazioni, potrebbe "compilare" - sostituendolo con una di quelle 3 classi concrete delle 3 "soluzioni".)
ToolmakerSteve

@PeriataBreatta - e quando dico "l'interfaccia può adattarsi a tutti", non sto dicendo "ahhhh, è meraviglioso che questa funzione in una lingua possa coprire questi diversi casi". Sto dicendo che la definizione delle interfacce riduce al minimo le modifiche che potrebbe essere necessario apportare. Nel migliore dei casi, il design può cambiare completamente dal caso più semplice (soluzione 1), al caso più difficile (soluzione 3), senza che l'interfaccia cambi completamente - solo le esigenze interne del produttore sono diventate più complesse. E anche quando sono necessari cambiamenti, tendono ad essere meno pervasivi, IMHO.
ToolmakerSteve
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.