Używanie interfejsów dla luźno sprzężonego kodu

10

tło

Mam projekt, który zależy od użycia określonego rodzaju urządzenia sprzętowego, podczas gdy tak naprawdę nie ma znaczenia, kto je tworzy, o ile robi to, czego potrzebuję. Biorąc to pod uwagę, nawet dwa urządzenia, które powinny robić to samo, będą się różnić, jeśli nie zostaną wyprodukowane przez tego samego producenta. Zastanawiam się więc, czy użyć interfejsu do oddzielenia aplikacji od konkretnej marki / modelu urządzenia, a zamiast tego interfejs powinien obejmować tylko funkcjonalność najwyższego poziomu. Oto, jak myślę, jak będzie wyglądać moja architektura:

  1. Zdefiniuj interfejs w jednym projekcie C # IDevice.
  2. Posiadaj konkretny element w bibliotece zdefiniowanej w innym projekcie C #, który będzie używany do reprezentowania urządzenia.
  3. Niech konkretne urządzenie wdroży IDeviceinterfejs.
  4. IDeviceInterfejs może mieć metod, takich jak GetMeasurementczy SetRange.
  5. Spraw, aby aplikacja miała wiedzę na temat betonu, i przekaż ten beton do kodu aplikacji, który wykorzystuje (a nie implementuje ) IDeviceurządzenie.

Jestem całkiem pewien, że jest to właściwy sposób, aby to zrobić, ponieważ wtedy będę mógł zmienić, które urządzenie jest używane, bez wpływu na aplikację (co zdarza się czasami). Innymi słowy, nie będzie miało znaczenia, w jaki sposób implementacje GetMeasurementlub SetRangefaktycznie działają przez beton (co może różnić się między producentami urządzenia).

Jedyne wątpliwości, jakie mam, to to, że teraz zarówno aplikacja, jak i konkretna klasa urządzenia zależą od biblioteki zawierającej IDeviceinterfejs. Ale czy to zła rzecz?

Nie widzę też, w jaki sposób aplikacja nie musi wiedzieć o urządzeniu, chyba że urządzenie i IDevicesą w tej samej przestrzeni nazw.

Pytanie

Czy wydaje się to właściwym podejściem do implementacji interfejsu w celu oddzielenia zależności między moją aplikacją a urządzeniem, którego używa?

Podejrzeć
źródło
To jest absolutnie właściwy sposób na zrobienie tego. Zasadniczo programujesz sterownik urządzenia, i właśnie w ten sposób sterowniki urządzeń są tradycyjnie pisane, bez uzasadnionego powodu. Nie można obejść faktu, że aby skorzystać z możliwości urządzenia, musisz polegać na kodzie, który zna te możliwości przynajmniej w abstrakcyjny sposób.
Kilian Foth,
@KilianFoth Tak, miałem przeczucie, że zapomniałem dodać jedną część do mojego pytania. Zobacz # 5.
Snoop,

Odpowiedzi:

5

Myślę, że dobrze rozumiesz, jak działa oddzielone oprogramowanie :)

Jedyne, co mam wątpliwości, to to, że teraz zarówno aplikacja, jak i konkretna klasa urządzenia zależą od biblioteki zawierającej interfejs IDevice. Ale czy to zła rzecz?

Nie musi tak być!

Nie widzę też, w jaki sposób aplikacja nie musi wiedzieć o urządzeniu, chyba że urządzenie i IDevice znajdują się w tej samej przestrzeni nazw.

Możesz rozwiązać wszystkie te problemy za pomocą Struktury Projektu .

Sposób, w jaki zazwyczaj to robię:

  • Umieść wszystkie moje abstrakcyjne rzeczy w Commonprojekcie. Coś jak MyBiz.Project.Common. Inne projekty mogą się do niego odwoływać, ale nie mogą odnosić się do innych projektów.
  • Kiedy tworzę konkretną implementację abstrakcji, umieszczam ją w osobnym projekcie. Coś jak MyBiz.Project.Devices.TemperatureSensors. Ten projekt będzie odnosił się do Commonprojektu.
  • Mam wtedy mój Clientprojekt, który jest punktem wejścia do mojej aplikacji (coś w rodzaju MyBiz.Project.Desktop). Podczas uruchamiania aplikacja przechodzi proces ładowania początkowego, w którym konfiguruję odwzorowania abstrakcji / konkretnych implementacji. Mogę utworzyć instancję moich konkretnych elementów, IDevicestakich jak WaterTemperatureSensori IRCameraTemperatureSensortutaj, lub skonfigurować usługi, takie jak fabryki lub kontener IoC, aby później utworzyć dla mnie odpowiednie typy konkretnych elementów.

Kluczową kwestią jest to, że tylko Twój Clientprojekt musi być świadomy zarówno Commonprojektu abstrakcyjnego , jak i wszystkich konkretnych projektów wdrożeniowych. Ograniczając abstrakcyjne> konkretne mapowanie do kodu Bootstrap, pozwalasz pozostałej części aplikacji na błogą nieświadomość konkretnych typów.

Luźno powiązany kod ftw :)

