简体   繁体   English

如何在 C# 中设计硬件接口,同时保持 DRY 和 SOLID?

[英]How to design hardware interfaces in C# while staying DRY and SOLID?

I am having a design problem with an application that handles two or more networked I/O devices.我在处理两个或更多联网 I/O 设备的应用程序时遇到了设计问题。

Both devices share properties like a name, IP address, and port.两个设备共享名称、IP 地址和端口等属性。 They will also share methods such as Connect(), Disconnect(), IsConnected() .他们还将共享Connect(), Disconnect(), IsConnected()等方法。

In an effort to stay DRY, this leads me to believe I need some interface - IODevice.为了保持 DRY,这让我相信我需要一些接口 - IODevice。

public interface IODevice
{
    int Id { get; set; }
    string Name { get; set; }
    IPAddress IPAddress { get; set; }
    int Port { get; set; }
    
    bool IsConnected();
    void Connect();
    void Disconnect();
}

With both devices defined:定义了两个设备:

public class DeviceOne : IODevice
{

    private DeviceOneApi _api;
    
    public int Id { get; set; }
    public string Name { get; set; }
    public IPAddress IPAddress { get; set; }
    int Port { get; set; }
        
    public DeviceOne()
    {
        _api = new DeviceOneApi();
    }

    public bool IsConnected()
    {
        return _api.IsDeviceConnected();
    }

    public void Connect() 
    {
        _api.OpenConnection();
    }
    
    public void Disconnect()
    {
        _api.CloseConnection();
    }

}
public class DeviceTwo : IODevice
{
    ...
}

I will need to monitor the I/O to detect changes - this could be a digital sensor or a bit-change in memory.我需要监控 I/O 以检测变化——这可能是数字传感器或 memory 中的位变化。 I was thinking of using some controller to loop through all defined devices and check the device I/O.我正在考虑使用一些 controller 循环遍历所有定义的设备并检查设备 I/O。 An event aggregator will be used to send out notifications.事件聚合器将用于发送通知。

public class IOController
{
    private Thread _monitorThread;
    private int _monitorDelay;  
    private bool _canMonitor;
    private ILogger _logger;
    
    public static List<IODevice> Devices { get; set; }
    
    public IOController(ILogger logger)
    {
        _logger = logger;
        _monitorDelay = 100;
        _monitorThread = new Thread(Monitor);
    }
    
    public void AddDevice(IODevice device)
    {
        Devices.Add(device);
    }
    
    public void StartMonitor()
    {
        _canMonitor = true;
        _monitorThread.Start();
    }
    
    private void Monitor()
    {
        while(_canMonitor)
        {
            foreach(IODevice device in Devices)
            {
                // Check I/O Points
                EventAggregator.Instance.Publish(new SomeIOChange(IODetails));
                Thread.Sleep(_monitorDelay)
            }
        }
    }
}

This is the first place I am trying to make a decision.这是我试图做出决定的第一个地方。 Each device implements its own types of I/OIe DeviceOne may use integers in memory and DeviceTwo may use booleans from digital signals.每个设备实现自己的 I/OIe类型DeviceOne 可以使用 memory 中的整数,而 DeviceTwo 可以使用来自数字信号的布尔值。 A device may also use multiple types of I/O - digital, analog, strings, etc and the device API will implement methods to read/write each of these types.设备还可以使用多种类型的 I/O - 数字、模拟、字符串等,并且设备 API 将实现读取/写入每种类型的方法。

So, I could either keep a seperate list of each device type and run multiple foreach loops:因此,我可以保留每种设备类型的单独列表并运行多个 foreach 循环:

foreach(IODeviceOne deviceOne in DeviceOnes) {  }
foreach(IODeviceTwo deviceTwo in DeviceTwos) {  }

Or, I could have the IODevice implement a method that checks its own I/O.或者,我可以让 IODevice 实现一个检查自己的 I/O 的方法。

public interface IODevice
{
    ...
    void CheckIO();
}
private void Monitor()
{
    while(_canMonitor)
    {
        foreach(IODevice device in Devices)
        {
            device.CheckIO();
        }
    }
}

However, there will also be external scripts and user input that will need to read or modify an I/O type directly.但是,也有需要直接读取或修改 I/O 类型的外部脚本和用户输入。 With the IODevice interface defined in a way to be shared across devices, there is not a specific implementation of read/write.由于 IODevice 接口以跨设备共享的方式定义,因此没有特定的读/写实现。

For instance, a user may hit a toggle on the front-end that will affect a digital output in device one.例如,用户可能会点击前端的开关,这将影响设备一中的数字 output。 Eventually, this action needs to be propagated to:最终,此操作需要传播到:

