Singleton Pattern/ru

From Lazarus wiki
Jump to navigationJump to search


Введение

Этот шаблон считается одним из самых простых и, вероятно, наиболее обсуждаемым: синглтон. Что такого сложного в этом узоре? Что ж, независимо от языка, на котором вы его реализуете, вам придется делать некоторые нелогичные вещи, чтобы воспользоваться всеми преимуществами шаблона. У каждого языка программирования есть свои особенности этого шаблона, FreePascal не является исключением. В результате любая реализация может привести к обсуждению, и выбор, который вы сделаете для реализации, будет противоречить принципам другого программиста. Реализация этого шаблона никогда не бывает идеальной, включая подходы, представленные в этой статье. Тем не менее, я надеюсь, что эта статья будет для вас полезной.

Вот, что вы узнаете

  • Преимущества шаблона Singleton и почему его следует избегать
  • Глобальные переменные и переменные класса в FreePascal.
  • Как скрыть конструктор и сохранить его конфиденциальность

Информация к размышлению

Примеры и аргументы придумал не я. Примеры я нашел в обсуждениях на форумах и в их архивах. Эта статья представляет собой обзор дискуссии о синглтонах и коллекцию полезных примеров, которые я нашел.

Шаблон

Схема

The Singleton pattern

Паттерн Singleton схематично изображен на картинке выше. Класс Singleton определяет частную переменную класса, содержащую экземпляр Singleton. Метод класса GetInstance создает этот экземпляр один и только один раз и возвращает его клиенту. Метод конструктора Singleton() является закрытым и поэтому не виден клиенту. В книге «Шаблоны проектирования» перечислены следующие преимущества синглтона:

  • Контролируемый доступ к единственному экземпляру
  • Уменьшение пространств имен.
  • Разрешает уточнение операций и представления.
  • Разрешает переменное количество экземпляров (расширьте класс, чтобы разрешить контролируемое количество экземпляров, если дизайнер хочет разрешить такую ​​ситуацию)
  • работа с ним более гибкая, чем операции с классами.

Клиент-синглтон

Следующая программа будет типичным примером использования синглтона.

program TrueSingleton;
 
 {$mode objfpc}{$H+}
 
 uses
   {$IFDEF UNIX}{$IFDEF UseCThreads}
   cthreads,
   {$ENDIF}{$ENDIF}
   Classes, Singleton2
   { you can add units after this };
 
 {$R TrueSingleton.res}
 var
   s1, s2: TSingleton;
 
 begin
   s1:= TSingleton.GetInstance;
   s1.name := 'one';
   writeln('name of s1: '+s1.name);
 
   s2:= TSingleton.GetInstance;
   s2.name := 'two';
 
   writeln('name of s1: '+s1.name);
   writeln('name of s2: '+s2.name);
   //writeln('name of singleton: ' + Singleton.name);
   readln;
 end.

Это пример, когда синглтон работает по шаблону. Метод GetInstance — это метод класса, который возвращает единственный доступный экземпляр. Мы поможем нашему пользователю-программисту, если конструктор Create вообще не появится в автодополнении кода. Это помогает нашим пользователям помнить, что им следует использовать GetInstance. Однако проблема заключается в том, что FreePascal, в отличие от других языков, не требует конкретного имени в качестве конструктора. Init — такое же хорошее имя для конструктора, как и Create.

Правильный вывод будет выглядеть так:

name of s1: one
name of s1: two
name of s2: two

При создании правильного вывода код Singleton.GetInstance вернет экземпляр синглтона, а имена s1 и s2 всегда будут одинаковыми.

Неправильный вывод будет выглядеть так:

name of s1: one
name of s1: one
name of s2: two

Когда выдается неправильный вывод, создается второй экземпляр, имеющий собственное имя.

Синглон - это прекрасный шаблон, но старайтесь не использовать его

Или хотя бы подумайте, прежде чем использовать его. Есть много причин не использовать синглтон. На самом деле причин не использовать синглтон больше, чем причин использовать синглтон. См. разделы Недостатки и справочные разделы старой версии статьи WikiPedia, в которой обсуждаются синглтоны. Основные доводы:

  • Трудно проверить. Из-за своей природы синглтоны трудно заменить для тестирования.
  • Синглтоны делают зависимости неясными. Хотя синглтон исходит из другого модуля, в интерфейсе вашего объекта вы не можете увидеть, что синглтон используется. Ну, раздел «использование» дает подсказку, но не говорит, «где» именно оно используется.

