10

Background

I have a project that depends on the usage of a certain type of hardware device, while it doesn't really matter who makes that hardware device as long as it does what I need it to do. With that being said, even two devices that are supposed to do the same thing will have differences when they are not made by the same manufacturer. So I am thinking to use an interface to decouple the application from the particular make/model of device involved, and instead have the interface just cover the highest-level functionality. Here is what I am thinking my architecture will look like:

  1. Define an interface in one C# project IDevice.
  2. Have a concrete in a library defined in another C# project, that will be used to represent the device.
  3. Have the concrete device implement the IDevice interface.
  4. The IDevice interface might have methods like GetMeasurement or SetRange.
  5. Make the application have knowledge about the concrete, and pass the concrete to the application code that utilizes (not implements) the IDevice device.

I am pretty sure that this is the right way to go about it, because then I will be able to change out what device is being used without affecting the application (which seems to happen sometimes). In other words, it won't matter how the implementations of GetMeasurement or SetRange actually work through the concrete (as may differ among manufacturers of the device).

The only doubt in my mind, is that now both the application, and the device's concrete class both depend on the library that contains the IDevice interface. But, is that a bad thing?

I also don't see how the application won't need to know about the device, unless the device and IDevice are in the same namespace.

Question

Does this seem like the right approach for implementing an interface to decouple the dependency between my application and the device that it uses?

Snoop
  • 2,718
  • 5
  • 24
  • 52
  • This is absolutely the correct way to do it. You're essentially programming a device driver, and this is exactly how device drivers are traditionally written, with good reason. You can't get around the fact that to use the capabilities of a device, you have to rely on code that knows these capabilities at least in an abstract way. – Kilian Foth May 09 '16 at 17:14
  • @KilianFoth Yeah I had a feeling, forgot to add one part to my question. See #5. – Snoop May 09 '16 at 17:16

3 Answers3

5

I think you've got a pretty good understanding of how decoupled software works :)

The only doubt in my mind, is that now both the application, and the device's concrete class both depend on the library that contains the IDevice interface. But, is that a bad thing?

It doesn't need to be!

I also don't see how the application won't need to know about the device, unless the device and IDevice are in the same namespace.

You can deal with all these concerns with Project Structure.

The way I typically do it:

  • Put all my abstract stuff in a Common project. Something like MyBiz.Project.Common. Other projects are free to reference it, but it may not reference other projects.
  • When I create a concrete implementation of a an abstraction I put it in a separate project. Something like MyBiz.Project.Devices.TemperatureSensors. This project will reference the Common project.
  • I then have my Client project which is the entrypoint to my application (something like MyBiz.Project.Desktop). At startup, the application goes through a Bootstrapping process where I configure abstraction/concrete-implementation mappings. I can instantiate my concrete IDevices like WaterTemperatureSensor and IRCameraTemperatureSensor here, or I can configure services like Factories or an IoC container to instantiate the right concrete types for me later on.

The key thing here is that only your Client project needs to be aware of both the abstract Common project and all the concrete implementation projects. By confining the abstract->concrete mapping to your Bootstrap code you're making it possible for the rest of your application to be blissfully unaware of the concrete types.

Loosely coupled code ftw :)

