Perché dovrei usare l'iniezione di dipendenza?

92

Sto avendo difficoltà a cercare risorse sul perché dovrei usare iniezione di dipendenza . La maggior parte delle risorse che vedo spiega che passa semplicemente un'istanza di un oggetto a un'altra istanza di un oggetto, ma perché? È solo per l'architettura / codice più pulito o influisce sulle prestazioni nel suo complesso?

Perché dovrei fare quanto segue?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Invece del seguente?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}
    
posta Daniel 13.11.2018 - 09:04
fonte

9 risposte

104

Il vantaggio è che senza l'iniezione di dipendenza, la tua classe profilo

  • deve sapere come creare un oggetto Impostazioni (viola il Principio di Responsabilità Unica)
  • Crea sempre l'oggetto Impostazioni nello stesso modo (crea un accoppiamento stretto tra i due)

Ma con l'iniezione di dipendenza

  • La logica per la creazione di oggetti Settings è altrove
  • È facile utilizzare diversi tipi di oggetti Impostazioni

Questo può sembrare (o addirittura essere) irrilevante in questo caso particolare, ma immagina se non stiamo parlando di un oggetto Settings, ma di un oggetto DataStore, che potrebbe avere implementazioni diverse, uno che memorizza i dati nei file e un altro che lo memorizza in un database. E per i test automatici vuoi anche un'implementazione simulata. Ora non vuoi davvero che la classe Profile esegua l'hardcode su quale usa, e ancora più importante, davvero, davvero non vuoi che la classe Profile sappia sui percorsi del filesystem, sulle connessioni DB e sulle password , quindi la creazione di oggetti DataStore ha di succedere altrove.

    
risposta data 13.11.2018 - 10:28
fonte
60

Iniezione delle dipendenze semplifica il test del codice.

Ho imparato questa cosa in prima persona quando mi è stato assegnato il compito di correggere un bug difficoltoso nell'integrazione con PayPal di Magento.

Sorgeva un problema quando PayPal comunicava a Magento un pagamento non andato a buon fine: Magento non avrebbe registrato correttamente l'errore.

Testare una potenziale correzione "manualmente" sarebbe molto noioso: avresti bisogno in qualche modo di attivare una notifica "Failed" su PayPal. Dovresti inviare un e-check, annullarlo e attendere che si verifichi l'errore. Ciò significa più di 3 giorni per testare un cambio di codice di un personaggio!

Fortunatamente, sembra che gli sviluppatori del nucleo di Magento che hanno sviluppato questa funzione avevano in mente i test, e hanno usato un modello di iniezione dipendente per renderlo banale. Questo ci permette di verificare il nostro lavoro con un semplice caso di test come questo:

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

Sono sicuro che il pattern DI ha molti altri vantaggi, ma l'aumentata testabilità è il singolo grande vantaggio nella mia mente .

Se sei curioso della soluzione a questo problema, consulta il repository GitHub qui: link

    
risposta data 13.11.2018 - 15:38
fonte
26

Perché (qual è anche il problema)?

Why should I use dependency injection?

Il miglior codice mnemonico che ho trovato è " new is glue ": ogni volta che utilizzi new nel tuo codice, quel codice è legato a quella specifica implementazione . Se usi ripetutamente nuovi costruttori, creerai una catena di implementazioni specifiche . E poiché non puoi "avere" un'istanza di una classe senza costruirla, non puoi separare quella catena.

Ad esempio, immagina di scrivere un videogioco da gara. Hai iniziato con una classe Game , che crea un RaceTrack , che crea l'8% diCars, ognuno dei quali crea un Motor . Ora se vuoi aggiungere il 4% in più diCars con un'accelerazione diversa, dovrai cambiare ogni classe menzionata , eccetto forse Game .

Codice pulitore

Is this just for cleaner architecture/code

.

Tuttavia, potrebbe sembrare molto meno chiaro in questa situazione, perché è più un esempio di come farlo . Il vantaggio effettivo mostra solo quando sono coinvolte diverse classi ed è più difficile da dimostrare, ma immagina che avresti usato DI nell'esempio precedente. Il codice che crea tutte queste cose potrebbe essere simile a questo:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