Синглтоны обычно используются для общих ресурсов или 'движков':

  • Принтеров
  • Баз данных
  • Логгеров

Чтобы избежать использования слишком большого количества синглтонов, старайтесь избегать их и используйте во всем приложении только один. Например, вы можете захотеть создать набор инструментов (также известный как [Реестр]) для своего приложения. Теперь набор инструментов знает принтер, базу данных или логгер, и ваш код запрашивает экземпляры в наборе инструментов. Ваш код зависит от набора инструментов, а набор инструментов зависит от ресурсов, которые можно запросить. Это уменьшает количество зависимостей от большого количества синглтонов до одного.

Другая идея — использовать [Внедрение зависимостей]. Если объекту 'B' для правильной работы нужен другой объект 'A', передайте объект 'A' конструктору класса 'B'. По крайней мере, это ясно показывает, что класс B зависит от класса A в интерфейсе.

Чтобы улучшить тестирование вашего кода, вы можете захотеть, чтобы ваши синглтоны реализовали интерфейс. Затем метод или функция 'GetInstance' может вернуть экземпляр, соответствующий интерфейсу. В зависимости от чего-либо (директивы компиляции и т. д.) метод 'GetInstance' может возвращать 'реальный' объект или макет.

В оставшейся части статьи я расскажу, как создать синглтон.

Выход на глобальный уровень

In Your (Inter)Face

В Free Pascal довольно часто определяют глобальную переменную для шаблона Singleton. В исходном коде FreePascal и Lazarus вы найдете примеры этого. Идея состоит в том, чтобы создать экземпляр при инициализации файла модуля и затем использовать его при необходимости. Программист-клиент должен просто знать, что существует глобальный экземпляр и что этот глобальный экземпляр следует использовать. Каждый программист пишет и читает полную документацию, поэтому обычно с этим проблем не возникает.

unit GlobalVariableSingleton;
 
 {$mode objfpc}{$H+}
 
 interface
 
 type
   TSingleton = class
     name: String;
     constructor create;
   end;
 
 var
   Singleton: TSingleton = nil;
 
 implementation
 {* Здесь что-то происходит *}
 
 initialization
   Singleton := TSingleton.Create;
 
 end.

Когда этот модуль загружается, создается экземпляр синглтона, который можно использовать. Инициализация происходит при запуске программы, поэтому синглтон присутствует всегда. Это нарушает сокращение пространства имен, пространство имен загрязняется глобальной переменной, ожидающей использования. Даже если программиста не интересует экземпляр, а интересуют только другие функции вашего устройства, экземпляр создается и будет использовать память. С другой стороны, нам же хотелось иметь синглтон, чтобы можно было его использовать, верно? Здесь нет никакой вероятности, что мы будем использовать экземпляр. Итак, почему бы не создать экземпляр при запуске приложения?

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

constructor TSingleton.Create;
   begin
     If Assigned(Singleton) then
       Raise Exception.Create('Пожалуйста, используйте глобальную переменную Singleton');
     inherited Create;
   end;

Однако такой конструктор предупреждает нас только во время выполнения и не помогает во время разработки. Альтернативой оператору 'if' может быть конструкция 'assert'. С помощью 'assert' обнаружение будет перенесено на время тестирования, что, как вы знаете, мы все делаем, и это происходит задолго до времени выполнения. Однако 'assert' не помогает даже во время выполнения, поскольку 'assert' не связано с приложением во время выполнения. Согласен, бывает, что не весь код всегда тестируется. В данном конструкторе программа как минимум ломается из-за необработанного исключения. Программист вашего singleton-клиента, вероятно, обойдет это неудобство, попробовав try ... except. Хотя программисты могут обойти эту проблему, их код не работает, так зачем им это делать?

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