MetaFight
  • 11,549
  • 3
  • 44
  • 75
  • Okay, so during your bootstrapping process you would declare some `IDevice` member, and then assign it say... some object of type `DeviceA`, or `DeviceB` during runtime? – Snoop May 09 '16 at 18:08
  • Yup, that's one perfectly valid way of doing it. – MetaFight May 09 '16 at 18:31
  • Ok, and then would dependency injection be like the next step above doing that? Because I read up and a lot of developers are using DI to handle these sorts of things but I am not ready to go there yet because I don't have the time right now. – Snoop May 09 '16 at 18:33
  • 2
    DI does follow naturally from here. That's largely a project/code structure concern as well. Having an IoC container can help with DI but it's not a prerequisite. You can achieve DI by manually injecting dependencies too! – MetaFight May 09 '16 at 18:39
  • 1
    @StevieV Yeah, if the Foo object within your application requires an IDevice, dependency inversion can be as simple as injecting it via a constructor (`new Foo(new DeviceA());`), rather than having a private field within Foo instantiate DeviceA itself (`private IDevice idevice = new DeviceA();`)— you still achieve DI, as Foo is unaware of DeviceA in the first case – anotherdave May 09 '16 at 18:47
  • 2
    @anotherdave yours and MetaFight input was very helpful. – Snoop May 09 '16 at 18:48
  • 1
    @StevieV When you get time, [here's a nice intro](https://drive.google.com/file/d/0BwhCYaYDn8EgMjdlMWIzNGUtZTQ0NC00ZjQ5LTkwYzQtZjRhMDRlNTQ3ZGMz/view) into Dependency Inversion as a concept (from "Uncle Bob" Martin, the guy who coined it), distinct from an Inversion of Control/Depency Injection framework, like Spring (or some other more popular example in the C♯ world :) ) – anotherdave May 09 '16 at 19:09
  • 1
    @anotherdave Definitely planning to check that out, thank you again. – Snoop May 09 '16 at 19:09
3

Yes, that does seem like the right approach. No, it is not a bad thing for the application and the device library to depend on the interface, especially if you are in control of the implementation.

If you are concerned that the devices might not always implement the interface, for whatever reason, you can utilize the adapter pattern and adapt the concrete implementation of the devices to the interface.

EDIT

In addressing your fifth concern, think of a structure like this (I am assuming you are in control of defining your devices):

You have a core library. In it is an interface called IDevice.

In your devices library, you have a reference to your core library, and you have defined a series of devices which all implement IDevice. You also have a factory which knows how to create different kinds of IDevices.

In your application you include references to the core library and the devices library. Your application now uses the factory to create instances of objects which conform to the IDevice interface.

This is one of many possible ways to address your concerns.

EXAMPLES:

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
  • 635
  • 4
  • 8
  • So with this implementation, where does the concrete go? – Snoop May 09 '16 at 17:11
  • I can only speak for what I would choose to do. If you are in control of the devices, you would still put the concrete devices in their own library. If you are not in control of the devices you would create another library for the adapters. – Price Jones May 09 '16 at 17:27
  • I guess the only thing is, how will the application get access to the specific concrete that I need? – Snoop May 09 '16 at 17:29
  • One solution would be to use the abstract factory pattern to create IDevices. – Price Jones May 09 '16 at 17:53
1

The only doubt in my mind, is that now both the application, and the device's concrete class both depend on the library that contains the IDevice interface. But, is that a bad thing?

You need to think the other way around. If you don't go this way, the application would need to know about all of the different devices / implementations. What you should keep in mind is, that changing that interface could break your application if it is already out in the wild, hence you should design that said interface carefully.

Does this seem like the right approach for implementing an interface to decouple the dependency between my application and the device that it uses?

Simply yes

how the concrete actually gets to the app

The application can load the concrete assembly(dll) at runtime. See: https://stackoverflow.com/questions/465488/can-i-load-a-net-assembly-at-runtime-and-instantiate-a-type-knowing-only-the-na

If you need to dynamically unload/load such a "driver" you need to load the assembly into a separate AppDomain.

Heslacher
  • 151
  • 1
  • 9
  • Thank you, I really wish I knew more about interfaces when I first started coding. – Snoop May 09 '16 at 17:08
  • Sorry, forgot to add on one part (how the concrete actually gets to the app). Can you address that? See #5. – Snoop May 09 '16 at 17:15
  • Do you actually do your apps this way, loading the DLLs at run-time? – Snoop May 09 '16 at 18:02
  • If e.g the concrete class isn't known by me then yes. Assume a device vendor decides to use your `IDevice` interface so his device can be used with your app, then there wouldn't be another way IMO. – Heslacher May 09 '16 at 18:04