Паттерн проектирования «Одиночка» в D

В этой статье, нам потребуется не только компьютер, но и кое-что из мира электроники. Но кроме этого, нам потребуется одна вещь, которая впервые появилась в мире архитектуры, а уж затем стала привычным элементом мира программирования.

Для полного удовлетворения своего любопытства сегодня, нам потребуются:

  • 3 светодиода (зеленый, желтый и красный, хотя цвета не имеют особого значения);
  • 3 резистора на 220 Ом (хотя в принципе, можно и без них);
  • 1 Arduino Uno или любой другой Arduino;
  • Макетная плата;
  • Библиотека serial-port (взять можно отсюда);
  • Библиотека DFL2;
  • Прямые руки;
  • Голова (с модулем /dev/brain);

Для начала, установим Arduino IDE (если не установлена), скачав ее с официального сайта. После установки, необходимо будет выяснить, к какому COM-порту подключен Arduino: для чего в среде Arduino переходим в меню Инструменты и находим пункт Порт (напротив этого поля обычно показан текущий COM-порт в выпадающем списке, если же нет, проверьте подключение Arduino к USB. Если же указан другой порт, то опять же из выпадающего списка портов надо выбрать нужный порт, который обычно помимо именования,  содержит еще и имя подключенного к нему устройства (это в новой версии среды, 1.6.5 которая), затем записываем или запоминаем номер порта — это нам потребуется несколько позже.

Следующий шаг, который мы делаем, открываем Entice Designer и быстро создаем форму примерно вот такого вида:

После этого, аккуратно сохраняем весь код в какую-нибудь папку, например, в папку с именем arduinocontrol.

Следующим этапом, скачиваем с GitHub библиотеку serial-port и из скачанного архива, из папки serial-port-master/source/serial достаем файл device.d. Этот файл надо скопировать в папку arduinocontrol, ибо, увы, используя D Form Library пришлось распрощаться с возможностью установки этой замечательной библиотечки через dub и как следствие, весь проект придется компилировать вручную…

Закончив с serial-port, откладываем клавиатуру компьютера в сторону, берем макетную плату и собираем простую схему, соединяя каждый из светодиодов с некоторым выбранным выводом Arduino через резистор. Не забывайте о том, что нужно еще и подключить каждый из светодиодов к общей земле Arduino, к выходу с интригующим названием GND.

Подключаем Arduino к USB-порту и открываем Arduino IDE, так как сейчас мы сделаем небольшую тривиальную программу для микроконтроллера на плате. Итак, вы выбрали цифровые выходы Arduino и связали их с конкретными светодиодами (в моем случае получилось так: 5 выход соответствует красному светодиоду, 6 выход — желтому, а 7 выход — зеленому) и надеюсь запомнили, какой светодиод и к какому подключили — эти сведения очень понадобятся сейчас, при программировании Arduino.

Открыв среду и выбрав соответствующую версию своей платы (а также порт), мы вносим туда следующий код, написанный на Wiring:

byte incomingByte = 0;   // переменная для хранения полученного байта
 
void setup() {
    Serial.begin(9600); // устанавливаем последовательное соединение
    pinMode(5, OUTPUT);
    pinMode(6, OUTPUT);
    pinMode(7, OUTPUT);
}


void loop() {
   if (Serial.available() > 0) {  //если есть доступные данные
        // считываем байт
        incomingByte = Serial.read();
        switch (incomingByte)
        {
          case 'R':
              digitalWrite(5, HIGH);
              break;
          case 'r':
              digitalWrite(5, LOW);
              break;
          case 'Y':
              digitalWrite(6, HIGH);
              break;
          case 'y':
              digitalWrite(6, LOW);
              break;
          case 'G':
              digitalWrite(7, HIGH);
              break;
          case 'g':
              digitalWrite(7, LOW);
              break;
          default:
              break;
        }
    }
}

Скажу честно, я не особо крут в Wiring и Arduino, кроме того, давно этим не занимался, но позвольте объяснить как работает этот код и что он делает…

Для начала нам потребуется пустая переменная, которая хранит ровно один байт данных и этот байт данных равен нулю (помните, Wiring производный от C, а потому, целые числа и символьные данные — это по сути одно и то же числовое представление).
Процедура setup() осуществляет предварительную настройку последовательного соединения (передача/прием данных по COM-порту, ну а в данном случае, устанавливается скорость обмена данными с компьютером равная 9 600 бод и эта скорость, в общем-то, является стандартной) и настройку цифровых портов (в данном случае портов 5, 6, 7 на выход).

Процедура loop() является аналогом процедуры main() в D, но только в отличие от последней, выполняется в вечном (до перезагрузки или выключения, конечно же) цикле. Эта процедура содержит в себе опознание введенного символа, который служит командой для включения/выключения светодиода висящего на некотором порту микроконтроллера в плате.

Как вы уже наверное поняли, команды включения подаются большими буквами латинского алфавита: R (red) — включение красного светодиода, Y (yellow) — включение желтого светодиода, G (green) — включение зеленого светодиода; а команды выключения, соответственно, маленькими буквами латинского алфавита r, y, g.

Итак, мы хотим добиться примерного следующего эффекта: при включении флажка с надписью, указывающей название некоторого цвета, загорается светодиод на макетной плате подключенной к Arduino, а если флажок вернуть в исходное положение (изначально задаем флажкам состояние «выключено»), то нужный светодиод перестает светить. По сути дела, наша задача состоит в том, чтобы обеспечить простой вид взаимодействия некоторого устройства, подключенного к USB с компьютером — и в этом нам поможет наличие эмуляции COM-порта в самом драйвере Arduino (зашит в бутлоадер, ну и присутствует в виде драйвера в системе) и библиотека работы с COM-портом в D serial-port.

Однако, перед программированием стоит задуматься вот о какой вещи: обработка нажатия на флажок — это довольно тривиальная операция, и в итоге, получается три обработчика события click.
Казалось бы, что все нормально, но при этом получается, что нужно либо на глобальном уровне иметь переменную, в которой содержится ссылка на уже открытый COM-порт, либо постоянно открывать/закрывать порт внутри каждого из обработчиков…

Первый вариант плох по стилистическим соображениям — правила «хорошего тона» в программирования не поощряют использование глобальных переменных. Кроме того, наличие такой переменной может являться ограничивающим или сдерживающим расширение программы фактором, а размещение внутри класса переменной подобного рода есть верный признак того, что система получилась не слишком удачной и, скорее всего, дальнейших изменений она не переживет. Второй вариант может быть еще хуже из-за возможного потребления ресурсов, кроме того, у вас не будет гарантии того, что порт не занят кем-то другим.

Что делать ?
Изначально понятие «паттерн проектирования» появилось в архитектуре, а затем, с появлением знаменитой книги «банды Четырех» (так собирательно, полушутя, называют четырех программистов: Эриха Гамму, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса) «Приемы объектно-ориентированного проектирования. Паттерны проектирования» (в оригинале она называется «Design Patterns: Elements of Reusable Object-Oriented Software») пришло и в программирование. Под «паттерном проектирования» понимают решение некоторой довольно часто повторяющейся задачи с учетом некоторого контекста, и так уж сложилось, что первые 23 таких решения были изложены впервые в книге от «банды Четырех» (gang of Four, GoF) и именно эта книга, которая выступает в роли каталога паттернов, поможет нам решить проблему.

Итак, для решения проблемы мы воспользуемся порождающим паттерном проектирования, который называется Одиночка (Singleton), который на импровизированной UML-диаграмме (к сожалению, не смогли установить источник этой картинки) выглядит вот так:

Официальное определение паттерна выглядит следующим образом:

Паттерн Одиночка гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру

Не пугайтесь этих страшных слов, ибо на самом деле все довольно просто: мы создаем класс, который что бы ни случилось, имеет только один экземпляр и обеспечивает доступ только к этому экземпляру.
Вот, взгляните на простейший пример паттерна «Одиночка»:

class ClassicSingleton
{
	private static ClassicSingleton uniqueInstance = null;

	private this()
	{

	}

	public static ClassicSingleton instance()
	{
		if (uniqueInstance is null)
		{
			instance = new ClassicSingleton();
		}
		return uniqueInstance;
	}
}

Вначале, внутренняя приватная статическая переменная (статические переменные разделяются между всеми экземплярами данного класса, т.е. по сути дела это нечто вроде глобальной переменной для класса) содержит пустую ссылку на объект типа ClassicSingleton (классический одиночка) и этот момент не вызывает никаких эмоций или удивления. Удивительно другое: конструктор класса объявлен как private, что означает, что такой конструктор можно вызвать только внутри класса, что как может показаться не имеет никакого практического смысла, но на самом деле — private служит заглушкой для вызова конструктора, что означает тот факт, что вся работа делается где-то в другом месте.

Вообще, вся магия происходит внутри метода instance, который гарантирует создание единственного экземпляра: ведь, если внутренняя ссылка (в статической переменной uniqueInstance) равна null (т.е. пустому объекту), то очевидно, что объект еще не был создан и объект создается вызовом приватного конструктора. Но, ведь если uniqueInstance отлична от пустоты, то значит в ней уже существует объект нашего класса ClassicSingleton.

После таких объяснений возникает хороший вопрос: если конструктор приватный, то как мне создать объект ClassicSingleton класса, если конструктор приватный (использование new даст ошибку компиляции) ?
Легко, как никогда, достаточно просто в нужном месте сделать примерно так:

auto singleton = ClassicSingleton.instance();

После того, как мы разобрались с «Одиночкой», собрали простенькую схему, вытащили библиотеку serial-port и подключили ее к проекту, мы можем наконец заняться написанием логики самой программы и разбирательством с COM-портом.

Для того, чтобы воспользоваться COM-портом, нужно подключить библиотеку serial-port (файл device.d) к своему проекту, создать экземпляр класса SerialPort (в конструкторе которого строкой указать имя порта для экзекуции)  и использовать метод write для записи некоторого массива данных (по умолчанию, этот массив имеет «универсальный» /в своем роде/ тип const(void[]) arr):

import serial.device;
auto com = new SerialPort("COM3"); // в линуксе порты именуются вот так: /dev/ttyAC3 или как-то похоже
com.write("Hello, world!");

Итак, для решения проблемы с доступом к COM-порту у нас уже есть все необходимое: осталось только создать синглетный класс, инкапсулирующий в себе открытие и запрос COM-порта, что осуществляется примерно так:

import serial.device;

class ClassicSingleton
{
	private static SerialPort com;
	private static ClassicSingleton instance = null;
	
	protected this(string portname)
	{
		scope(exit) com = new SerialPort(portname);
	}
	
	
	public static ClassicSingleton getInstance(string portname)
	{
		if (instance is null)
		{
			instance = new ClassicSingleton(portname);
		}
		return instance;
	}
	
	public void send(const(void[]) arr)
	{
		try
		{
			com.write(arr);
		}
		catch
		{
			
		}
	}
}

Теперь же, можно заняться обработчиками событий.
Обработчик события должен правильно отреагировать на нажатие клавишей мыши, для чего нам потребуется логическая переменная (а всего три переменных, с человекопонятными именами: red, yellow и green). После каждой установки или сброса флажка, необходимо саму переменную перевести в противоположное состояние, чего можно достигнуть просто используя унарный оператор «!», который в данном случае будет означать «не» или инверсию состояния, однако, перед тем как мучить переменную состояния необходимо в некоторую переменную поместить созданную (или уже существующую) ссылку на экземпляр «одиночки», созданный для операций с портом.

Дальнейшие операции тривиальны, а код обработчика может выглядеть например так:

// управляем желтым светодиодом
void onYELLOWClick(Object sender, EventArgs ea)
	{
		auto com = ClassicSingleton.getInstance("COM3"); // одиночка
		
		yellow = !yellow;
		
		if (yellow)
		{
			com.send(`Y`);
		}
		else
		{
			com.send(`y`);
		}
		
	}

Наконец, вписав обработчики для всех трех светодиодов, скомпилировав программку и залив прошивку в Arduino, мы получаем вот такое (что в конце концов, может привести к домашней автоматике):

Плата Arduino — самое начало всех манипуляций

 

Включаем желтый светодиод с компьютера

Весь код проекта (под спойлером).

import serial.device;
import dfl.all;

class ClassicSingleton
{
	private static SerialPort com;
	private static ClassicSingleton instance = null;
	
	protected this(string portname)
	{
		scope(exit) com = new SerialPort(portname);
	}
	
	
	public static ClassicSingleton getInstance(string portname)
	{
		if (instance is null)
		{
			instance = new ClassicSingleton(portname);
		}
		return instance;
	}
	
	public void send(const(void[]) arr)
	{
		try
		{
			com.write(arr);
		}
		catch
		{
			
		}
	}
}

class ArduinoControl: dfl.form.Form
{
private:
	dfl.button.CheckBox checkBox1;
	dfl.button.CheckBox checkBox2;
	dfl.button.CheckBox checkBox3;
	
	bool red, yellow, green;
	
	void initializeArduinoControl()
	{
		
		formBorderStyle = dfl.all.FormBorderStyle.FIXED_SINGLE;
		maximizeBox = false;
		text = "Arduino Control";
		clientSize = dfl.all.Size(250, 172);
		
		checkBox1 = new dfl.button.CheckBox();
		checkBox1.name = "checkBox1";
		checkBox1.text = "Зеленый";
		checkBox1.bounds = dfl.all.Rect(16, 24, 144, 32);
		checkBox1.click ~= &this.onGREENClick;
		checkBox1.parent = this;
		
		checkBox2 = new dfl.button.CheckBox();
		checkBox2.name = "checkBox2";
		checkBox2.text = "Желтый";
		checkBox2.bounds = dfl.all.Rect(16, 64, 144, 32);
		checkBox2.click ~= &this.onYELLOWClick;
		checkBox2.parent = this;
		
		checkBox3 = new dfl.button.CheckBox();
		checkBox3.name = "checkBox3";
		checkBox3.text = "Красный";
		checkBox3.bounds = dfl.all.Rect(16, 104, 144, 32);
		checkBox3.click~= &this.onREDClick;
		checkBox3.parent = this;
	}
	
	void onREDClick(Object sender, EventArgs ea)
	{
		auto com = ClassicSingleton.getInstance("COM3");
		
		red = !red;
		
		if (red)
		{
			com.send(`R`);
		}
		else
		{
			com.send(`r`);
		}
		
		
	}
	
	void onYELLOWClick(Object sender, EventArgs ea)
	{
		auto com = ClassicSingleton.getInstance("COM3");
		
		yellow = !yellow;
		
		if (yellow)
		{
			com.send(`Y`);
		}
		else
		{
			com.send(`y`);
		}
		
	}
	
	void onGREENClick(Object sender, EventArgs ea)
	{
		auto com = ClassicSingleton.getInstance("COM3");
		
		green = !green;
		
		if (green)
		{
			com.send(`G`);
		}
		else
		{
			com.send(`g`);
		}
	}
	
	protected override void onClosed(EventArgs ea)
	{
		super.onClosed(ea);
	}
	
public:
	this()
	{
		initializeArduinoControl();
		ClassicSingleton.getInstance("COM3");
	}
}


void main()
{
	try
	{
		
		Application.enableVisualStyles();
		Application.run(new ArduinoControl);
		
	}
	catch(DflThrowable o)
	{
		msgBox(o.toString(), "Fatal Error", MsgBoxButtons.OK, MsgBoxIcon.ERROR);
	}
}

Резюме.

Таким образом, мы быстренько рассмотрели паттерн «Одиночка», который имеет смысл использовать, если:

  • используется существующая ОО-библиотека;
  • есть шансы, когда-нибудь один объект может превратится в несколько;
  • интерфейс объекта слишком сложен и не стоит засорять основное пространство имён большим количеством функций;
  • в зависимости от каких-нибудь условий и настроек, создаётся один из нескольких объектов.

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

Кроме того, традиционные минусы синглетных классов заключаются в том, что бывают ситуации, когда объект не стоит создавать или же когда объект содержит внутри себя что-то достаточно сложное и динамичное. Наличие синглетных классов в программе способно создать некоторое количество проблем, связанных с быстродействием и многопоточностью, и именно из-за этого, «одиночка» является самым спорным и проблемным паттерном из книги GoF.

В общем, если решите использовать паттерн, то постарайтесь все очень хорошенько обдумать, а на этом статья окончена, спасибо за внимание.

P.S: Большое спасибо Мэтту Вайсфелду (за потрясающее описание работы паттерна в его книге “Объектно-ориентированное мышление”), Эриху Гамме, Ричарду Хелму, Ральфу Джонсону, Джону Влиссидесу (за каталог паттернов и вообще идею), ну и Википедии, любезно предоставившей некоторый материал о достоинствах/недостатках паттерна и его категории.

Добавить комментарий