program TrueSingleton;
 
 {$mode objfpc}{$H+}
 
 uses
   {$IFDEF UNIX}{$IFDEF UseCThreads}
   cthreads,
   {$ENDIF}{$ENDIF}
   Classes, GlobalVariableSingleton
   { you can add units after this };
 
 {$R TrueSingleton.res}
 var
   s1, s2: TSingleton;
 
 begin
   s1:= Singleton;
   s1.name := 'one';
   writeln('name of s1: '+s1.name);
 
   s2:= Singleton;
   s2.name := 'two';
 
   writeln('name of s1: '+s1.name);
   writeln('name of s2: '+s2.name);
   //writeln('name of singleton: ' + Singleton.name);
   readln;
 end.

Конечно, вместо присвоения глобальной переменной локальной переменной вы также можете напрямую использовать глобальную переменную.

Up Your Implementation

Обратите внимание, как в модуле GlobalVariableSingleton глобальная переменная определяется в разделе interface. Именно это делает переменную Singleton глобальной переменной. Если вы поместите переменную в раздел implementation, она будет доступна только для кода в разделе implementation. Таким образом, переменная, определенная в разделе implementation, действует аналогично переменной класса или пакета. Переменная implementation может использоваться всем кодом в модуле, но не за его пределами. Переменная interface является общедоступной, ее можно использовать внутри и снаружи модуля. Это используется в следующем коде.

unit GlobalFunctionSingleton;
 
 {$mode objfpc}{$H+}
 
 interface
 
 uses
   Classes, SysUtils; 
 type
   TSingleton = class
   public
     name : String;
     constructor Create;
   end;
 
 function GetSingleton : TSingleton;
 
 implementation
 var
   Singleton : TSingleton = nil;
 
   constructor TSingleton.Create;
   begin
     assert(Singleton=nil, 'Незаконное воссоздание Singleton.');
     inherited Create;
     Singleton := self;
   end;
 
 function GetSingleton : TSingleton;
 Begin
   If(Singleton = nil) then
     raise Exception.Create('Синглтон не создается во время инициализации.');
   Result := Singleton;
 end;
 
 initialization
   Singleton := TSingleton.Create;
 end.

В этом случае экземпляр синглтона не является глобальной переменной (он определен в разделе implementation) и не может использоваться вне раздела implementation. Однако функцию GetSingleton можно использовать вне устройства. Но в отличие от схемы шаблона - это не метод класса, а старая добрая глобальная функция. Преимущество по сравнению с предыдущей реализацией заключается в том, что функция GetSingleton может содержать больше функциональности, чем просто возврат одного экземпляра. Например, эту функцию можно использовать для раздачи экземпляров из пула одиночных объектов. Клиентский код выглядит следующим образом:

program TrueSingleton;
 
 {$mode objfpc}{$H+}
 
 uses
   {$IFDEF UNIX}{$IFDEF UseCThreads}
   cthreads,
   {$ENDIF}{$ENDIF}
   Classes, GlobalFunctionSingleton
   { you can add units after this };
 
 {$R TrueSingleton.res}
 var
   s1, s2: TSingleton;
 
 begin
   s1:= GetSingleton;
   s1.name := 'one';
   writeln('name of s1: '+s1.name);
 
   s2:= GetSingleton;
   s2.name := 'two';
 
   writeln('name of s1: '+s1.name);
   writeln('name of s2: '+s2.name);
   //writeln('name of singleton: ' + Singleton.name);
   readln;
 end.

Это работает, но я не видел такого применения ни в одном исходном коде.

Стильная реализация

Простой Singleton

Следующий код представляет очень простой синглтон.

unit Singleton1;
 
 {$mode objfpc}{$H+}
 
 interface
 
 type
   TSingleton = class
     name: String;
     constructor create;
   end;
 
 implementation
 
 var
   Singleton: TSingleton = nil;
 
 constructor TSingleton.Create;
 begin
   if not(assigned(Singleton)) then begin
     inherited;
     (*... выполняем инициализацию ...*)
     Singleton := self;
   end else begin
     self := Singleton;
   end;
 end;
 
 end.

В модуле Singleton1 обратите внимание на следующее:

  • Переменная Singleton определена в разделе implementation. Это делает переменную невидимой вне модуля.
  • Используется общедоступный конструктор.

Free Pascal не поддерживает переменные класса и свойства класса. С помощью ключевого слова class можно изменять только процедуры и функции. Определение 'статической' переменной в разделе implementation — это самое близкое к статической переменной, которое мы можем получить. При инициализации для этой переменной установлено значение nil, поэтому память еще не используется. Хотя эта реализация работает, self := Singleton; меня беспокоит. Я не пробовал, но предполагаю, что это допускает утечки памяти. Даже если это не допускает утечек памяти, это просто выглядит неправильно.

