Verwenden von Schnittstellen für lose gekoppelten Code

10

Hintergrund

Ich habe ein Projekt, das von der Verwendung eines bestimmten Hardwaregerätetyps abhängt, während es nicht wirklich wichtig ist, wer dieses Hardwaregerät herstellt, solange es das tut, wofür ich es brauche. Abgesehen davon weisen sogar zwei Geräte, die dasselbe tun sollen, Unterschiede auf, wenn sie nicht vom selben Hersteller hergestellt werden. Daher denke ich daran, eine Schnittstelle zu verwenden, um die Anwendung von der jeweiligen Marke / dem jeweiligen Gerätemodell zu entkoppeln , und stattdessen die Schnittstelle nur die Funktionalität auf höchster Ebene abzudecken. Ich denke, meine Architektur wird folgendermaßen aussehen:

  1. Definieren Sie eine Schnittstelle in einem C # -Projekt IDevice.
  2. Lassen Sie einen Beton in einer Bibliothek in einem anderen C # -Projekt definieren, der zur Darstellung des Geräts verwendet wird.
  3. Lassen Sie das konkrete Gerät die IDeviceSchnittstelle implementieren .
  4. Die IDeviceSchnittstelle kann Methoden wie GetMeasurementoder haben SetRange.
  5. Stellen Sie sicher, dass die Anwendung Kenntnisse über den Beton hat, und übergeben Sie den Beton an den Anwendungscode, der das Gerät verwendet ( nicht implementiert ) IDevice.

Ich bin mir ziemlich sicher, dass dies der richtige Weg ist, denn dann kann ich ändern, welches Gerät verwendet wird, ohne die Anwendung zu beeinträchtigen (was manchmal zu passieren scheint). Mit anderen Worten, es spielt keine Rolle, wie die Implementierungen des Betons funktionieren GetMeasurementoder SetRangetatsächlich funktionieren (wie es bei den Herstellern des Geräts unterschiedlich sein kann).

Der einzige Zweifel in meinem Kopf ist, dass jetzt sowohl die Anwendung als auch die konkrete Klasse des Geräts von der Bibliothek abhängen, die die IDeviceSchnittstelle enthält . Aber ist das eine schlechte Sache?

Ich sehe auch nicht, wie die Anwendung nichts über das Gerät wissen muss, es sei denn, das Gerät IDevicebefindet sich im selben Namespace.

Frage

Scheint dies der richtige Ansatz für die Implementierung einer Schnittstelle zu sein, um die Abhängigkeit zwischen meiner Anwendung und dem verwendeten Gerät zu entkoppeln?

Schnüffeln
quelle
Dies ist absolut der richtige Weg. Sie programmieren im Wesentlichen einen Gerätetreiber, und genau so werden Gerätetreiber aus gutem Grund traditionell geschrieben. Sie können die Tatsache nicht umgehen, dass Sie sich auf Code verlassen müssen, der diese Funktionen zumindest abstrakt kennt, um die Funktionen eines Geräts nutzen zu können.
Kilian Foth
@KilianFoth Ja, ich hatte das Gefühl, vergessen zu meiner Frage einen Teil hinzuzufügen. Siehe # 5.
Snoop

Antworten:

5

Ich denke, Sie haben ein ziemlich gutes Verständnis dafür, wie entkoppelte Software funktioniert :)

Der einzige Zweifel in meinem Kopf ist, dass jetzt sowohl die Anwendung als auch die konkrete Klasse des Geräts von der Bibliothek abhängen, die die IDevice-Schnittstelle enthält. Aber ist das eine schlechte Sache?

Es muss nicht sein!

Ich sehe auch nicht, wie die Anwendung nichts über das Gerät wissen muss, es sei denn, das Gerät und das IDevice befinden sich im selben Namespace.

Mit all diesen Problemen können Sie sich mit der Projektstruktur befassen .

So mache ich es normalerweise:

  • Setzen Sie alle meine abstrakten Sachen in ein CommonProjekt ein. So etwas wie MyBiz.Project.Common. Andere Projekte können darauf verweisen, aber möglicherweise nicht auf andere Projekte.
  • Wenn ich eine konkrete Implementierung einer Abstraktion erstelle, füge ich sie in ein separates Projekt ein. So etwas wie MyBiz.Project.Devices.TemperatureSensors. Dieses Projekt verweist auf das CommonProjekt.
  • Ich habe dann mein ClientProjekt, das der Einstiegspunkt für meine Anwendung ist (so etwas wie MyBiz.Project.Desktop). Beim Start durchläuft die Anwendung einen Bootstrapping- Prozess, bei dem ich Abstraktions- / konkrete Implementierungszuordnungen konfiguriere. Ich kann meinen Beton IDeviceswie WaterTemperatureSensorund IRCameraTemperatureSensorhier instanziieren oder Dienste wie Fabriken oder einen IoC-Container konfigurieren, um später die richtigen Betontypen für mich zu instanziieren.

Das Wichtigste dabei ist, dass nur Ihr ClientProjekt sowohl das abstrakte CommonProjekt als auch alle konkreten Implementierungsprojekte kennen muss. Indem Sie die abstrakte-> konkrete Zuordnung auf Ihren Bootstrap-Code beschränken, können Sie dafür sorgen, dass der Rest Ihrer Anwendung die konkreten Typen glücklicherweise nicht kennt.

Locker gekoppelter Code ftw :)

