Tuesday, May 6, 2014

Использование паттерна Адаптер

В этой статье я расскажу о том, как с помощью паттерна Адаптер (англ. Adapter) можно расширить возможности базового класса. Паттерн Адаптер позволяет нашей системе работать с объектом, не подходящим для нашей системы. Мы адаптируем его к интерфейсу, подходящему для нашей системы. Этот паттерн относится к классу структурных паттернов. Много начинающих разработчиков после прочтения книги "Приемы объектно-ориентированного проектирования. Паттерны проектирования" "банды четырех" не используют данный паттерн в таком виде, в котором он приведен в книге. Они могут использовать данный паттерн в других контекстах, не зная названия подхода. Часто разработчики UI употребляют второе название этого паттерна – Обертка (Wrapper).
Только недавно я столкнулся с интересным аспектом данного паттерна, который позволяет, кроме адаптации нужного класса к нужному интерфейсу, расширить возможности исходного класса. По сути, данный паттерн можно скомбинировать с паттерном Декоратор (Decorator). Это посложнее, но такой способ использования тоже имеет место быть. Есть несколько подходов использования паттерна Adapter. Один из них основан на композиции, второй – на наследовании. Нужно смотреть на то, как будет лучше использовать интерфейс в конкретном контексте, и тогда выбирать наиболее подходящий путь.
Задача: допустим, у нас есть некое устройство, которое работает по сети, позволяет считывать некоторые данные и передавать их в центр обработки. У нас есть документация по данному устройству. Нам нужно сделать возможность нашей программы работать с разным количеством таких устройств. Но после того, как мы посмотрели на интерфейс, мы увидели, что интерфейс, который предоставляет нам SDK данного устройства, не позволяет его использовать в таком виде для наших нужд. Ниже приведен прототип данного устройства, который мы будем использовать для дальнейшей работы.
public class DeviceClass
{
    private int _machineId;
    private int _port;

    public int MachineNumber { get { return _machineId; } }
    public bool Connect_Net(string ip)
    {
        OnConnected();
        return true;
    }
       
    public bool RegEvent(int machineId, int port)
    {
        _machineId = machineId;
        _port = port;

        return true;
    }

    public event Action OnConnected = delegate {};
    public event Action OnDisConnected = delegate { };
    public event Action OnAlarm = delegate { };
    public event Action OnAttTransaction = delegate { };
    public event Action OnAttTransactionEx = delegate { };
    public event Action OnHIDNum = delegate { };
    public event Action OnKeyPress = delegate { };
}
Этот класс только является прототипом реального класса для работы с данным устройством.  Смотрим в SDK, как нам нужно использовать данный класс.
DeviceClass device= new DeviceClass();

bool bconnected = device.Connect_Net("192.168.1.101");

int iMachineNumber=1; //Id устройства

if (bconnected)
{
    if (device.RegEvent(iMachineNumber, 65535))
    {
        device.OnConnected += new device_OnConnected;
        device.OnDisConnected += device_OnDisConnected;
    }
}
void device_OnConnected()
{
    ....
}

void device_OnDisConnected()
{
    ....
}
И т.д. Как видим, работать с данным устройством мы можем только в том случае, если успешно установили соединение с ним и смогли его зарегистрировать. Только после этого мы можем подписываться на события данного устройства. Но если мы посмотрим на событие device_OnConnected, то увидим, что это обычный Action, который не принимает аргументов и возвращает void. Если у нас одно устройство, то, в принципе, проблем нет. А что делать, если у нас может быть до 100 устройств? Создавать на каждое устройство свое событие – не вариант. По текущему событию мы не сможем узнать, какое устройство вызвало событие OnConnected и OnDisconnected. Тут на помощь приходит паттерн Адаптер, который позволит расширить текущие возможности нашего интерфейса. Для нашего примера хорошо подходит композиция. Первым делом я вижу такие расширения для данного класса:
1. Добавить необходимые методы и свойства;
2. Делать обертку над событиями;
3. Сделать несколько простых методов, чтобы скрыть реализацию.
Давайте посмотрим, как это будет выглядеть:
public class DeviceClassAdapter : IDisposable
{
    #region Variables
    private int _machine;
    private int _port;
    private string _address;
    private DeviceClass _device;
    private bool _isRegistered;
    #endregion

    #region Constructor
    public DeviceClassAdapter(int machine, string address, int port)
    {
        _machine = machine;
        _port = port;
        _address = address;
        _device = new DeviceClass();
    }
    #endregion

    #region Public Methods
    public void RegisterDevice()
    {
        bool connected = _device.Connect_Net(_address);
        if (!connected)
            return;

        _isRegistered = _device.RegEvent(_machine, _port);
        if (_isRegistered)
        {
            _device.OnConnected += device_OnConnected;
            _device.OnDisConnected += device_DisConnected;
            _device.OnAlarm += ma300_OnAlarm;
            _device.OnAttTransaction += ma300_OnAttTransaction;
            _device.OnAttTransactionEx += ma300_OnAttTransactionEx;
            _device.OnHIDNum += ma300_OnHIDNum;
            _device.OnKeyPress += ma300_OnKeyPress;
        }
    }