Почему в Паскале все по-другому

Тогда почему бы не реализовать синглтон так, как это показано на схеме шаблона? Ну, поехали:

unit Singleton2;
 
 {$mode objfpc}{$H+}
 
 interface
 
 uses
   Classes, SysUtils;
 
 type
   TSingleton = class
     private
       constructor Create;
     public
       name: String;
       class function GetInstance : TSingleton;
   end;
 
 implementation
 var
   Singleton : TSingleton = nil; 
 
 constructor TSingleton.Create;
 begin
   inherited Create;
 end;
 
 class function TSingleton.GetInstance : TSingleton;
 begin
   if Singleton = nil then
     Singleton := TSingleton.Create;
   Result := Singleton;
 end;
 
 end.

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

program TrueSingleton;
 
 {$mode objfpc}{$H+}
 
 uses
   {$IFDEF UNIX}{$IFDEF UseCThreads}
   cthreads,
   {$ENDIF}{$ENDIF}
   Classes, Singleton2
   { you can add units after this };
 
 {$R TrueSingleton.res}
 var
   s1, s2: TSingleton;
 
 begin
   s1:= TSingleton.Create;
   s1.name := 'one';
   writeln('name of s1: '+s1.name);
 
   s2:= TSingleton.Create;
   s2.name := 'two';
 
   writeln('name of s1: '+s1.name);
   writeln('name of s2: '+s2.name);
   //writeln('name of singleton: ' + Singleton.name);
   readln;
 end.

Трудно увидеть проблему. В модуле Singleton2 мы сделали конструктор приватным. Запускаем клиентский код, и компилятор жалуется, что конструктор не является публичным, но после этого программа работает и показывает, что код не работает как синглтон. Будут созданы два разных экземпляра. Из этого мы узнаем, что, хотя мы уменьшили видимость конструктора, конструктор TObject.Create все еще виден.

'Конструктор должен быть общедоступным' Singleton

Небольшое изменение имеет большое значение:

unit Singleton3;
 
 {$mode objfpc}{$H+}
 
 interface
 
 uses
   Classes, SysUtils;
 
 type
   TSingleton = class
     private
       constructor Init;
     public
       name: String;
       class function Create: TSingleton;
   end;
 
 implementation
 var
   Singleton : TSingleton = nil;
 
 constructor TSingleton.Init;
 begin
   inherited Create;
 end;
 
 class function TSingleton.Create: TSingleton;
 begin
   if Singleton = nil then
     Singleton := TSingleton.Init;
   Result := Singleton;
 end;
 
 end.

Конструктор Init предоставляет нам конструктор, который нам нужен, чтобы избежать незаконного сокрытия конструктора TObject. Конструктор Init также является приватным. Функция Create скрывает конструктор TObject.Create. Этот код работает. Используя вышеизложенное, мы контролируем доступ к синглтону. Мы также не загрязняем пространство имен глобальными переменными. Мы можем создавать столько дополнительных методов, сколько захотим. Кроме того, если мы объявим массив синглтонов в разделе implementation, мы сможем предоставить дополнительные, но ограниченные экземпляры синглтона. И нам не нужны дополнительные статические методы, кроме функции Create. Таким образом, достигаются все преимущества шаблона Singleton.

Типичный клиентский код теперь должен выглядеть следующим образом:

program TrueSingleton;
 
 {$mode objfpc}{$H+}
 
 uses
   {$IFDEF UNIX}{$IFDEF UseCThreads}
   cthreads,
   {$ENDIF}{$ENDIF}
   Classes, Singleton3
   { you can add units after this };
 
 {$R TrueSingleton.res}
 var
   s1, s2: TSingleton;
 
 begin
   s1:= TSingleton.Create;
   s1.name := 'one';
   writeln('name of s1: '+s1.name);
 
   s2:= TSingleton.Create;
   s2.name := 'two';
 
   writeln('name of s1: '+s1.name);
   writeln('name of s2: '+s2.name);
   //writeln('name of singleton: ' + Singleton.name);
   readln;
 end.

