Dovrei incapsulare un oggetto all'interno di un altro oggetto come metodi o semplicemente accedervi direttamente?

5

Supponiamo di avere una classe A:

public class A{
    public void a(){
    }
}

e la classe B usa A:

public class B{
    private A a;
}

Devo incapsulare A in B:

public class B{
    private A a;
    public void a(){
        a.a();
    }
}

poi

new B().a();

o semplicemente consenti agli utenti di B di accedere direttamente a:

public class B{
    private A a;
    public A getA(){
        return A;
    }
}

poi

new B().getA().a();

Credo che molte persone preferiscano la nuova B (). a (); perché consente agli utenti di B non ha bisogno di sapere di A. Ma ho scoperto che ha almeno uno svantaggio: se aggiungo nuovi metodi in A, devo modificare sia A che B, mentre se permetto agli utenti di accedere A direttamente, quindi non ho bisogno di modificare B e solo:

new B().getA().a();
new B().getA().newMethod(); 

Dovrebbe incapsulare A in B come metodi? O permettere semplicemente agli utenti di B di accedere A direttamente?

    
posta ggrr 05.12.2016 - 03:40
fonte

5 risposte

4

Devi evitare entrambi . Sono entrambi odori di per sé (almeno nel modo descritto).

La prima opzione (come descritto) è un odore se (e solo se ) è davvero necessario replicare tutti i metodi che A espone in B . La classe B dovrebbe esistere per fare qualcosa. Se B replica l'interfaccia pubblica di A ( senza implementare la stessa interfaccia ) mi sembra, che B non faccia nulla (o fa quello che fa e espone l'interfaccia a A - cioè B fa più di una cosa, che è un odore stesso). Ad ogni modo, se sia A che B derivano dalla stessa classe base o interfaccia, è un'altra storia. B potrebbe essere un decoratore che implementa funzionalità aggiuntive. Un esempio sarebbe registrare le chiamate su A.a() . B.a() potrebbe emettere un log-message, chiamare A.a() e quindi emettere un altro log-message.

La seconda opzione (che espone pubblicamente B.a ) è considerata una cattiva idea da molte persone. Ad ogni modo, ci sono sono eccezioni. Può essere considerato ok, se A è una semplice classe di dati (ma non necessariamente).

In entrambi i casi è considerato più pulito se B fornisce la propria interfaccia pubblica che funziona in A in qualche modo.

    
risposta data 05.12.2016 - 12:32
fonte
2

La Legge di Demetra dice che dovresti usare la versione 1. Molti di noi preferiscono chiamarla "suggerimento di Demetra

Don't_repeat_yourself , linee guida generali LEAN, ad esempio usa la versione 2 così quando aggiungi un nuovo metodo a A devi solo aggiungere una volta, non di nuovo in B (e C, D ...) La cosa bella dei principi del software è che hai molti tra cui scegliere. : - (

La mia vista: dipende

Se A è una classe astratta, robusta e ben compresa, come una lista o uno stream, usa la versione 2 e consenti agli utenti di connettersi direttamente. Loro conoscono già l'interfaccia con A, non ottieni nulla nascondendolo. Sii pratico.

Ma se A è una tua classe complessa e disordinata, beta di tua proprietà, limita l'accesso usando la legge di Demeter.

    
risposta data 05.12.2016 - 03:54
fonte
1

Immagina di scrivere la documentazione per B . Se A può essere riferito in modo naturale e direttamente in quella documentazione (diversa dalla parte relativa alla costruzione) dovresti usare la seconda opzione. Se la documentazione si riferirà solo al dettaglio dell'implementazione A (" dietro le quinte usa A per ... "), allora dovresti usare la prima opzione.