public class DeviceOne : IODevice
{
    ...
    public void WriteDeviceOnePointTypeOne(DeviceOnePointDetails details)
    {
        _api.WriteDeviceOnePointTypeOne(details);
    }
}

Since I am using EventAggregator, should I just implement the event listeners in each device instance?由于我使用的是 EventAggregator,我应该只在每个设备实例中实现事件侦听器吗?

public class DeviceOne : IODevice, ISubscriber<DeviceOnePointUpdate>
{
    ...
    
    public void OnEvent(DeviceOnePointTypeOneUpdate e)
    {
        _api.WriteDeviceOnePointTypeOne(e.Details)
    }
}

Or should I just use specific interfaces all the way down even though this may not follow DRY?或者我应该一直使用特定的接口,即使这可能不遵循 DRY?

public DeviceOneController : IDeviceOneController
{
    ...
    private void Monitor()
    {
        while(_canMonitor)
        {
            foreach(IODeviceOne deviceOne in DeviceOnes)
            {
                // Check for I/O updates in device one
            }
        }
    }
}

Would casting be an option while still remaining SOLID?铸造是一种选择,同时仍然保持稳定吗?

public class FrontEndIOEventHandler
{
    private ILogger _logger;
    private IOController _controller;
    
    public FrontEndIOEventHandler(ILogger logger, IOController controller)
    {
        ...
    }

    public void UpdateDeviceOnePointTypeOne(int deviceId, DeviceOnePointTypeOneUpdate details)
    {
        DeviceOne deviceOne = _controller.GetDeviceById(deviceId) as DeviceOne;
        deviceOne.WriteDeviceOnePointTypeOne(details);
    }
}

I am using interfaces to aid in unit-testing and I eventually want to implement an IoC container.我正在使用接口来帮助进行单元测试,最终我想实现一个 IoC 容器。 The device information will come from some external configuration and this configuration will include a device map (the only important I/O points will be defined by the user).设备信息将来自一些外部配置,此配置将包括一个设备 map(唯一重要的 I/O 点将由用户定义)。

<devices>
    <device type="device_one">
        <name></name>
        <ip_address></ip_address>
        <port></port>
        <map>
            <modules>
                <module type="point_type_one">
                    <offset>0</module>
                    <points>
                        <point>
                            <offset>0</offset>
                        </point>
                        <point>
                            <offset>1</offset>
                        </point>
                    </points>
                </module>
            </modules>
        </map>
    </device>
    <device type="device_two">
        <name></name>
        <ip_address></ip_address>
        <port></port>
        <map>
            <integers>
                <integer length="32" type="point_type_four">
                    <offset>0</module>
                </integer>
            </integers>
        </map>
    </device>
</devices>

The overall goal is to read the configuration, instantiate each device, connect to them, start monitoring the specified I/O for changes, and have the device available for direct read/write.总体目标是读取配置、实例化每个设备、连接到它们、开始监视指定 I/O 的更改,并使设备可用于直接读/写。

I am open to all comments and criticisms.我对所有评论和批评持开放态度。 Please let me know if I am way off: I am still a novice and attempting to become a better (read. more professional) developer, I know there are ways to do this quick and dirty.如果我离题了,请告诉我:我仍然是一个新手,并试图成为一个更好(阅读。更专业)的开发人员,我知道有一些方法可以快速而肮脏地做到这一点。 but this is part of a bigger project and I'd like this code to be maintainable + extensible.但这是一个更大项目的一部分,我希望这段代码是可维护的+可扩展的。

Let me know if I need to provide any additional detail.如果我需要提供任何其他详细信息,请告诉我。 Thanks.谢谢。

I tried to keep this code maintainable an extensible我试图保持这段代码可维护和可扩展

public interface IIODevice
{
    int Id { get; set; }
    string Name { get; set; }
    IPAddress IPAddress { get; set; }
    int Port { get; set; }

    bool IsConnected();
    void Connect();
    void Disconnect();
    void CheckIO();
    void WriteDevicePointType(DevicePointTypeUpdateBase details);

}

devices defined设备定义

sealed class DeviceOne : IIODevice
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IPAddress IPAddress { get; set; }
    public int Port { get; set; }

    private readonly IDeviceApi deviceApi;
    // Constructor injection of DeviceApi
    public DeviceOne(IDeviceApi deviceApi) => this.deviceApi = deviceApi;

    public void Connect()
    {
        deviceApi.OpenConnection();
    }

    public void Disconnect()
    {
        deviceApi.CloseConnection();
    }

    public bool IsConnected()
    {
        return deviceApi.IsDeviceConnected();
    }

    public void CheckIO()
    {
        Console.WriteLine("....");
    }

    public void WriteDevicePointType(DevicePointTypeUpdateBase details)
    {
        var pointType = details as DeviceOnePointTypeOneUpdate;
        deviceApi.WriteDevicePointType(pointType);
        Console.WriteLine(pointType);
    }
}