Ora è possibile aggiungere queste 4 auto diverse aggiungendo queste linee:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • Non sono state necessarie modifiche in RaceTrack , Game , Car o Motor - il che significa che possiamo essere sicuri al 100% di non aver introdotto nuovi bug!
  • Invece di dover saltare tra diversi file, sarai in grado di vedere la modifica completa sullo schermo allo stesso tempo. Questo è il risultato di vedere la creazione / configurazione / configurazione come una propria responsabilità - non è compito di un'automobile costruire un motore.

Considerazioni sulle prestazioni

or does this affect performance as a whole?

No . Ma per essere completamente onesto con te, potrebbe.

Tuttavia, anche in questo caso, è una quantità così ridicola che non devi preoccuparti. Se a un certo punto del futuro, dovresti scrivere il codice per un tamagotchi con l'equivalente di 5Mhz CPU e 2MB di RAM, allora forse potresti potresti avere a cuore questo.

Nel 99,999% * dei casi, le prestazioni saranno migliori, perché hai impiegato meno tempo a correggere i bug e più tempo a migliorare gli algoritmi ricchi di risorse.

* numero completamente composto

Informazioni aggiunte: "hardcoded"

Non commettere errori, questo è ancora molto "hard-coded" - i numeri sono scritti direttamente nel codice. Non hard-coded significherebbe qualcosa come la memorizzazione di quei valori in un file di testo - ad es. in formato JSON - e quindi li legge da quel file.

Per fare ciò, devi aggiungere codice per leggere un file e poi analizzare JSON. Se consideri di nuovo l'esempio; nella versione non DI, una Car o una Motor ora deve leggere un file. Non sembra troppo sensato.

Nella versione DI, dovresti aggiungerlo al codice che imposta il gioco.

    
risposta data 13.11.2018 - 18:45
fonte
16

Sono sempre stato sconcertato dall'iniezione di dipendenza. Sembrava esistere solo all'interno delle sfere di Java, ma quelle sfere ne parlarono con grande riverenza. È stato uno dei grandi modelli, vedi, che si dice porti ordine nel caos. Ma gli esempi erano sempre complessi e artificiali, stabilendo un non-problema e poi tentando di risolverlo rendendo il codice più complicato.

Ha più senso quando un collega Python dev mi ha dato questa saggezza: è solo passare argomenti a funzioni. È a malapena un modello; più come un promemoria che puoi chiedere qualcosa come argomento, anche se potresti aver fornito un valore ragionevole da solo.

Quindi la tua domanda è approssimativamente equivalente a "perché la mia funzione dovrebbe prendere argomenti?" e ha molte delle stesse risposte, ovvero: per consentire al chiamante di prendere decisioni .

