使用接口进行松耦合代码


10

背景

我有一个项目,该项目取决于某种类型的硬件设备的使用情况,但是只要该硬件设备执行我需要的操作,则由谁来制造该硬件设备并不重要。话虽这么说,即使两个本来应该做同样事情的设备,如果不是由同一家制造商生产的,它们也会有差异。因此,我正在考虑使用一个接口将应用程序与所涉及设备的特定制造商/型号分离开,而使该接口仅涵盖最高级别的功能。这就是我在想我的体系结构的外观:

  1. 在一个C#项目中定义一个接口IDevice
  2. 在另一个C#项目中定义的库中有一个具体的东西,它将用来表示设备。
  3. 有具体的设备实现IDevice接口。
  4. IDevice接口可能具有类似GetMeasurement或的方法SetRange
  5. 使应用程序了解具体的知识,并将具体的知识传递给利用(而非实现IDevice设备的应用程序代码。

我非常确定这是正确的做法,因为这样我就可以在不影响应用程序的情况下更改正在使用的设备(这似乎有时会发生)。换句话说,具体的实现方式GetMeasurementSetRange实际操作方式并不重要(设备制造商之间可能会有所不同)。

我唯一的疑问是,现在应用程序和设备的具体类都依赖于包含IDevice接口的库。但是,这是一件坏事吗?

我也看不到应用程序不需要知道设备的方式,除非设备与设备IDevice位于相同的名称空间中。

这似乎是实现接口以解耦应用程序与所使用设备之间的依赖关系的正确方法吗?


这绝对是正确的方法。您实质上是在编写设备驱动程序,而这正是有充分理由传统上编写设备驱动程序的方式。您无法回避这样一个事实,即要使用设备的功能,就必须依赖至少以抽象方式了解这些功能的代码。
凯莉安·佛斯

@KilianFoth是的,我有一种感觉,忘了在我的问题中添加一部分。参见#5。
史努比(Snoop)

Answers:


5

我认为您已经对解耦软件的工作方式有了很好的了解:)

我唯一的疑问是,现在应用程序和设备的具体类都依赖于包含IDevice接口的库。但是,这是一件坏事吗?

不必是!

我也看不到应用程序不需要知道设备的方式,除非设备和IDevice位于同一名称空间中。

您可以使用Project Structure处理所有这些问题。

我通常的做法是:

  • 将我所有的抽象内容放入一个Common项目中。有点像MyBiz.Project.Common。其他项目可以自由引用它,但它可能不引用其他项目。
  • 创建抽象的具体实现时,我将其放在单独的项目中。有点像MyBiz.Project.Devices.TemperatureSensors。该项目将引用该Common项目。
  • 然后,有了我的Client项目,这是我的应用程序的入口点(类似MyBiz.Project.Desktop)。在启动时,该应用程序将经历引导过程,在此过程中,我将配置抽象/具体实现映射。我可以实例具体我IDevices喜欢WaterTemperatureSensorIRCameraTemperatureSensor这里,我也可以配置诸如工厂或IoC容器服务以后实例权的具体类型适合我。

这里的关键是,只有您的Client项目才需要了解抽象Common项目和所有具体的实现项目。通过将abstract-> concrete映射限制在您的Bootstrap代码中,您可以使其余应用程序幸福地意识到具体类型。

松耦合代码ftw :)


2
DI确实从这里自然而然地遵循。这在很大程度上也是项目/代码结构方面的问题。拥有IoC容器可以帮助DI,但这不是前提条件。您也可以通过手动注入依赖项来实现DI!
MetaFight

1
@StevieV是的,如果您应用程序中的Foo对象需要IDevice,则依赖项反转可以像通过构造函数(new Foo(new DeviceA());)注入一样简单,而不是在Foo中使用私有字段实例化DeviceA本身(private IDevice idevice = new DeviceA();),您仍然可以实现DI,因为Foo在第一种情况下不知道DeviceA
另一天是

2
@anotherdave和MetaFight的输入非常有帮助。
史努比

1
@StevieV有时间的时候,这里有一个很好的介绍依赖反转的概念(来自“ Uncle Bob” Martin的创造者),与诸如Spring(或其他一些东西)的控制/能力注入反转框架不同。在C♯世界中流行的例子:))
anotherdave's

1
@anotherdave一定打算检查一下,再次感谢您。
史努比

3

是的,这似乎是正确的方法。不,依靠接口对应用程序和设备库来说不是一件坏事,尤其是在您控制实现的情况下。

如果您出于某种原因担心设备可能不总是实现接口,则可以利用适配器模式并使设备的具体实现适应接口。

编辑

解决您的第五个问题时,请考虑以下结构(我假设您可以控制设备的定义):

您有一个核心库。其中有一个称为IDevice的接口。

在设备库中,您有对核心库的引用,并且定义了一系列均实现IDevice的设备。您还拥有一个工厂,该工厂知道如何创建不同类型的IDevice。

在您的应用程序中,您包括对核心库和设备库的引用。您的应用程序现在使用工厂来创建符合IDevice接口的对象的实例。

这是解决您的问题的多种可能方法之一。

例子:

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.
            }
        }
    }
}

那么,通过这种实现,具体的去向何去何从?
史努比(Snoop)

我只能说我会选择做什么。如果您控制设备,则仍将具体设备放在其自己的库中。如果您不受设备控制,则可以为适配器创建另一个库。
价格琼斯

我猜唯一的是,应用程序将如何访问我需要的特定混凝土?
史努比

一种解决方案是使用抽象工厂模式创建IDevice。
价格琼斯

1

我唯一的疑问是,现在应用程序和设备的具体类都依赖于包含IDevice接口的库。但是,这是一件坏事吗?

您需要反过来思考。如果您不这样做,那么应用程序将需要了解所有不同的设备/实现。您应该牢记的是,如果该接口已经被淘汰,那么更改该接口可能会破坏您的应用程序,因此您应该仔细设计该接口。

这似乎是实现接口以解耦应用程序与所使用设备之间的依赖关系的正确方法吗?

简直是

具体实际如何到达应用程序

应用程序可以在运行时加载具体的Assembly(dll)。参见:https : //stackoverflow.com/questions/465488/can-i-load-a-net-assembly-at-runtime-and-instantiate-a-type-knowing-only-the-na

如果需要动态卸载/加载此类“驱动程序”,则需要将程序集加载到单独的AppDomain中


谢谢,我真的希望我第一次开始编码时对接口有更多的了解。
史努比

抱歉,忘记添加一个部分(具体如何实际到达应用程序)。你能解决吗?参见#5。
史努比

您是否真的以这种方式运行您的应用程序,并在运行时加载DLL?
史努比

如果例如我不知道具体的课程,那就可以。假设设备供应商决定使用您的IDevice界面,以便他的设备可以与您的应用程序一起使用,那么IMO就不会有其他方式。
Heslacher '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.