Utilizzo delle interfacce per il codice debolmente accoppiato


10

sfondo

Ho un progetto che dipende dall'uso di un certo tipo di dispositivo hardware, mentre non importa chi produce quel dispositivo hardware fintanto che fa ciò di cui ho bisogno. Detto questo, anche due dispositivi che dovrebbero fare la stessa cosa avranno delle differenze quando non sono realizzati dallo stesso produttore. Quindi sto pensando di utilizzare un'interfaccia per disaccoppiare l'applicazione dalla particolare marca / modello del dispositivo coinvolto, e invece l'interfaccia ha solo la funzionalità di massimo livello. Ecco come sto pensando che la mia architettura sarà simile:

  1. Definire un'interfaccia in un progetto C # IDevice.
  2. Avere un concreto in una libreria definita in un altro progetto C #, che verrà utilizzato per rappresentare il dispositivo.
  3. Chiedi al dispositivo concreto di implementare l' IDeviceinterfaccia.
  4. L' IDeviceinterfaccia potrebbe avere metodi come GetMeasuremento SetRange.
  5. Fai in modo che l'applicazione abbia conoscenza del calcestruzzo e passa il calcestruzzo al codice dell'applicazione che utilizza ( non implementa ) il IDevicedispositivo.

Sono abbastanza sicuro che questo sia il modo giusto per farlo, perché poi sarò in grado di cambiare quale dispositivo viene utilizzato senza influire sull'applicazione (che sembra accadere a volte). In altre parole, non importa come le implementazioni GetMeasuremento SetRangeeffettivamente funzionano attraverso il calcestruzzo (poiché potrebbero differire tra i produttori del dispositivo).

L'unico dubbio nella mia mente è che ora sia l'applicazione, sia la classe concreta del dispositivo dipendono entrambe dalla libreria che contiene l' IDeviceinterfaccia. Ma è una brutta cosa?

Inoltre non vedo come l'applicazione non avrà bisogno di conoscere il dispositivo, a meno che il dispositivo e non si IDevicetrovino nello stesso spazio dei nomi.

Domanda

Questo sembra l'approccio giusto per implementare un'interfaccia per disaccoppiare la dipendenza tra la mia applicazione e il dispositivo che utilizza?


Questo è assolutamente il modo corretto di farlo. Stai essenzialmente programmando un driver di dispositivo, ed è esattamente come i driver di dispositivo sono tradizionalmente scritti, con una buona ragione. Non è possibile aggirare il fatto che per utilizzare le funzionalità di un dispositivo, è necessario fare affidamento sul codice che conosce queste funzionalità almeno in modo astratto.
Kilian Foth,

@KilianFoth Sì, ho avuto la sensazione, ho dimenticato di aggiungere una parte alla mia domanda. Vedi # 5.
Snoop,

Risposte:


5

Penso che tu abbia una buona comprensione di come funziona il software disaccoppiato :)

L'unico dubbio nella mia mente è che ora sia l'applicazione, sia la classe concreta del dispositivo dipendono entrambe dalla libreria che contiene l'interfaccia IDevice. Ma è una brutta cosa?

Non deve essere!

Inoltre non vedo come l'applicazione non avrà bisogno di conoscere il dispositivo, a meno che il dispositivo e IDevice siano nello stesso spazio dei nomi.

Puoi affrontare tutte queste preoccupazioni con la struttura del progetto .

Il modo in cui lo faccio in genere:

  • Metti tutte le mie cose astratte in un Commonprogetto. Qualcosa del genere MyBiz.Project.Common. Altri progetti sono liberi di fare riferimento a esso, ma potrebbe non fare riferimento ad altri progetti.
  • Quando creo un'implementazione concreta di un'astrazione, la inserisco in un progetto separato. Qualcosa del genere MyBiz.Project.Devices.TemperatureSensors. Questo progetto farà riferimento al Commonprogetto.
  • Ho quindi il mio Clientprogetto che è il punto di accesso alla mia applicazione (qualcosa del genere MyBiz.Project.Desktop). All'avvio, l'applicazione passa attraverso un processo di Bootstrap in cui configuro i mapping di astrazione / implementazione concreta. Posso istanziare il mio calcestruzzo IDevicescome WaterTemperatureSensore IRCameraTemperatureSensorqui, oppure posso configurare servizi come Factory o un contenitore IoC per istanziare i tipi di calcestruzzo giusti per me in seguito.

