Статьи

Шаблоны проектирования в Ruby: Observer, Singleton

obsing Я собираюсь опубликовать несколько статей, касающихся шаблонов разработки программного обеспечения и их применения в Ruby. Первые два паттерна, которые будут рассмотрены, это паттерн Observer и паттерн Singleton .

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

Если вы не знакомы с этим шаблоном, не беспокойтесь, это один из механизмов для информирования других «заинтересованных» объектов об изменении его состояния. Чтобы быть немного более наглядным, вот прямая цитата из Википедии :

Шаблон наблюдателя (он же Dependents, publish / subscribe) — это шаблон разработки программного обеспечения, в котором объект, называемый субъектом, ведет список своих зависимых объектов, называемых наблюдателями, и автоматически уведомляет их о любых изменениях состояния, обычно вызывая одно из их методы. Он в основном используется для реализации распределенных систем обработки событий.

Модуль Observable в стандартной библиотеке Ruby предоставляет механизм, необходимый для реализации этого шаблона, и его довольно просто использовать.

Планирование

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

Основная структура

Первое, что мы собираемся сделать, — это создать базовую структуру для нашего класса Notifier, который будет выступать в качестве наблюдателя. Одним из пунктов, на который вам действительно необходимо обратить внимание, является метод update () . Это обратный вызов, который модуль Observable будет использовать при уведомлении об изменениях в наблюдателе, и имя метода необходимо обновить () .

Давайте начнем с создания простого класса Notifier :

class Notifier def update() end end 

Это так просто, как только может. Далее, давайте создадим структуру для предмета, нашего класса автомобилей :

 class Car attr_reader :mileage, :service def initialize(mileage = 0, service = 3000) @mileage, @service = mileage, service end def log(miles) @mileage += miles end end 

Он содержит атрибуты пробег и сервис , а также методы initialize () и log () . Метод initialize () устанавливает начальные значения для текущего пробега автомобиля и пробега, который необходимо доставить в сервис. Метод log () запишет, сколько миль он проехал за последнее время, и добавит его к общему пробегу автомобиля.

Исправление класса уведомителя

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

 class Notifier def update(car, miles) puts "The car has logged #{miles} miles, totaling #{car.mileage} miles traveled." puts "The car needs to be taken in for a service!" if car.service <= car.mileage end end 

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

Собираем все вместе

После завершения базовой структуры для класса Car и создания класса Notifier нам остается только установить модуль Observable .

Во-первых, нам нужно включить его в наш класс автомобилей :

 require 'observer' class Car include Observable ... end 

Далее мы добавим наблюдателя каждый раз, когда создается новый экземпляр класса Car . Давайте изменим метод инициализации :

 def initialize(mileage = 0, service = 3000) @mileage, @service = mileage, service add_observer(Notifier.new) end 

Последнее изменение, которое нам нужно сделать в этом классе, — в нашем методе log . Нам нужно сообщить наблюдателю, что наш объект меняется каждый раз, когда мы регистрируем новые мили:

 def log(miles) @mileage += miles changed notify_observers(self, miles) end 

Здесь мы называем changed , который устанавливает измененное состояние объекта (по умолчанию true). И notify_observers(self, miles) , который уведомляет наблюдателя об изменении.

Полный класс автомобилей выглядит следующим образом:

 require 'observer' class Car include Observable attr_reader :mileage, :service def initialize(mileage = 0, service = 3000) @mileage, @service = mileage, service add_observer(Notifier.new) end def log(miles) @mileage += miles changed notify_observers(self, miles) end end 

Подводя итог, вот изменения, которые мы внесли в наш класс автомобилей :

  • Для того, чтобы использовать модуль Observable , нам сначала нужно это сделать;
  • Далее мы включаем его с помощью include Observable ;
  • Когда создается экземпляр нашего класса Car , добавляется наблюдатель;
  • Когда вызывается метод log () , он уведомляет наблюдателей, утверждая, что объект изменился

Выполнение кода

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

 car = Car.new(2300, 3000) car.log(100) => "The car has logged 100 miles, totaling 2400 miles traveled." car.log(354) => "The car has logged 354 miles, totaling 2754 miles traveled." car.log(300) => "The car has logged 300 miles, totaling 3054 miles traveled." => "The car needs to be taken in for service!" 

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

Каждый раз, когда регистрируется новый пробег, класс Notifier выводит добавленный пробег, а также подсчет пройденных миль. Когда общее количество миль за автомобиль превышает пробег, установленный для обслуживания, генерируется другой результат. Довольно круто, а?

Синглтон

Концепция паттерна Singleton довольно проста: может существовать только один экземпляр класса. Это может быть полезно, когда приложение позволяет создавать только один объект для данного класса.

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

планирование

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

интеграция

Первым шагом в создании класса Singleton является require и include модуля Singleton в класс:

 require 'singleton' class AppConfig include Singleton end 

Если вы попытаетесь создать экземпляр этого класса, как это обычно NoMethodError в обычном классе, NoMethodError исключение NoMethodError . Конструктор сделан приватным, чтобы предотвратить случайное создание других экземпляров:

 AppConfig.new #=> NoMethodError: private method `new' called for AppConfig:Class 

Чтобы получить доступ к экземпляру этого класса, нам нужно использовать метод instance() предоставленный модулем Singleton. Когда этот метод вызывается впервые, создается экземпляр класса, и все последующие вызовы возвращают созданный экземпляр. Любопытно посмотреть, правда ли это на самом деле?

 first, second = AppConfig.instance, AppConfig.instance first == second #=> true 

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

 #... attr_accessor :data def version '1.0.0' end 

Здесь мы добавили атрибут data который будет содержать данные о конфигурации, и метод version который возвращает текущую версию. Собираем все вместе, вот полный класс:

 require 'singleton' class AppConfig include Singleton attr_accessor :data def version '1.0.0' end end 

Поздравляем, вы только что реализовали шаблон Singleton в Ruby! Теперь давайте поиграем с этим:

 AppConfig.instance.data = {enabled: true} => {:enabled=>true} AppConfig.instance.version => "1.0.0" second = AppConfig.instance second.data = {enabled: false} => {:enabled=>false} AppConfig.instance.data => {:enabled=>false} 

Сначала мы устанавливаем атрибут data с произвольными значениями и проверяем его версию. Далее мы дублируем экземпляр singleton, меняем его значение data и подтверждаем, что значение изменилось в одном экземпляре.

Вывод

В этой статье демонстрируется, как шаблон Observer и шаблон Singleton можно использовать с Ruby, и я надеюсь, что представленные здесь примеры могут послужить основой для их реализации в ваших собственных приложениях.

(Примечание: диаграммы на картинке для этой статьи были сделаны с помощью http://yuml.me )