Utilisation d'interfaces pour du code à couplage lâche

10

Contexte

J'ai un projet qui dépend de l'utilisation d'un certain type de périphérique matériel, alors peu importe qui fabrique ce périphérique tant qu'il fait ce dont j'ai besoin. Cela étant dit, même deux appareils qui sont censés faire la même chose auront des différences lorsqu'ils ne sont pas fabriqués par le même fabricant. Je pense donc à utiliser une interface pour dissocier l'application de la marque / du modèle particulier du périphérique concerné, et à la place, l'interface ne couvre que les fonctionnalités de plus haut niveau. Voici à quoi je pense que mon architecture ressemblera:

  1. Définissez une interface dans un projet C # IDevice.
  2. Avoir un concret dans une bibliothèque définie dans un autre projet C #, qui sera utilisé pour représenter le périphérique.
  3. Demandez au dispositif concret de mettre en œuvre l' IDeviceinterface.
  4. L' IDeviceinterface peut avoir des méthodes comme GetMeasurementou SetRange.
  5. Faites en sorte que l'application connaisse le béton et passez le béton au code d'application qui utilise (et non implémente ) le IDevicepériphérique.

Je suis à peu près sûr que c'est la bonne façon de procéder, car je pourrai alors changer le périphérique utilisé sans affecter l'application (ce qui semble parfois se produire). En d'autres termes, peu importe la façon dont les implémentations GetMeasurementou SetRangefonctionnent réellement à travers le béton (comme cela peut différer selon les fabricants de l'appareil).

Le seul doute dans mon esprit, c'est que maintenant l'application et la classe concrète du périphérique dépendent toutes deux de la bibliothèque qui contient l' IDeviceinterface. Mais est-ce une mauvaise chose?

Je ne vois pas non plus comment l'application n'aura pas besoin de connaître le périphérique, sauf si le périphérique et se IDevicetrouvent dans le même espace de noms.

Question

Cela semble-t-il être la bonne approche pour implémenter une interface pour découpler la dépendance entre mon application et le périphérique qu'elle utilise?

Fouiner
la source
C'est absolument la bonne façon de procéder. Vous programmez essentiellement un pilote de périphérique, et c'est exactement comme cela que les pilotes de périphérique sont traditionnellement écrits, pour une bonne raison. Vous ne pouvez pas contourner le fait que pour utiliser les capacités d'un appareil, vous devez vous fier à du code qui connaît ces capacités au moins de manière abstraite.
Kilian Foth,
@KilianFoth Oui, j'ai eu un sentiment, j'ai oublié d'ajouter une partie à ma question. Voir # 5.
Snoop

Réponses:

5

Je pense que vous avez une assez bonne compréhension du fonctionnement des logiciels découplés :)

Le seul doute dans mon esprit, c'est que maintenant l'application et la classe concrète du périphérique dépendent toutes les deux de la bibliothèque qui contient l'interface IDevice. Mais est-ce une mauvaise chose?

Ce n'est pas nécessaire!

Je ne vois pas non plus comment l'application n'aura pas besoin de connaître le périphérique, sauf si le périphérique et IDevice sont dans le même espace de noms.

Vous pouvez traiter toutes ces préoccupations avec la structure du projet .

La façon dont je le fais généralement:

  • Mettez tous mes trucs abstraits dans un Commonprojet. Quelque chose comme MyBiz.Project.Common. D'autres projets sont libres de le référencer, mais il peut ne pas faire référence à d'autres projets.
  • Lorsque je crée une implémentation concrète d'une abstraction, je la mets dans un projet distinct. Quelque chose comme MyBiz.Project.Devices.TemperatureSensors. Ce projet fera référence au Commonprojet.
  • J'ai ensuite mon Clientprojet qui est le point d'entrée de mon application (quelque chose comme MyBiz.Project.Desktop). Au démarrage, l'application passe par un processus d' amorçage où je configure les mappages d'abstraction / mise en œuvre concrète. Je peux instancier mon béton IDevicescomme WaterTemperatureSensoret IRCameraTemperatureSensorici, ou je peux configurer des services comme des usines ou un conteneur IoC pour instancier les bons types de béton pour moi plus tard.

L'essentiel ici est que seul votre Clientprojet doit être conscient à la fois du Commonprojet abstrait et de tous les projets concrets de mise en œuvre. En confinant le mappage abstrait-> concret à votre code Bootstrap, vous permettez au reste de votre application d'ignorer parfaitement les types concrets.

Code ftw faiblement couplé :)