La cosa chiave qui è che solo il tuo Clientprogetto deve essere consapevole sia del Commonprogetto astratto che di tutti i progetti di implementazione concreti. Limitando la mappatura astratta-> concreta al tuo codice Bootstrap stai rendendo possibile che il resto della tua applicazione sia felicemente inconsapevole dei tipi concreti.

Codice debolmente accoppiato ftw :)


2
DI segue naturalmente da qui. Anche questo è in gran parte un problema di struttura del progetto / codice. Avere un contenitore IoC può aiutare con DI ma non è un prerequisito. Puoi ottenere DI anche iniettando manualmente le dipendenze!
MetaFight,

1
@StevieV Sì, se l'oggetto Foo all'interno dell'applicazione richiede un IDevice, l'inversione delle dipendenze può essere semplice come iniettarla tramite un costruttore ( new Foo(new DeviceA());), piuttosto che avere un campo privato all'interno di Foo istanziato DeviceA stesso ( private IDevice idevice = new DeviceA();) - ottieni comunque DI, come Foo non è a conoscenza di DeviceA nel primo caso
un'altra volta

2
@anotherdave i tuoi input e MetaFight sono stati molto utili.
Snoop,

1
@StevieV Quando hai tempo, ecco una bella introduzione a Dependency Inversion come concetto (da "Zio Bob" Martin, il ragazzo che l'ha coniato), distinto da un framework di Inversion of Control / Depency Injection, come Spring (o qualche altro esempio popolare nel mondo C♯ :))
AnotherDave

1
@anotherdave Sicuramente sto programmando di verificarlo, grazie ancora.
Snoop,

3

Sì, sembra l'approccio giusto. No, non è male per l'applicazione e la libreria di dispositivi dipendere dall'interfaccia, soprattutto se si ha il controllo dell'implementazione.

Se si teme che i dispositivi potrebbero non implementare sempre l'interfaccia, per qualsiasi motivo, è possibile utilizzare il modello di adattatore e adattare l'implementazione concreta dei dispositivi all'interfaccia.

MODIFICARE

Nell'affrontare la tua quinta preoccupazione, pensa a una struttura come questa (suppongo che tu abbia il controllo della definizione dei tuoi dispositivi):

Hai una libreria principale. In esso è un'interfaccia chiamata IDevice.

Nella tua libreria di dispositivi, hai un riferimento alla tua libreria principale e hai definito una serie di dispositivi che implementano tutti IDevice. Hai anche una fabbrica che sa come creare diversi tipi di IDevices.

Nella tua applicazione includi riferimenti alla libreria principale e alla libreria dei dispositivi. L'applicazione ora utilizza la factory per creare istanze di oggetti conformi all'interfaccia IDevice.

Questo è uno dei molti modi possibili per rispondere alle tue preoccupazioni.

ESEMPI:

namespace Core
{
    public interface IDevice { }
}


namespace Devices
{
    using Core;

    class DeviceOne : IDevice { }

    class DeviceTwo : IDevice { }

    public class Factory
    {
        public IDevice CreateDeviceOne()
        {
            return new DeviceOne();
        }

        public IDevice CreateDeviceTwo()
        {
            return new DeviceTwo();
        }
    }
}

// do not implement IDevice
namespace ThirdrdPartyDevices
{

    public class ThirdPartyDeviceOne  { }

    public class ThirdPartyDeviceTwo  { }

}

namespace DeviceAdapters
{
    using Core;
    using ThirdPartyDevices;

    class ThirdPartyDeviceAdapterOne : IDevice
    {
        private ThirdPartyDeviceOne _deviceOne;

        // use the third party device to adapt to the interface
    }

    class ThirdPartyDeviceAdapterTwo : IDevice
    {
        private ThirdPartyDeviceTwo _deviceTwo;

        // use the third party device to adapt to the interface
    }

    public class AdapterFactory
    {
        public IDevice CreateThirdPartyDeviceAdapterOne()
        {
            return new ThirdPartyDeviceAdapterOne();
        }

        public IDevice CreateThirdPartyDeviceAdapterTwo()
        {
            return new ThirdPartyDeviceAdapterTwo();
        }
    }
}

namespace Application
{
    using Core;
    using Devices;
    using DeviceAdapters;

    class App
    {
        void RunInHouse()
        {
            var factory = new Factory();
            var devices = new List<IDevice>() { factory.CreateDeviceOne(), factory.CreateDeviceTwo() };
            foreach (var device in devices)
            {
                // call IDevice  methods.
            }
        }

        void RunThirdParty()
        {
            var factory = new AdapterFactory();
            var devices = new List<IDevice>() { factory.CreateThirdPartyDeviceAdapterOne(), factory.CreateThirdPartyDeviceAdapterTwo() };
            foreach (var device in devices)
            {
                // call IDevice  methods.
            }
        }
    }
}

Quindi, con questa implementazione, dove va il calcestruzzo?
Snoop,

Posso solo parlare per quello che sceglierei di fare. Se hai il controllo dei dispositivi, inseriresti comunque i dispositivi concreti nella loro libreria. Se non hai il controllo dei dispositivi, creeresti un'altra libreria per gli adattatori.
Prezzo Jones,

Immagino che l'unica cosa sia, come farà l'applicazione ad accedere al calcestruzzo specifico di cui ho bisogno?
Snoop,

Una soluzione sarebbe quella di utilizzare il modello astratto di fabbrica per creare IDevices.
Prezzo Jones,

1

L'unico dubbio nella mia mente è che ora sia l'applicazione, sia la classe concreta del dispositivo dipendono entrambe dalla libreria che contiene l'interfaccia IDevice. Ma è una brutta cosa?

Devi pensare al contrario. Se non vai in questo modo, l'applicazione dovrebbe conoscere tutti i diversi dispositivi / implementazioni. Quello che dovresti tenere a mente è che cambiare quell'interfaccia potrebbe interrompere la tua applicazione se è già in circolazione, quindi dovresti progettare attentamente quell'interfaccia.

Questo sembra l'approccio giusto per implementare un'interfaccia per disaccoppiare la dipendenza tra la mia applicazione e il dispositivo che utilizza?

Semplicemente si

come il calcestruzzo arriva effettivamente all'app

L'applicazione può caricare l'assemblaggio di calcestruzzo (dll) in fase di esecuzione. Vedi: /programming/465488/can-i-load-a-net-assembly-at-runtime-and-instantiate-a-type-knowing-only-the-na

Se è necessario scaricare / caricare in modo dinamico tale "driver", è necessario caricare l'assembly in un AppDomain separato .


Grazie, vorrei davvero sapere di più sulle interfacce quando ho iniziato a scrivere codice.
Snoop,

Siamo spiacenti, ho dimenticato di aggiungere da una parte (come il calcestruzzo effettivamente arriva all'app). Puoi affrontarlo? Vedi # 5.
Snoop,

Realizzi davvero le tue app in questo modo, caricando le DLL in fase di esecuzione?
Snoop,

Se ad esempio la classe concreta non è conosciuta da me, allora sì. Supponiamo che un fornitore di dispositivi decida di utilizzare la tua IDeviceinterfaccia in modo che il suo dispositivo possa essere utilizzato con la tua app, quindi non ci sarebbe un altro modo IMO.
Heslacher,
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.