Обратите внимание, что эта программа использует метод Create, а не метод GetInstance. Но взгляните на определение класса еще раз. Create — это не конструктор, а метод класса, который возвращает экземпляр Singleton. Init — это конструктор.

Спрячьте это от меня

При компиляции компилятор предупреждает, что конструктор не является публичным методом. Итак, теперь вы предупреждены. Вы можете игнорировать предупреждение, потому что код действительно тот, который вам нужен. Вы также можете отключить предупреждение. Код сообщения для предупреждения 'Constructor should be public' — 3018. Чтобы скрыть вывод предупреждения, добавьте -vm3018 в командную строку компилятора. В Lazarus вы можете снять выделение сообщения {В меню Project (Проект) выберите Project Options(Параметры проекта). Затем в диалоговом окне Options for Project: (Параметры проекта:) откройте ветку Compiler Options (Параметры компилятора) на панели навигации и нажмите Messages (Сообщения). Теперь снимите флажок 'Warning: Constructor should be public' (Конструктор должен быть общедоступным) в списке сообщений компилятора.}

Обсуждение вариантов

В этой части мы уделим внимание некоторым обсуждениям.

Уменьшение области видимости конструктора

Конечно, есть причина, по которой конструктор должен быть публичным методом. Таким образом, компилятор Free Pascal выдает предупреждение, когда конструктор становится приватным методом. Однако почему FreePascal выдает предупреждение? Другие языки программирования без проблем допускают использование приватных конструкторов. Кроме того, это плохое поведение при программировании на Паскале, я не знаю ответа. Может быть, поскольку это предупреждение, то причина может заключаться в том, что если конструктор не является публичным методом, то нет возможности создать объект и какой смысл в классе, если вы не можете сделать из него объект? Резюмируя перечисленное, аргумент выглядит следующим образом:

Причина, по которой вы не можете снизить видимость конструктора или любого 
другого метода, если на то пошло... Интерфейс класса (раздел implementation), 
даже для Object, как в вашем примере, - это соглашение о том, как вы можете 
использовать этот класс и что это вам доступно.  Снижая видимость 
метода или конструктора в классе-потомке, вы нарушаете 
ранее определенное соглашение. Этого делать категорически нельзя, вот почему
это не разрешено.

Помощь во время проектирования против ошибок во время выполнения

Следующие аргументы могут привести вас к выбору модуля Singleton3 в качестве примера реализации:

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

Шаблон не соблюдается

Да, шаблон не соблюдается строго. Конструктор называется Init, тогда как во FreePascal существует соглашение о вызове конструктора Create. Затем поверх этого имени для статической функции используется Create вместо имени GetInstance. Следующие аргументы говорят в пользу наиболее приемлемой реализации:

  • Цель шаблона соблюдена и преимущества достигнуты.
  • Код довольно приятный и хорошо себя ведет. В коде нет встроенных прерываний выполнения и нет сомнительных конструкций.
  • Чтобы синглтон работал во FreePascal, пришлось пойти на некоторые компромиссы.

У реализаций есть проблема с многопоточностью и синхронизацией.

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

Реализация singleton-класса имеет проблему с независимостью от платформы

У синглтона никогда не может быть потомков. Что должно произойти, когда класс-потомок вызывает inherited Create? Также было бы совершенно неправильно заставлять синглтон происходить от чего-либо еще, кроме TObject.

Теперь рассмотрим независимость платформы. Посмотрите исходники FreePascal, где глобальная переменная используется для принтера или любого другого центрального ресурса. Эти глобальные переменные без исключения создаются в модулях, специфичных для платформы, а общая функциональность записывается в независимом от платформы модуле. Рассмотрим, как это следует сделать с помощью одноэлементного класса. (Я подожду здесь, пока вы не обнаружите, что иерархия синглтонов неизбежно приведет к путанице и не позволит создавать независимый от платформы код.)

Теперь, когда вы убеждены, мы можем легко сказать, что класс Singleton нельзя использовать с общими функциями, повторение функций Singleton для всех реализаций платформы приведет к путанице в обслуживании.

Заключение

Да, синглтоны возможны! Даже во FreePascal. Но синглтоны — не очень хорошая идея в независимом от платформы коде FreePascal. Независимо от того, используете ли вы класс или глобальную переменную, идеальной реализации синглтона не существует.