Questo ha un costo, ovviamente, perché ora stai forzando il chiamante per prendere una decisione alcuni (a meno che tu non rendi l'argomento opzionale), e l'interfaccia è un po 'più complesso. In cambio, ottieni flessibilità.

Quindi: c'è una buona ragione per cui hai specificamente bisogno di usare questo particolare tipo / valore Setting ? C'è una buona ragione per cui il codice potrebbe richiedere un diverso% tipo / valore% co_de? (Ricorda, test sono codice !)

    
risposta data 14.11.2018 - 17:44
fonte
7

L'esempio che fornisci non è un'iniezione di dipendenza nel senso classico. L'iniezione delle dipendenze di solito si riferisce al passaggio di oggetti in un costruttore o utilizzando "setter injection" subito dopo la creazione dell'oggetto, per impostare un valore su un campo in un oggetto appena creato.

L'esempio passa un oggetto come argomento a un metodo di istanza. Questo metodo di istanza quindi modifica un campo su quell'oggetto. Iniezione di dipendenza? No. Rompere l'incapsulamento e nascondere i dati? Assolutamente!

Ora, se il codice fosse così:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

Quindi direi che stai usando l'iniezione di dipendenza. La differenza degna di nota è un oggetto Settings passato al costruttore o un oggetto Profile .

Questo è utile se l'oggetto Settings è costoso o complesso da costruire, o Settings è un'interfaccia o una classe astratta in cui esistono molteplici implementazioni concrete per modificare il comportamento del tempo di esecuzione.

Dal momento che accedi direttamente a un campo nell'oggetto Settings invece di chiamare un metodo, non puoi sfruttare il Polymorphism, che è uno dei vantaggi dell'iniezione di dipendenza.

Sembra che le impostazioni per un profilo siano specifiche per quel profilo. In questo caso, farei una delle seguenti azioni:

  1. Crea un'istanza dell'oggetto Impostazioni all'interno del costruttore Profile

  2. Passa l'oggetto Settings nel costruttore e copia i singoli campi che si applicano al profilo

Onestamente, passando l'oggetto Impostazioni a deactivateProfile e quindi modificando un campo interno dell'oggetto Impostazioni è un odore di codice. L'oggetto Impostazioni dovrebbe essere l'unico a modificare i suoi campi interni.

    
risposta data 13.11.2018 - 15:00
fonte
7

So che arriverò in ritardo a questa festa, ma ritengo che un punto importante sia mancato.

Why should I do this:

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Non dovresti. Ma non perché l'iniezione di dipendenza è una cattiva idea. È perché questo è sbagliato.

Osserviamo queste cose usando il codice. Faremo questo:

$profile = new Profile();
$profile->deactivateProfile($setting);

quando otteniamo la stessa cosa da questo:

$setting->isActive = false; // Deactivate profile

Quindi ovviamente sembra una perdita di tempo. È quando lo fai in questo modo. Questo non è il miglior uso di Iniezione di Dipendenza. Non è nemmeno il miglior uso di una classe.

Ora cosa succede se invece avessimo questo:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

E ora application è libero di attivare e disattivare profile senza dover sapere nulla in particolare del setting che sta effettivamente cambiando. Perché è buono? Nel caso in cui è necessario modificare le impostazioni. Il application viene rimosso da quelle modifiche, quindi sei libero di impazzire in uno spazio sicuro e senza dover guardare che tutto si rompe non appena tocchi qualcosa.

Questo segue il principio costruzione separata dal comportamento . Il pattern DI qui è semplice. Costruisci tutto ciò di cui hai bisogno al livello più basso che puoi, collegale insieme, quindi inizia il comportamento facendo il ticchettio con una chiamata.

Il risultato è che hai un luogo separato per decidere cosa si collega a cosa e in un posto diverso per gestire ciò che dice cosa fare.

Provalo su qualcosa che devi mantenere nel tempo e vedi se non aiuta.

    
risposta data 13.11.2018 - 22:06
fonte
6

Come cliente, quando assumi un meccanico per fare qualcosa alla tua macchina, ti aspetti che il meccanico costruisca una macchina da zero solo per poi lavorare con essa? No, tu dai al meccanico la macchina su cui vuoi che funzioni .

Come proprietario del garage, quando chiedi a un meccanico di fare qualcosa per un'auto, ti aspetti che il meccanico crei il suo cacciavite / chiave / parti di automobili? No, tu fornisci al meccanico le parti / gli strumenti che deve utilizzare

Perché lo facciamo? Ci pensiamo. Sei un proprietario di un garage che vuole assumere qualcuno per diventare il tuo meccanico. Insegnerai loro a fare il meccanico (= scrivi il codice).

Che cosa sarà più semplice:

  • Insegna a un meccanico come attaccare uno spoiler a un'auto usando un cacciavite.
  • Insegna a un meccanico per creare un'auto, creare uno spoiler, creare un cacciavite quindi collega lo spoiler appena creato alla nuova auto creata con il cacciavite appena creato.

Ci sono enormi vantaggi nel non avere la tua meccanica che crei tutto da zero:

  • Ovviamente, la formazione (= sviluppo) è drasticamente ridotta se fornisci al tuo meccanico strumenti e parti esistenti.
  • Se la stessa meccanica deve eseguire lo stesso lavoro più volte, puoi assicurarti che riutilizzi il cacciavite invece di gettare sempre quello vecchio e crearne uno nuovo.
  • Inoltre, un meccanico che ha imparato a creare tutto dovrà essere molto più di un esperto, e quindi si aspetterà un salario più alto. L'analogia della codifica qui è che una classe con molte responsabilità è molto più difficile da gestire rispetto a una classe con una sola responsabilità strettamente definita.
  • Inoltre, quando nuove invenzioni colpiscono il mercato e gli spoiler vengono ora realizzati in carbonio anziché in plastica; dovrai riqualificare (= redevelop) il tuo meccanico esperto. Ma la tua meccanica "semplice" non dovrà essere riqualificata fino a quando lo spoiler potrà essere ancora attaccato allo stesso modo.
  • Avere un meccanico che non fa affidamento su un'auto che hanno costruito da solo significa avere un meccanico in grado di gestire qualsiasi auto che potrebbero ricevere. Comprese le auto che non esistevano ancora al momento dell'allenamento del meccanico. Tuttavia, il tuo meccanico esperto non sarà in grado di costruire auto nuove che sono state create dopo il loro addestramento.

Se assumi e istruisci il meccanico esperto, finirai con un dipendente che costa di più, impiega più tempo a svolgere quello che dovrebbe essere un lavoro semplice, e avrà sempre bisogno di essere riqualificato ogni volta che uno di loro molte responsabilità devono essere aggiornate.

L'analogia dello sviluppo è che se usi classi con dipendenze hardcoded, allora finirai con classi difficili da mantenere che avranno bisogno di continue modifiche / cambiamenti ogni volta che una nuova versione dell'oggetto ( Settings nel tuo caso ) viene creato e dovrai sviluppare la logica interna affinché la classe abbia la possibilità di creare diversi tipi di oggetti Settings .

Inoltre, chiunque consuma la tua classe ora dovrà anche chiedere alla classe di creare l'oggetto Settings corretto, invece di essere semplicemente in grado di passare alla classe qualsiasi oggetto Settings vuole passare. Ciò significa uno sviluppo aggiuntivo per il consumatore per capire come chiedere alla classe di creare lo strumento giusto.

Sì, l'inversione delle dipendenze richiede un po 'più di sforzo per scrivere invece di codificare la dipendenza. Sì, è fastidioso dover digitare altro.

Ma questo è lo stesso argomento della scelta dei valori letterali dell'hardcode perché "dichiarare le variabili richiede più impegno". Tecnicamente corretto, ma i pro superano i contro di diversi ordini di grandezza .

Il beneficio dell'inversione di dipendenza non si verifica quando si crea la prima versione dell'applicazione. Il beneficio dell'inversione di dipendenza si verifica quando è necessario modificare o estendere tale versione iniziale. E non farti ingannare dal pensare che avrai capito bene la prima volta e non avrai bisogno di estendere / cambiare il codice. Dovrai cambiare le cose.

does this affect performance as a whole?

Questo non influisce sulle prestazioni di runtime dell'applicazione. Ma impatta in maniera massiccia i tempi di sviluppo (e quindi le prestazioni) dello sviluppatore .

    
risposta data 13.11.2018 - 10:51
fonte
0

Dovresti usare le tecniche per risolvere i problemi che sono in grado di risolvere quando hai questi problemi. L'inversione di dipendenza e l'iniezione non sono diversi.

L'inversione o l'iniezione di dipendenza è una tecnica che consente al codice di decidere quale implementazione di un metodo viene richiamata in fase di esecuzione. Questo massimizza i benefici del legame tardivo. La tecnica è necessaria quando il linguaggio non supporta la sostituzione del tempo di esecuzione delle funzioni non di istanza. Ad esempio, Java non ha un meccanismo per sostituire le chiamate a un metodo statico con chiamate a un'implementazione diversa; contrasto con Python, dove tutto ciò che è necessario per sostituire la chiamata di funzione è quello di associare il nome a una funzione diversa (riassegnare la variabile che contiene la funzione).

Perché dovremmo voler variare l'implementazione della funzione? Ci sono due ragioni principali:

  • Vogliamo utilizzare i falsi a scopo di test. Questo ci permette di testare una classe che dipende da un recupero del database senza realmente connettersi al database.
  • Dobbiamo supportare più implementazioni. Ad esempio, potremmo aver bisogno di configurare un sistema che supporti sia i database MySQL che PostgreSQL.

Puoi anche prendere nota dell'inversione dei contenitori di controllo. Questa è una tecnica che ha lo scopo di aiutarti a evitare alberi di costruzione enormi e ingarbugliati che assomigliano a questo pseudocodice:

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

Ti consente di registrare le tue classi e quindi ti costruisce la costruzione:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

Nota che è più semplice se le classi registrate possono essere singleton single-less .

Parola di cautela

Nota che l'inversione di dipendenza dovrebbe non essere la tua risposta per la logica di disaccoppiamento. Cerca opportunità di utilizzare Parametrizzazione . Considera questo metodo pseudocodice per esempio:

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

Potremmo usare l'inversione di dipendenza per alcune parti di questo metodo:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

Ma non dovremmo, almeno non completamente. Nota che abbiamo creato una classe stateful con Querier . Ora contiene un riferimento ad alcuni oggetti di connessione essenzialmente globali. Questo crea problemi come la difficoltà a comprendere lo stato generale del programma e il modo in cui le diverse classi si coordinano tra loro. Si noti inoltre che siamo costretti a simulare il querier o la connessione se vogliamo testare la logica della media. Ulteriore approccio migliore sarebbe quello di aumentare la parametrizzazione :

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

E la connessione sarebbe gestita ad un livello ancora più alto che è responsabile per l'operazione nel suo complesso e sa cosa fare con questo output.

Ora possiamo testare la logica della media in modo completamente indipendente dall'interrogazione, e per di più possiamo usarlo in una più ampia varietà di situazioni. Potremmo chiederci se abbiamo anche bisogno degli oggetti MyQuerier e Averager , e forse la risposta è che non lo facciamo se non intendiamo il test unitario StuffDoer , e non il test unitario StuffDoer sarebbe perfettamente ragionevole dal momento che è così strettamente collegato al database. Potrebbe essere più sensato lasciare che i test di integrazione lo coprano. In tal caso, potremmo fare bene fetchAboveMin e averageData in metodi statici.

    
risposta data 13.11.2018 - 16:56
fonte
0

Come tutti i modelli, è molto importante chiedere "perché" per evitare disegni gonfiati.

Per l'iniezione di dipendenza, questo è facilmente visibile pensando ai due aspetti, forse più importanti, del design OOP ...

Accoppiamento basso

Accoppiamento nella programmazione del computer :

In software engineering, coupling is the degree of interdependence between software modules; a measure of how closely connected two routines or modules are; the strength of the relationships between modules.

Vuoi ottenere un accoppiamento basso . Due cose che sono strongmente accoppiate significa che se cambi quella, è molto probabile che cambi l'altra. Bug o restrizioni in uno probabilmente inducono bug / restrizioni nell'altro; e così via.

Una classe che istanzia gli oggetti degli altri è un accoppiamento molto strong, perché l'uno ha bisogno di sapere dell'esistenza dell'altro; ha bisogno di sapere come istanziarlo (quali argomenti il costruttore ha bisogno), e quegli argomenti devono essere disponibili quando si chiama il costruttore. Inoltre, a seconda che la lingua abbia bisogno di una decostruzione esplicita (C ++), ciò introdurrà ulteriori complicazioni. Se introduci nuove classi (ad esempio, NextSettings o qualsiasi altra cosa), devi tornare alla classe originale e aggiungere altre chiamate ai loro costruttori.

Alta coesione

Cohesion :

In computer programming, cohesion refers to the degree to which the elements inside a module belong together.

Questo è l'altro lato della medaglia. Se guardi una unità di codice (un metodo, una classe, un pacchetto, ecc.), Vuoi avere tutto il codice all'interno di quell'unità per avere il minor numero possibile di responsabilità.

Un esempio di base per questo potrebbe essere il pattern MVC: separa chiaramente il modello di dominio dalla vista (GUI) e un livello di controllo che li incolla.

Ciò evita che il codice si gonfia dove si ottengono grandi blocchi che fanno molte cose diverse; se vuoi cambiarne una parte, devi tenere traccia di tutte le altre caratteristiche, cercando di evitare bug, ecc .; e ti programmi rapidamente in un buco in cui è molto difficile uscire.

Con l'iniezione delle dipendenze, delegate la creazione o tenete traccia delle dipendenze da qualsiasi classe (o file di configurazione) che implementa il vostro DI. Le altre classi non si preoccupano molto di cosa sta succedendo esattamente - lavoreranno con alcune interfacce generiche e non avranno idea di quale sia l'effettiva implementazione, il che significa che non sono responsabili per le altre cose.

    
risposta data 15.11.2018 - 08:02
fonte

Leggi altre domande sui tag