    public void Dispose()
    {
        if(_isRegistered)
        {
            //Unsubscribe events
            if (_device == null)
                return;
            _device.OnConnected -= device_OnConnected;
            _device.OnDisConnected -= device_DisConnected;
            _device.OnAlarm -= ma300_OnAlarm;
            _device.OnAttTransaction -= ma300_OnAttTransaction;
            _device.OnAttTransactionEx -= ma300_OnAttTransactionEx;
            _device.OnHIDNum -= ma300_OnHIDNum;
            _device.OnKeyPress -= ma300_OnKeyPress;
        }
    }
    #endregion

    #region Public Properties
    public int MachineNumber
    {
        get
        {
            return _machine;
        }
    }

    public int Port
    {
        get
        {
            return _port;
        }
    }
    #endregion

    #region Private Methods
    private void device_DisConnected()
    {
        OnDisConnected(this, new EventArgs());
    }

    private void device_OnConnected()
    {
        OnConnected(this, new EventArgs());
    }

    private void ma300_OnKeyPress()
    {
        OnKeyPress(this, new EventArgs());
    }

    private void ma300_OnHIDNum()
    {
        OnHIDNum(this, new EventArgs());
    }

    private void ma300_OnAttTransactionEx()
    {
        OnAttTransactionEx(this, new EventArgs());
    }

    private void ma300_OnAttTransaction()
    {
        OnAttTransaction(this, new EventArgs());
    }

    private void ma300_OnAlarm()
    {
        OnAlarm(this, new EventArgs());
    }
    #endregion

    #region Events
    public event EventHandler OnConnected = delegate {};
    public event EventHandler OnDisConnected = delegate { };
    public event EventHandler OnAlarm = delegate { };
    public event EventHandler OnAttTransaction = delegate { };
    public event EventHandler OnAttTransactionEx = delegate { };
    public event EventHandler OnHIDNum = delegate { };
    public event EventHandler OnKeyPress = delegate { };
    #endregion
}
Мы создали простой класс DeviceClassAdapter, который скрывает в себе некоторую логику по работе с данным устройством. Теперь каждое устройство мы можем легко идентифицировать по номеру. Также над каждым событием мы сделали обертку и подменили события устройства на те, которые выгодны нам. Теперь когда мы подписываемся на событие OnConnected класса DeviceClassAdapter, сразу несколькими подписчиками, мы можем легко идентифицировать, какое устройство бросило нам то или иное событие. Для того чтобы сделать удобную работу с несколькими такими устройствами, мы напишем простенький класс DeviceWorker, который будет хранить информацию о всех подключенных устройствах, а также будет реализовывать всю логику по работе с этими устройствами.
public class DeviceWorker : IDisposable
{
    private Dictionary<int, DeviceClassAdapter> _devices;

    public DeviceWorker()
    {
        _devices = new Dictionary<int, DeviceClassAdapter>();
    }

    public void RegisterDevice(int machine, string address, int port)
    {
        //If machine found
        if (_devices.ContainsKey(machine))
            return;

        var device = new DeviceClassAdapter(machine, address, port);
        device.OnConnected += device_OnConnected;
        device.OnDisConnected += device_DisConnected;
        ///И так все события
        device.RegisterDevice();
        _devices.Add(machine, device);
    }

    public void UnregisterDevice(int machine)
    {
        if (_devices.ContainsKey(machine))
        {
            _devices[machine].OnConnected -= device_OnConnected;
            _devices[machine].OnDisConnected -= device_DisConnected;
            _devices.Remove(machine);
        }
    }

    void device_OnConnected(object sender, EventArgs args)
    {
        var device = sender as DeviceClassAdapter;
        if (device == null)
            return;

        //work with some device
        Console.WriteLine(device.MachineNumber);
    }

    void device_DisConnected(object sender, EventArgs args)
    {
        var device = sender as DeviceClassAdapter;
        if (device == null)
            return;

        //work with some device
        Console.WriteLine(device.MachineNumber);
    }

    public int DeviceCount
    {
        get
        {
            return _devices.Count;
        }
    }
    public void Dispose()
    {
        foreach(var device in _devices.Values)
        {
            device.OnConnected -= device_OnConnected;
            device.OnDisConnected -= device_DisConnected;
        }
    }
}
Для простоты я не реализовывал всю логику, а только часть, чтобы показать, как можно улучшить наш функционал. Напоследок приведу пример, как это можно использовать.
class Program
{
    static void Main(string[] args)
    {
        var worker = new DeviceWorker();
        worker.RegisterDevice(1, "195.195.195.4", 1234);
        worker.RegisterDevice(2, "195.195.195.4", 1234);
        worker.RegisterDevice(3, "195.195.195.4", 1234);

        worker.UnregisterDevice(1);
        worker.UnregisterDevice(2);
        worker.UnregisterDevice(3);
        Console.WriteLine(worker.DeviceCount);
        Console.ReadLine();
    }
}

Теперь у нас вся логика ложится на класс DeviceWorker, который работает не непосредственно классами, которые предоставляет SDK для работы с нашим устройством, а работает с адаптированным вариантом, который намного упростил нам работу. Основное, о чем нужно помнить при работе с данным паттерном,  это о его простоте, и адаптировать его под свои условия.  

No comments:

Post a Comment