Inversione di dipendenza senza metodi statici?

1

È chiaro che il principio di inversione delle dipendenze e l'uso delle interfacce rendono i componenti software meno accoppiati e promuovono la manutenibilità. D'altra parte, questi principi rendono necessario l'uso di metodi e fabbriche statici. Penso che i metodi statici dovrebbero essere fuori da qualsiasi buona progettazione orientata agli oggetti, dal momento che inquinano il codice con pratiche di programmazione imperative che non hanno nulla a che fare con il puro OOP.

La mia domanda è la seguente: esiste un modo per realizzare il DIP e utilizzare in modo efficace le interfacce per separare i componenti senza utilizzare metodi di produzione statici? È così difficile progettare software gestibili con OOP puro e semplice? L'incapsulamento, l'ereditarietà e il polimorfismo non sono davvero sufficienti?

    
posta fsestini 30.05.2015 - 15:17
fonte

3 risposte

10

On the other hand, these principles make using static methods and factories necessary.

Da quando?

I think static methods should be out of any good object-oriented design, since they pollute the code with imperative programming practices that don't have anything to do with pure OOP.

Potresti, ma ti sbaglieresti. Il design OO moderno spesso include metodi statici per incorporare concetti di programmazione funzionale laddove appropriato. Ricorda che il tuo lavoro è fare le cose, non la purezza. A volte la soluzione richiede una funzione standalone (pura), quindi fare qualcos'altro sta rendendo il codice peggiore.

Are encapsulation, inheritance and polymorphism really not enough?

Questi non sono OO. Sono le OO insegnate dalle scuole Java, la fondazione gettata oltre un decennio fa che le persone hanno ripetuto da allora. Non sono davvero sufficienti, poiché hanno un impatto minimo sull'accoppiamento, la misura chiave della qualità del codice. L'inferno, l'eredità stessa è disapprovata in questi giorni in molte situazioni.

In ogni caso, torna alla tua domanda sul codice:

is there a way to accomplish the DIP and effectively use interfaces to decouple components without using static factory methods?

Sì, è semplicissimo. Si crea una classe che prende le sue dipendenze (come interfacce) nel suo costruttore. Il codice che usa quella classe o sa quale tipo concreto creare, o ha un'istanza dell'interfaccia stessa (poiché è una dipendenza). Il codice che usa quella classe passa la dipendenza come parametro. La classe stessa può quindi ignorare il tipo concreto, ottenendo il disaccoppiamento desiderato. Nessuna fabbrica inutile, nessun metodo statico.

    
risposta data 30.05.2015 - 15:55
fonte
2

I metodi statici sono cattivi, non perché siano "pre-OOP", ma proprio perché sono essi stessi delle dipendenze che non possono essere invertiti. Recentemente ho provato a testare una base di codice che includeva una chiamata come

void foo() {
    ...
    Time now = Clock::getCurrentSystemTime();
    ...
}

Ha senso che questo è statico poiché (a) questo codice è sensibile alle prestazioni, e (b) può esserci solo un'ora corrente. Sfortunatamente questo significava che non potevo iniettare un Clock del mio per questi test in modo da poter testare il comportamento del sistema attorno a certe volte. In altre parole, quel codebase ha violato il principio di inversione delle dipendenze.

L'inversione delle dipendenze è semplice: devo solo rendere esplicita la dipendenza e passarla come parametro all'unità di codice corrente (ad es. costruttore di oggetti o parametro di metodo):

type CurrentTimeProvider = () => Time;

void foo(CurrentTimeProvider currentTime) {
    ...
    Time now = currentTime();
    ...
}

// in production:
foo(Clock::getCurrentSystemTime);

// in testing:
foo(() => Time("2000-01-01T00:00:00.000Z"));

Non ci sono metodi statici coinvolti qui in alcun modo significativo. Nei casi più complicati, introdurremmo probabilmente un contenitore di iniezione di dipendenza completo che si occupa della risoluzione di tutte le dipendenze. Il contenitore DI è di per sé un oggetto fabbrica astratto. Idealmente, sarebbe passato in giro esplicitamente. Poi:

type CurrentTimeProvider = () => Time;

void foo(DIContainer deps) {
    ...
    Time now = deps.get(CurrentTimeProvider)();
    ...
}

DiContainer deps;

// in production:
deps.set(CurrentTimeProvider, Clock::getCurrentSystemTime);
foo(deps);

// in testing:
deps.set(CurrentTimeProvider, () => Time("2000-01-01T00:00:00.000Z"));
foo(deps);

Bene, ma le persone si stancano rapidamente di qualsiasi tipo di iniezione di dipendenza manuale. Quindi, supponendo che una dipendenza non invertibile su un sistema di input delle dipendenze sia OK, potremmo rendere statico l'API del contenitore DI (o rendere il contenitore un singleton, che è essenzialmente la stessa cosa). L'esempio precedente potrebbe cambiare in:

type CurrentTimeProvider = () => Time;

void foo() {
    ...
    Time now = DIContainer.get(CurrentTimeProvider)();
    ...
}

// in production:
DIContainer.set(CurrentTimeProvider, Clock::getCurrentSystemTime);
foo();

// in testing:
DIContainer.set(CurrentTimeProvider, () => Time("2000-01-01T00:00:00.000Z"));
foo();

In realtà, è abbastanza elegante e sufficientemente equilibrato. E 'un design OOP abbagliante? Probabilmente no, ma non ti imbatterai in alcun tipo di problema con questo (a meno che tu non provi a prendere in giro il sistema di dipendenze stesso). Se non ti piace un sistema di DI così globale (e non mi piace neanche), puoi scegliere una delle altre strategie disponibili disponibili per DI (di nuovo: parametro esplicito / iniezione costruttore o iniezione di un contenitore DI / oggetto di fabbrica). In una visualizzazione indipendente dalla lingua, l'unica cosa nel tuo programma che non può essere statica è la funzione del punto di ingresso del tuo programma / main .

    
risposta data 30.05.2015 - 16:00
fonte
1

Se vuoi veramente disaccoppiare le tue classi, allora ignorerai tutti i costrutti in fase di compilazione che collegano staticamente i metodi insieme e andranno alla definizione OO originale delle chiamate ai metodi, che è il passaggio dei messaggi.

Ovviamente, questo renderà il tuo codice più difficile da gestire (potenzialmente, ho avuto un grande successo con i sistemi distribuiti che utilizzano un'architettura di passaggio dei messaggi) o il debug tramite errori del compilatore.

IIRC, IoC richiede di passare l'oggetto di configurazione "globale" a ciascun oggetto tramite il suo costruttore anziché l'oggetto che recupera questo oggetto di configurazione quando necessario. I vari framework di contenitori per IoC e DI possono utilizzare statica, fabbriche e quant'altro, ma non è necessario utilizzarli. Qualunque cosa tu faccia, non ottenere confuse le scelte progettuali di un framework per l'aspetto fondamentale del pattern che stai utilizzando.

Quindi, dato il sistema con cui stai lavorando, devi scambiare idealismo per praticità. Se questo significa usare metodi statici, o globali o (horror !!!) anche singleton, quindi usarli. Il tuo compito non è quello di creare codice perfetto, ma di rendere il codice facilmente comprensibile e facilmente gestibile che assolva il suo ruolo nella produzione di un prodotto che faccia qualcosa di utile per le persone.

    
risposta data 30.05.2015 - 16:05
fonte