MetaFight
la source
2
DI suit naturellement d'ici. C'est également une préoccupation de structure de projet / code. Avoir un conteneur IoC peut aider avec DI, mais ce n'est pas une condition préalable. Vous pouvez également réaliser DI en injectant manuellement les dépendances!
MetaFight
1
@StevieV Oui, si l'objet Foo dans votre application nécessite un IDevice, l'inversion de dépendance peut être aussi simple que de l'injecter via un constructeur ( new Foo(new DeviceA());), plutôt que d'avoir un champ privé dans Foo instancier DeviceA lui-même ( private IDevice idevice = new DeviceA();) - vous obtenez toujours DI, comme Foo n'est pas au courant de DeviceA dans le premier cas
anotherdave
2
@anotherdave le vôtre et la contribution de MetaFight a été très utile.
Snoop
1
@StevieV Lorsque vous avez le temps, voici une belle introduction à Dependency Inversion en tant que concept (de "Uncle Bob" Martin, le gars qui l'a inventé), distincte d'un cadre d'inversion de contrôle / injection de dépendance, comme Spring (ou un autre plus) exemple populaire dans le monde C♯ :))
anotherdave
1
@anotherdave Prévoyant certainement de vérifier cela, merci encore.
Snoop
3

Oui, cela semble être la bonne approche. Non, ce n'est pas une mauvaise chose pour l'application et la bibliothèque de périphériques de dépendre de l'interface, surtout si vous contrôlez l'implémentation.

Si vous craignez que les périphériques n'implémentent pas toujours l'interface, pour une raison quelconque, vous pouvez utiliser le modèle d'adaptateur et adapter l'implémentation concrète des périphériques à l'interface.

ÉDITER

En répondant à votre cinquième préoccupation, pensez à une structure comme celle-ci (je suppose que vous contrôlez la définition de vos appareils):

Vous avez une bibliothèque principale. Il contient une interface appelée IDevice.

Dans votre bibliothèque de périphériques, vous avez une référence à votre bibliothèque principale et vous avez défini une série de périphériques qui implémentent tous IDevice. Vous disposez également d'une usine qui sait créer différents types d'IDevices.

Dans votre application, vous incluez des références à la bibliothèque principale et à la bibliothèque de périphériques. Votre application utilise désormais la fabrique pour créer des instances d'objets conformes à l'interface IDevice.

C'est l'une des nombreuses façons possibles de répondre à vos préoccupations.

EXEMPLES:

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.
            }
        }
    }
}
Price Jones
la source
Alors avec cette implémentation, où va le béton?
Snoop
Je ne peux que parler de ce que je choisirais de faire. Si vous contrôlez les périphériques, vous devez toujours placer les périphériques concrets dans leur propre bibliothèque. Si vous ne contrôlez pas les périphériques, vous devez créer une autre bibliothèque pour les adaptateurs.
Price Jones
Je suppose que la seule chose est de savoir comment l'application pourra-t-elle accéder au béton spécifique dont j'ai besoin?
Snoop
Une solution serait d'utiliser le modèle d'usine abstrait pour créer des IDevices.
Price Jones
1

Le seul doute dans mon esprit, c'est que maintenant l'application et la classe concrète du périphérique dépendent toutes les deux de la bibliothèque qui contient l'interface IDevice. Mais est-ce une mauvaise chose?

Vous devez penser l'inverse. Si vous ne procédez pas de cette façon, l'application devra connaître tous les différents appareils / implémentations. Ce que vous devez garder à l'esprit, c'est que changer cette interface pourrait casser votre application si elle est déjà dans la nature, donc vous devez concevoir cette interface avec soin.

Cela semble-t-il être la bonne approche pour implémenter une interface pour découpler la dépendance entre mon application et le périphérique qu'elle utilise?

Tout simplement oui

comment le béton arrive à l'application

L'application peut charger l'assemblage de béton (dll) au moment de l'exécution. Voir: /programming/465488/can-i-load-a-net-assembly-at-runtime-and-instantiate-a-type-knowing-only-the-na

Si vous devez décharger / charger dynamiquement un tel "pilote", vous devez charger l'assembly dans un AppDomain distinct .

Heslacher
la source
Merci, j'aimerais vraiment en savoir plus sur les interfaces lorsque j'ai commencé à coder.
Snoop
Désolé, j'ai oublié d'ajouter une partie (comment le béton arrive réellement à l'application). Pouvez-vous répondre à cela? Voir # 5.
Snoop
Effectuez-vous réellement vos applications de cette façon, en chargeant les DLL au moment de l'exécution?
Snoop
Si par exemple la classe concrète n'est pas connue de moi, alors oui. Supposons qu'un fournisseur d'appareils décide d'utiliser votre IDeviceinterface pour que son appareil puisse être utilisé avec votre application, alors il n'y aurait pas d'autre moyen IMO.
Heslacher