Обработчики событий и интерфейсы

У меня есть интерфейс под названием IDataIO:

public interface IDataIO
{
  event DataReceivedEvent DataReceived;
  //.....more events,methods and properties
}

У меня также есть несколько классов, которые реализуют этот интерфейс, а именно UdpIO, TcpIO, SerialIO.

Теперь у меня есть класс IO, который позволяет мне переключаться между различными устройствами ввода/вывода. Каждый экземпляр этого класса имеет свойство CurrentIODevice, которое может быть одним из SerialIO, UdpIO или TcpIO. Когда это свойство назначено, я присоединяю 1 или более обработчиков к DataReceivedEvent, чтобы мой GUI был уведомлен о приеме входящих данных, а также о других классах, которые необходимо уведомить.

public class IO
{
  IDataIO CurrentIODevice;

  public IO()
  {
    SerialIO serial = new SerialIO();
    TcpIO tcp = new TcpIO();
    UdpIO udp = new UdpIO();
    CurrentIODevice = serial;
  }
}

У меня также есть класс IOManager, который содержит несколько объектов IO.

public class IOManager
{
  List<IO> Ports = new List<IO>();
  public IOManager()
  {
    Ports.Add(new IO());
    Ports.Add(new IO());
  }

  Ports[0].CurrentIODevice = serial;
  Ports[0].CurrentIODevice.DataReceivedHandler += MyGuiUpdate;
  Ports[0].CurrentIODevice.DataReceivedHandler += MyDataProcessing;
}

Мое беспокойство (это не проблема atm) заключается в том, как я буду менять разные интерфейсы IDataIO во время выполнения.

Каков эффект во время выполнения, выполняющий следующий оператор:

//i know this is illegal but just to demonstrate
IOManager.Ports[0].CurrentIODevice = tcp; 

Будут ли выполняться обработчики событий (и правильно)?

Нужно ли переназначать события перед назначением CurrentIODevice, а затем снова назначать обработчики после? Если это так, я вижу, что этот подход становится довольно грязным, поэтому, если у кого-то есть лучший подход к этой проблеме, я все уши:)

Ответы

Ответ 1

Нет, ваши обработчики будут не работать, потому что они привязаны к старому объекту. Интерфейсы обеспечивают... интерфейс к объекту, рассматривают его как своего рода контракт, но они не являются самим другим объектом.

Если вам нужно переключиться между различными реализациями интерфейса (во время выполнения) и чтобы все обработчики работали, вы должны иметь одну и ту же ссылку на объект для самого интерфейса, например шаблон стратегии (более или менее).

В вашем случае вы можете, например, реализовать интерфейс IDataIO в объекте DataIO. Он будет раскрывать свойство (или метод, я думаю, что его намерение более ясное) для переключения между различными реализациями этого интерфейса (serial, TCP или что-то еще). Это будет единственный объект, который присоединяет обработчик события к этому интерфейсу (и он удалит обработчик при изменении конкретной реализации). Пользователи этого объекта всегда будут видеть это, независимо от того, какую конкретную реализацию он использует.

Пример

Это небольшой пример, объясняющий эту концепцию. Общий интерфейс:

interface IDataIO
{
    void Write(byte[] data);

    byte[] Read();

    event EventHandler DataReceived;
}

Это конкретная реализация IDataIO, другие классы будут использовать только этот класс напрямую:

sealed class DataIO : IDataIO
{
    public void SetChannel(IDataIO concreteChannel)
    {
        if (_concreteChannel != null)
            _concreteChannel.DataReceived -= OnDataReceived;

        _concreteChannel = concreteChannel;
        _concreteChannel.DataReceived += OnDataReceived;
    }

    public void Write(byte[] data)
    {
        _concreteChannel.Write(data);
    }

    public byte[] Read()
    {
        return _concreteChannel.Read();
    }

    public event EventHandler DataReceived;

    private IDataIO _concreteChannel;

    private void OnDataReceived(object sender, EventArgs e)
    {
        EventHandler dataReceived = DataReceived;
        if (dataReceived != null)
            dataReceived(this, e);
    }
}

Наконец, код для тестирования:

class Test
{
    public Test()
    {
        _channel = new TcpIO();

        _channel.DataReceived += OnDataReceived;
    }

    public void SetChannel(IDataIO channel)
    {
        _channel.SetChannel(channel);

        // Nothing will change for this "user" of DataIO
        // but now the channel used for transport will be
        // the one defined here
    }

    private void OnDataReceived(object sender, EventArgs e)
    {
        // You can use this
        byte[] data = ((IDataIO)sender).Read();

        // Or this, the sender is always the concrete
        // implementation that abstracts the strategy in use
        data = _channel.Read();
    }

    private DataIO _channel;
}

Ответ 2

Очевидно, что вы должны рассмотреть шаблон стратегии . Сначала я отправлю код и объясню позже:

public interface IDataIO
{
    event DataReceivedEvent DataReceived;

    //this the new added method that each IO type should implement.
    void SetStrategy();
}

public class SerialIO : IDataIO
{
    public void SetStrategy()
    {
        //put the code that related to the Serial IO.
        this.DataReceivedHandler += MyGuiUpdate;
        this.DataReceivedHandler += MyDataProcessing;
    }
}

public class TcpIO : IDataIO
{
    public void SetStrategy()
    {
        //put the code that related to the Tcp IO.
        //I will not implement it because it is a demo.
    }
}

public class UdpIO : IDataIO
{
    public void SetStrategy()
    {
        //put the code that related to the Udp IO.
        //I will not implement it because it is a demo.
    }
}

public class IO
{
    IDataIO port = new IDataIO();

    public void SetIOType(IDataIO ioType)
    {
        this.port = ioType;

        port.SetStrategy();
    }

}

public class IOManager
{
    List<IO> ports = new List<IO>();

    SerialIO serial = new SerialIO();
    TcpIO tcp = new TcpIO();

    ports[0].SetIOType(serial);
    ports[1].SetIOType(tcp);
}
  • Интерфейс IDataIO определяет основы, которые должны реализовывать все типы ввода-вывода.

  • Серии SerialIO, TcpIO, UdpIO, полученные из IDataIO, реализуют метод SetStrategy() для удовлетворения каждой из своих потребностей.

  • Класс IO, которому принадлежит поле (named port), относится к типу IDataIO, это поле может быть настроено на определенный тип ввода-вывода во время выполнения, вызывая метод SetIOType(), определенный в IO класс. Как только этот метод вызывается, мы знаем, к какому типу относится поле "порт", и затем вызовите метод SetStrategy(), он запустит переопределенный метод в одном из классов IO.

  • Класс IOManager является клиентом. когда ему нужен определенный тип ввода-вывода, скажем, SerialIO, ему нужно только создать новый класс IO и вызвать метод SetIOType(), передав экземпляр класса SerialIO, и вся логика, связанная с типом SerialIO, будет автоматически установлена.

Надеюсь, мое описание поможет вам.