vendredi 28 mai 2021

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.

Both devices share properties like a name, IP address, and port. They will also share methods such as Connect(), Disconnect(), IsConnected().

In an effort to stay DRY, this leads me to believe I need some interface - 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 was thinking of using some controller to loop through all defined devices and check the device 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/O. I.e. DeviceOne may use integers in memory and DeviceTwo may use booleans from digital signals. 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.

So, I could either keep a seperate list of each device type and run multiple foreach loops:

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.

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. With the IODevice interface defined in a way to be shared across devices, there is not a specific implementation of read/write.

For instance, a user may hit a toggle on the front-end that will affect a digital output in device one. 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?

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?

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

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

Aucun commentaire:

Enregistrer un commentaire