MetaFight
quelle
2
DI folgt natürlich von hier aus. Dies ist größtenteils auch ein Problem der Projekt- / Codestruktur. Ein IoC-Container kann bei DI hilfreich sein, ist jedoch keine Voraussetzung. Sie können DI erreichen, indem Sie Abhängigkeiten auch manuell einfügen!
MetaFight
1
@StevieV Ja, wenn für das Foo-Objekt in Ihrer Anwendung ein IDevice erforderlich ist, kann die Abhängigkeitsinversion so einfach sein wie das Einfügen über einen Konstruktor ( new Foo(new DeviceA());), anstatt ein privates Feld in Foo zu haben, um DeviceA selbst zu instanziieren ( private IDevice idevice = new DeviceA();) - Sie erreichen dennoch DI, as Foo ist DeviceA im ersten Fall nicht bekannt
anotherdave
2
@anotherdave Ihre und MetaFight Eingabe war sehr hilfreich.
Snoop
1
@StevieV Wenn Sie Zeit haben, finden Sie hier eine nette Einführung in Dependency Inversion als Konzept (von "Onkel Bob" Martin, dem Typ, der es geprägt hat), der sich von einem Inversion of Control / Depency Injection-Framework wie Spring (oder einem anderen) unterscheidet beliebtes Beispiel in der C♯-Welt :))
anotherdave
1
@anotherdave Ich habe definitiv vor, das zu überprüfen, nochmals vielen Dank.
Snoop
3

Ja, das scheint der richtige Ansatz zu sein. Nein, es ist keine schlechte Sache für die Anwendung und die Gerätebibliothek, von der Schnittstelle abhängig zu sein, insbesondere wenn Sie die Kontrolle über die Implementierung haben.

Wenn Sie befürchten, dass die Geräte die Schnittstelle aus irgendeinem Grund nicht immer implementieren, können Sie das Adaptermuster verwenden und die konkrete Implementierung der Geräte an die Schnittstelle anpassen.

BEARBEITEN

Stellen Sie sich bei der Beantwortung Ihres fünften Problems eine Struktur wie diese vor (ich gehe davon aus, dass Sie die Kontrolle über die Definition Ihrer Geräte haben):

Sie haben eine Kernbibliothek. Darin befindet sich eine Schnittstelle namens IDevice.

In Ihrer Gerätebibliothek haben Sie einen Verweis auf Ihre Kernbibliothek und Sie haben eine Reihe von Geräten definiert, die alle IDevice implementieren. Sie haben auch eine Fabrik, die weiß, wie man verschiedene Arten von IDevices erstellt.

In Ihrer Anwendung enthalten Sie Verweise auf die Kernbibliothek und die Gerätebibliothek. Ihre Anwendung verwendet jetzt die Factory, um Instanzen von Objekten zu erstellen, die der IDevice-Schnittstelle entsprechen.

Dies ist eine von vielen Möglichkeiten, um Ihre Bedenken auszuräumen.

BEISPIELE:

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.
            }
        }
    }
}
Preis Jones
quelle
Wohin geht bei dieser Implementierung der Beton?
Snoop
Ich kann nur für das sprechen, was ich tun möchte. Wenn Sie die Kontrolle über die Geräte haben, würden Sie die konkreten Geräte dennoch in ihre eigene Bibliothek stellen. Wenn Sie nicht die Kontrolle über die Geräte haben, erstellen Sie eine weitere Bibliothek für die Adapter.
Price Jones
Ich denke, das Einzige ist, wie erhält die Anwendung Zugriff auf den spezifischen Beton, den ich benötige?
Snoop
Eine Lösung wäre, das abstrakte Factory-Muster zum Erstellen von IDevices zu verwenden.
Preis Jones
1

Der einzige Zweifel in meinem Kopf ist, dass jetzt sowohl die Anwendung als auch die konkrete Klasse des Geräts von der Bibliothek abhängen, die die IDevice-Schnittstelle enthält. Aber ist das eine schlechte Sache?

Sie müssen umgekehrt denken. Wenn Sie diesen Weg nicht gehen, muss die Anwendung alle verschiedenen Geräte / Implementierungen kennen. Was Sie beachten sollten, ist, dass das Ändern dieser Schnittstelle Ihre Anwendung beschädigen kann, wenn sie bereits in freier Wildbahn ist. Daher sollten Sie diese Schnittstelle sorgfältig entwerfen.

Scheint dies der richtige Ansatz für die Implementierung einer Schnittstelle zu sein, um die Abhängigkeit zwischen meiner Anwendung und dem verwendeten Gerät zu entkoppeln?

Einfach ja

wie der Beton tatsächlich zur App kommt

Die Anwendung kann die Betonbaugruppe (DLL) zur Laufzeit laden. Siehe: /programming/465488/can-i-load-a-net-assembly-at-runtime-and-instantiate-a-type-knowing-only-the-na

Wenn Sie einen solchen "Treiber" dynamisch entladen / laden müssen, müssen Sie die Assembly in eine separate AppDomain laden .

Heslacher
quelle
Vielen Dank, ich wünschte wirklich, ich wüsste mehr über Schnittstellen, als ich anfing zu programmieren.
Snoop
Entschuldigung, ich habe vergessen, einen Teil hinzuzufügen (wie der Beton tatsächlich zur App gelangt). Können Sie das ansprechen? Siehe # 5.
Snoop
Machen Sie Ihre Apps tatsächlich so und laden Sie die DLLs zur Laufzeit?
Snoop
Wenn mir zB die konkrete Klasse nicht bekannt ist, dann ja. Angenommen, ein Gerätehersteller entscheidet sich für die Verwendung Ihrer IDeviceBenutzeroberfläche, damit sein Gerät mit Ihrer App verwendet werden kann. Dann gibt es keinen anderen Weg, IMO.
Heslacher