MetaFight
źródło
2
DI wynika stąd naturalnie. W dużej mierze dotyczy to również projektu / struktury kodu. Posiadanie kontenera IoC może pomóc w DI, ale nie jest to warunkiem koniecznym. Możesz osiągnąć DI również poprzez ręczne wstrzykiwanie zależności!
MetaFight,
1
@StevieV Tak, jeśli obiekt Foo w twojej aplikacji wymaga IDevice, odwrócenie zależności może być tak proste, jak wstrzyknięcie go przez konstruktor ( new Foo(new DeviceA());), zamiast posiadania prywatnego pola w samej instancji Foo urządzenia DeviceA ( private IDevice idevice = new DeviceA();) - nadal osiągasz DI, ponieważ Foo nie wie o DeviceA w pierwszym przypadku
kolejnydave
2
@anotherdave twoja i MetaFight był bardzo pomocny.
Snoop,
1
@StevieV Kiedy masz czas, oto fajne wprowadzenie do koncepcji Dependency Inversion jako koncepcji (od „Uncle Bob” Martin, facet, który ją stworzył), odmiennej od frameworku Inversion of Control / Depency Injection, takiego jak Spring (lub kilka innych popularny przykład w świecie C♯ :))
anotherdave
1
@anotherdave Zdecydowanie planuję to sprawdzić, jeszcze raz dziękuję.
Snoop,
3

Tak, to wydaje się właściwe podejście. Nie, nie jest źle, że aplikacja i biblioteka urządzeń zależą od interfejsu, szczególnie jeśli kontrolujesz implementację.

Jeśli obawiasz się, że z jakiegoś powodu urządzenia nie zawsze mogą implementować interfejs, możesz użyć wzorca adaptera i dostosować konkretną implementację urządzeń do interfejsu.

EDYTOWAĆ

Odpowiadając na piąty problem, pomyśl o takiej strukturze (zakładam, że masz kontrolę nad definiowaniem swoich urządzeń):

Masz podstawową bibliotekę. Jest w nim interfejs o nazwie IDevice.

W bibliotece urządzeń masz odniesienie do biblioteki podstawowej i zdefiniowałeś serię urządzeń, które wszystkie implementują IDevice. Masz również fabrykę, która wie, jak tworzyć różnego rodzaju urządzenia ID.

W swojej aplikacji dołączasz odniesienia do biblioteki podstawowej i biblioteki urządzeń. Twoja aplikacja korzysta teraz z fabryki do tworzenia instancji obiektów zgodnych z interfejsem IDevice.

Jest to jeden z wielu możliwych sposobów rozwiązania problemów.

PRZYKŁADY:

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
źródło
Więc po tym wdrożeniu, dokąd zmierza beton?
Snoop,
Mogę mówić tylko za to, co chciałbym zrobić. Jeśli kontrolujesz urządzenia, nadal umieścisz konkretne urządzenia we własnej bibliotece. Jeśli nie kontrolujesz urządzeń, utwórz kolejną bibliotekę dla adapterów.
Cena Jones
Chyba jedyną rzeczą jest, w jaki sposób aplikacja uzyska dostęp do konkretnego betonu, którego potrzebuję?
Snoop,
Jednym z rozwiązań byłoby użycie abstrakcyjnego wzorca fabrycznego do utworzenia urządzeń ID.
Cena Jones
1

Jedyne, co mam wątpliwości, to to, że teraz zarówno aplikacja, jak i konkretna klasa urządzenia zależą od biblioteki zawierającej interfejs IDevice. Ale czy to zła rzecz?

Musisz pomyśleć na odwrót. Jeśli nie pójdziesz tą drogą, aplikacja będzie musiała wiedzieć o wszystkich różnych urządzeniach / implementacjach. Należy pamiętać, że zmiana tego interfejsu może spowodować uszkodzenie aplikacji, jeśli jest ona już dostępna w środowisku naturalnym, dlatego należy starannie zaprojektować ten interfejs.

Czy wydaje się to właściwym podejściem do implementacji interfejsu w celu oddzielenia zależności między moją aplikacją a urządzeniem, którego używa?

Po prostu tak

jak beton faktycznie dociera do aplikacji

Aplikacja może załadować konkretny zestaw (dll) w czasie wykonywania. Zobacz: /programming/465488/can-i-load-a-net-assembly-at-runtime-and-instantiate-a-type-knowing-only-the-na

Jeśli chcesz dynamicznie rozładować / załadować taki „sterownik”, musisz załadować zespół do osobnej AppDomain .

Heslacher
źródło
Dziękuję, naprawdę chciałbym wiedzieć więcej o interfejsach, kiedy zacząłem kodować.
Snoop,
Przepraszamy, zapomniałem dodać z jednej strony (jak konkretnie trafia do aplikacji). Czy możesz to rozwiązać? Zobacz # 5.
Snoop,
Czy faktycznie robisz swoje aplikacje w ten sposób, ładując biblioteki DLL w czasie wykonywania?
Snoop,
Jeśli np. Konkretna klasa nie jest przeze mnie znana, to tak. Załóżmy, że dostawca urządzenia decyduje się na użycie Twojego IDeviceinterfejsu, aby jego urządzenie mogło być używane z Twoją aplikacją, wtedy nie byłoby innego sposobu IMO.
Heslacher,