Add more device...添加更多设备...

sealed class DeviceTwo : IIODevice {}
sealed class DeviceThree : IIODevice {}

Device Api Interface (with this we don't need to modify your IController when new Device comes)设备 Api 接口(有了这个我们不需要在新设备来的时候修改你的 IController)

interface IDeviceApi
{
    bool IsDeviceConnected();
    void OpenConnection();
    void CloseConnection();

    void WriteDevicePointType(DevicePointTypeUpdateBase details);
}

Add Device Specific Api...添加设备特定 Api...

sealed class DeviceOneApi : IDeviceApi { }
sealed class DeviceTwoApi : IDeviceApi { }
sealed class DeviceThreeApi : IDeviceApi { }

Creating an abstract for IOController (you can inject this to your FfronEndController and any others)为 IOController 创建一个抽象(您可以将其注入您的 FfronEndController 和任何其他人)

 public abstract class IOControllerBase
{
    private Thread _monitorThread;
    private int _monitorDelay;
    private bool _canMonitor;
    private ILogger _logger;

    public static List<IIODevice> Devices { get; set; }

    public IOControllerBase(ILogger logger)
    {
        _logger = logger;
        _monitorDelay = 100;
        _monitorThread = new Thread(Monitor);
        Devices = new List<IIODevice>();
    }

    public void AddDevice(IIODevice device)
    {
        Devices.Add(device);
    }

    public IIODevice GetDeviceById(int deviceID)
    {
        return Devices.FirstOrDefault(x => x.Id == deviceID);
    }

    public void StartMonitor()
    {
        _canMonitor = true;
        _monitorThread.Start();
    }

    protected void Monitor()
    {
        while (_canMonitor)
        {
            foreach (IIODevice device in Devices)
            {
                // Check I/O Points
                // If you have any parameter for each device 
                // you can create a ParameterBase abstract class
                // and inherit for each device which have different and common
                // check parameter.
                device.CheckIO();
                Thread.Sleep(_monitorDelay);
            }
        }
    }
}
public class IOController : IOControllerBase
{
    public IOController(ILogger logger) : base(logger) { }
}

/* For Unit Testing Purpose */
public class UnitTestIOController : IOControllerBase
{
    public UnitTestIOController(ILogger logger) : base(logger) { }
}

Your FrontEndIOEventHandler looks like您的 FrontEndIOEventHandler 看起来像

public class FrontEndIOEventHandler
{
    private ILogger _logger;
    private IOControllerBase _controller;
    public FrontEndIOEventHandler(ILogger logger, IOControllerBase controller)
    {
        _logger = logger;
        _controller = controller;
    }

    public void UpdateDevicePointTypeOne(int deviceId, DeviceOnePointTypeOneUpdate details) // or use DevicePointTypeUpdateBase
    {
        IIODevice deviceOne = _controller.GetDeviceById(deviceId);
        deviceOne.WriteDevicePointType(details);
    }
}

DevicePointTypeUpdateBase DevicePointTypeUpdateBase

public abstract class DevicePointTypeUpdateBase
{
    public int CommonPropery { get; set; }
}

DeviceOne Specific DeviceOne 特定

public class DeviceOnePointTypeOneUpdate : DevicePointTypeUpdateBase
{
    public int DeviceOneIOValue { get; set; }
}

DeviceTwo Specific DeviceTwo 特定

public class DeviceTwoPointTypeOneUpdate : DevicePointTypeUpdateBase
{
    public bool DeviceOneIOValue { get; set; }
}

so all the components are loosely coupled, Use any IoC Container to Inject Dependency所以所有的组件都是松耦合的,使用任何 IoC 容器来注入依赖

You can close your class for editing, add extension method for adding any new feature in any of your component use Extension Methods to base componets.您可以关闭 class 进行编辑,添加扩展方法以在任何组件中添加任何新功能,使用扩展方法到基础组件。 Adding more Device will not break you IOController,ie.. DeviceFour.添加更多设备不会破坏您的 IOController,即.. DeviceFour。 Whenever we adding DeviceFour you should create new DeviceFour concrete class and DeviceFourApi (if you can't use any Existing api) this will be easily attach with your IOController.每当我们添加 DeviceFour 时,您应该创建新的 DeviceFour 具体 class 和 DeviceFourApi(如果您不能使用任何现有的 api),这将很容易与您的 IOController 连接。

Happy Coding..快乐编码..

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM