Penso che tu abbia perso il punto dietro Iniezione delle dipendenze che è Inversione di dipendenza .
In particolare, il chiamante non conosce la reale implementazione del servizio, questo richiede l'indirezione tramite virtual (in C ++) o function pointers (C).
Ha diversi vantaggi, in particolare con i test in quanto i collaboratori possono essere un doppio test di qualche tipo. Fornisce inoltre una certa flessibilità spostando le decisioni dalla parte che fa il codice alla parte di configurazione del codice che ne sa di più sul comportamento desiderato più ampio.
Nel caso del tuo oggetto astratto, l'implementazione (l'oggetto costruito reale) conosce le implementazioni delle sue collaborazioni, perché le costruisce in modo specifico. Riesce a raggiungere la condivisione logica (tramite la classe base), ma non è possibile scambiare uno di questi due collaboratori per nessuna ragione senza che cambi per tutti, o duplicando la classe derivata. Questo design soffre anche di cambiamenti, poiché le implementazioni influiscono sulla classe base e la classe base ha effetto su tutte le sue implementazioni.
Questo ha davvero bisogno di un esempio. Utilizzerò un programma di compressione: il caso d'uso di base è: legge un file, lo comprime e lo scrive su un altro file.
Dato che la maggior parte dei compilatori C sono anche compilatori C ++, scriverò principalmente C, ma usando alcune caratteristiche del C ++, quindi il sotto è C +.
int read_file(handle h, char* buffer, int length);
int write_file(handle h, char* buffer, int length);
int compress_file(handle from, handle to)
{
handle h = //obtained by opening file
const int N = //some length.
char buffer[N];
int k;
while(0 != (k = read_file(from, buffer, N)))
{
//compression logic here
...
//buffer and k now represent the compressed data.
if (k != write_file(to, buffer, k))
{
return -1; //failed report some error code
}
}
return 0; //some success code, or maybe the compressed length.
}
int main()
{
//open handles
if (0 != compress_file(from, to))
{
//handle error
}
//cleanup
}
Fatto bene.
Oops, i requisiti sono cambiati e ora dobbiamo comprimere un file in una rete. Sfortunatamente dobbiamo usare un'API diversa.
class NetworkPoint
{
void write_byte(char a);
}
Quindi come fai a evitare questo? Un'opzione è spingere la logica per la compressione. È probabilmente la parte più complicata di questo sistema, nessun punto di riscrittura.
int compress_buffer(char* buffer_in, int N, int* n, char* buffer_out, int K, int* k)
{
//logic about compression here
}
compress_file non cambia molto e ora c'è una seconda versione per coprire il nuovo caso d'uso della compressione sulla rete.
int compress_file(handle from, handle to)
{
handle h = //obtained by opening file
const int N = //some length.
char buffer[N];
int k;
while(0 != (k = read_file(from, buffer, N)))
{
compress_buffer(...);
if (k != write_file(to, buffer, k))
{
return -1; //failed report some error code
}
}
return 0; //some success code, or maybe the compressed length.
}
int compress_network(handle from, NetworkPoint& to)
{
handle h = //obtained by opening file
const int N = //some length.
char buffer[N];
int k;
while(0 != (k = read_file(from, buffer, N)))
{
compress_buffer(...);
for (int i = 0; i < k; ++i)
{
to.write(buffer[i]);
}
}
return 0; //some success code, or maybe the compressed length.
}
Questa è la duplicazione del codice.
Se è necessario un altro end-point, questo aggiungerà un altro duplicato. Se la logica aziendale fondamentale cambia, ci sono due (o più) posti che devono essere aggiornati. Chiaramente questo va bene in alcuni casi, ma in altri questo andrà rapidamente fuori controllo.
Quindi, di cosa ha realmente bisogno la logica di business "compressa"? Ha bisogno di "ottenere dati", preferibilmente in un buffer, quindi deve fare un po 'di "compressione", quindi deve "inviare dati" che è stato appena compresso. Risulta che ogni "" è un servizio.
class Reader
{
virtual int read(char* buffer, int N)=0;
}
class Writer
{
virtual int write(char* buffer, int N)=0;
}
class Compressor
{
virtual int compress(char* buffer_in, int N, int* n, char* buffer_out, int K, int* k)=0;
}
Per essere chiari, un'interfaccia non dovrebbe mai avere funzioni implementate, né dovrebbe avere alcun membro di dati.
Se trovi che alcune strutture ripetitive che implementano una classe astratta per alcune delle implementazioni del servizio potrebbero funzionare, basta non inquinare la definizione dell'interfaccia stessa.
Ciò consente di compartimentare tale comportamento.
class FileWriter : public Writer
{
int handle;
...implement to write to a file handle...
}
class NetworkWriter : public Writer
{
NetworkPoint point;
...implement to write to a NetworkPoint...
}
E ora la dipendenza diventa invertibile.
int compress_data(Reader& from, Compressor& compress, Writer& to)
{
handle h = //obtained by opening file
const int N = //some length.
char buffer[N];
int k;
while(0 != (k = from.read(from, buffer, N)))
{
compress.compress(...);
if (k != to.write(to, buffer, k))
{
return -1; //failed report some error code
}
}
return 0; //some success code, or maybe the compressed length.
}
Se abbiamo bisogno di un nuovo tipo di lettore, scrittore o algoritmo di compressione, questa logica di business non interessa. Basta implementare il servizio e inoltrarlo.
Ora abbiamo già in corso l'iniezione di dipendenza. Questo è ciò che sono quei parametri di funzione. Il chiamante "inietta" le dipendenze in questa funzione. La funzione non ha il controllo diretto di tali servizi, ma può interagire con essi.
Ora consente di presumere che questa logica aziendale sia di per sé un servizio configurabile, diciamo che dovrebbe essere eseguita da un task manager.
class Task
{
virtual void execute()=0;
}
class CompressionTask
{
std::shared_ptr<Reader> reader;
std::shared_ptr<Compressor> compressor;
std::shared_ptr<Writer> writer;
CompressionTask(std::shared_ptr<Reader> reader, std::shared_ptr<Compressor> compressor, std::shared_ptr<Writer> writer)
: reader(reader), compressor(compressor), writer(writer)
{}
void execute()
{
compress_data(*reader, *compressor, *writer);
}
}
CompressionTask accetta diverse dipendenze iniettate, non gliene importa troppo, ma richiede alcune garanzie di durata (shared_ptr). Queste dipendenze sono nascoste dal TaskManager e, a parte la loro funzione, l'implementazione è nascosta alla logica aziendale.
Qualcuno da qualche parte dovrà creare l'oggetto CompressionTask e dovrà conoscere abbastanza le dipendenze per crearle, richiederle o passarle. Questo pezzo di codice dovrebbe tuttavia avere tutte le informazioni disponibili per prendere tali decisioni. Il resto della base di codice può ignorarlo, trattandolo come un collaboratore generico o una fonte di dipendenze.
Invertire le dipendenze sopra ottenute:
- Fonti di dati comprimibili multiple
- Sink di dati comprimibili multipli
- Algoritmi di compressione multipli
- Deduplicazione della logica aziendale
- Nascondere le decisioni di configurazione dalla più grande base di codice
- Spostare le decisioni di configurazione in un unico posto erano le conoscenze necessarie a rendere questa decisione
- Resistenza ridotta alle modifiche dei requisiti