Ad esempio, supponiamo che A sia Person e B sia Depratment . La documentazione può dire che un dipartimento ha un manager - non è un dettaglio di implementazione interno, fa parte della definizione ufficiale e esterna di Department ! Quindi, non vogliamo vedere il codice come department.getManagerName() : il dipartimento non ha solo un nome gestore - l'intero oggetto gestore fa parte dell'API - quindi dovremmo avere department.getManager().getName() . Questo significa che possiamo anche fare cose come vacation.addParticipant(department.getManager()) , che sembrerebbe imbarazzante con il primo approccio ( department.sendManagerToVacation(vacation) ugh ...).

Esempio diverso - diciamo A è TCPConnection e B è FooReader che legge codificato Foo s dalla connessione TCP per creare Foo oggetto. In che modo la documentazione di FooReader si riferisce a TCPConnection ? Probabilmente come un dettaglio di implementazione (" FooReader legge da un TCPConnection ") o quando si parla di costruzione (" Usa new FooReader(tcpConnection) per leggere Foo s da una connessione TCP "). La connessione TCP non fa parte dell'API Foo reader - se ottieni un oggetto FooReader tutto ciò che devi sapere è che legge Foo s - non ti interessa da dove li legge. Quindi usiamo la prima opzione qui e usiamo, ad esempio, fooStream.hasMore() invece di fooStream.getTcpConnection().hasMore() . L'utente non si preoccupa se la connessione TCP ha più valori - interessa solo se ci sono più Foo s da leggere.

    
risposta data 05.12.2016 - 16:24
fonte
1

Qual è lo scopo di B?

B è un decoratore che imita tutti i metodi A ma non fa altro che aggiungere alcune funzionalità ad A senza doverle modificare?

Non è chiaro nella tua domanda perché A e B non sembrano implementare la stessa interfaccia che è un requisito del modello decoratore. Ma se è così, allora l'opzione 1 è un'opzione molto valida.

È solo una dipendenza iniettata in B a delegare una funzione specifica?

In tal caso, B non di solito avvolge tutti i metodi in A, ma semplicemente effettua una chiamata a un paio di metodi A, di solito internamente. È un'altra forma di cambiare la funzionalità di B senza realmente cambiare il codice.

È solo un membro di B?

In questo caso devi stare attento all'utilizzo dell'opzione 2 perché il consumatore potrebbe modificare lo stato di A. È questo che vuoi? B sarà usato in più thread? (se così fosse, avresti bisogno di ulteriori considerazioni).

Come puoi vedere ci sono più motivi per incapsulare A piuttosto che esporlo.

Ci sono alcuni principi contro l'opzione 2 come la Legge di Demetra (anche chiamata il principio di minore conoscenza) , la nozione stessa di incapsulamento, un antipattern chiamato Train Wreck .

Legge di Demeter of Principle of less knowledge:

  1. Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
  2. Each unit should only talk to its friends; don't talk to strangers.
  3. Only talk to your immediate friends.

antipattern Train Wreck:

The train wreck anti pattern occurs when a series of method calls are appended to one another all in one line of code. If you find yourself spending a lot of time reading through a series of method calls and trying to figure out what the line of code is actually doing, you probably are dealing with a train wreck.

    
risposta data 05.12.2016 - 16:32
fonte
1

Non esiste una regola dura e veloce. In generale incapsulere A dietro B , solo esponendo funzionalità e dati in base alle esigenze e come senso nel contesto di ciò che B è.

Questo è in generale .

In pratica, sono sicuro che ci sono molti esempi in cui un oggetto Foo è composto da un oggetto Bar e la pragmatica soluzione deve semplicemente esporre Bar come% membro dipublic. Ma questa decisione dovrebbe essere presa caso per caso in quanto dipende da molte cose.

Questo alla fine si riduce a una domanda di modellizzazione. Quindi, a seconda di ciò che il tuo Foo (o B ) sta modellando, avrai risposte diverse.

TLDR: l'incapsulamento è una grande linea guida ma non dimenticare di essere pragmatico.

    
risposta data 05.12.2016 - 03:52
fonte

Leggi altre domande sui tag