Ruby Language
Шаблоны дизайна и идиомы в Ruby
Поиск…
одиночка
Стандартная библиотека Ruby имеет модуль Singleton, который реализует шаблон Singleton. Первым шагом в создании класса Singleton является требование и включение модуля Singleton
в класс:
require 'singleton'
class Logger
include Singleton
end
Если вы попытаетесь создать экземпляр этого класса, как обычно, будет обычный класс, NoMethodError
исключение NoMethodError
. Конструктор закрывается, чтобы предотвратить случайное создание других экземпляров:
Logger.new
#=> NoMethodError: private method `new' called for AppConfig:Class
Чтобы получить доступ к экземпляру этого класса, нам нужно использовать instance()
:
first, second = Logger.instance, Logger.instance
first == second
#=> true
Пример регистратора
require 'singleton'
class Logger
include Singleton
def initialize
@log = File.open("log.txt", "a")
end
def log(msg)
@log.puts(msg)
end
end
Чтобы использовать объект Logger
:
Logger.instance.log('message 2')
Без Singleton включают
Вышеупомянутые одноэлементные реализации также могут быть выполнены без включения модуля Singleton. Этого можно достичь с помощью следующего:
class Logger
def self.instance
@instance ||= new
end
end
который является сокращенным обозначением для следующего:
class Logger
def self.instance
@instance = @instance || Logger.new
end
end
Однако имейте в виду, что модуль Singleton проверен и оптимизирован, поэтому является лучшим вариантом для реализации вашего синглтона.
наблюдатель
Шаблон наблюдателя представляет собой шаблон разработки программного обеспечения, в котором объект (называемый subject
) поддерживает список своих иждивенцев (называемых observers
) и автоматически уведомляет их о любых изменениях состояния, как правило, путем вызова одного из своих методов.
Ruby предоставляет простой механизм для реализации шаблона проектирования Observer. Модуль Observable
предоставляет логику для уведомления абонента о любых изменениях объекта Observable.
Чтобы это работало, наблюдаемое должно было утверждать, что оно изменилось и уведомило наблюдателей.
Наблюдение объектов должно реализовать метод update()
, который будет обратным вызовом для Observer.
Давайте реализовываем небольшой чат, где пользователи могут подписаться на пользователей, а когда один из них что-то пишет, подписчики получают уведомление.
require "observer"
class Moderator
include Observable
def initialize(name)
@name = name
end
def write
message = "Computer says: No"
changed
notify_observers(message)
end
end
class Warner
def initialize(moderator, limit)
@limit = limit
moderator.add_observer(self)
end
end
class Subscriber < Warner
def update(message)
puts "#{message}"
end
end
moderator = Moderator.new("Rupert")
Subscriber.new(moderator, 1)
moderator.write
moderator.write
Производя следующий вывод:
# Computer says: No
# Computer says: No
Мы вызвали метод write
в классе Moderator дважды, уведомляя его подписчиков, в этом случае только один.
Чем больше подписчиков мы добавим, тем больше будут распространяться изменения.
Шаблон декоратора
Шаблон Decorator добавляет поведение к объектам, не затрагивая другие объекты того же класса. Шаблон декоратора является полезной альтернативой созданию подклассов.
Создайте модуль для каждого декоратора. Этот подход более гибкий, чем наследование, потому что вы можете смешивать и сопоставлять обязанности в большем количестве комбинаций. Кроме того, поскольку прозрачность позволяет декораторам быть вложенными рекурсивно, это допускает неограниченное количество обязанностей.
Предположим, что класс Pizza имеет метод стоимости, который возвращает 300:
class Pizza
def cost
300
end
end
Представляйте пиццу с добавленным слоем сыра, и стоимость увеличивается на 50. Самый простой подход - создать подкласс PizzaWithCheese
который возвращает 350 в методе стоимости.
class PizzaWithCheese < Pizza
def cost
350
end
end
Затем нам нужно представить большую пиццу, которая добавит 100 к стоимости обычной пиццы. Мы можем представить это с помощью подкласса LargePizza Pizza.
class LargePizza < Pizza
def cost
400
end
end
У нас также может быть ExtraLargePizza, который добавляет дополнительную стоимость 15 к нашей LargePizza. Если бы мы считали, что эти типы пиццы могут быть поданы с сыром, нам нужно будет добавить подклассы LargePizzaWithChese и ExtraLargePizzaWithCheese. В итоге в итоге будет 6 классов.
Чтобы упростить подход, используйте модули для динамического добавления поведения в класс Pizza:
Модуль + удлинитель + супер-декоратор: ->
class Pizza
def cost
300
end
end
module CheesePizza
def cost
super + 50
end
end
module LargePizza
def cost
super + 100
end
end
pizza = Pizza.new #=> cost = 300
pizza.extend(CheesePizza) #=> cost = 350
pizza.extend(LargePizza) #=> cost = 450
pizza.cost #=> cost = 450
полномочие
Объект прокси часто используется для обеспечения защищенного доступа к другому объекту, внутренняя бизнес-логика которого мы не хотим загрязнять с требованиями безопасности.
Предположим, мы хотели бы гарантировать, что только пользователь определенных разрешений может получить доступ к ресурсу.
Определение прокси: (он гарантирует, что только пользователи, которые действительно могут видеть оговорки, смогут пользоваться услугой customer_service)
class Proxy
def initialize(current_user, reservation_service)
@current_user = current_user
@reservation_service = reservation_service
end
def highest_total_price_reservations(date_from, date_to, reservations_count)
if @current_user.can_see_reservations?
@reservation_service.highest_total_price_reservations(
date_from,
date_to,
reservations_count
)
else
[]
end
end
end
Модели и резервированиеСервис:
class Reservation
attr_reader :total_price, :date
def initialize(date, total_price)
@date = date
@total_price = total_price
end
end
class ReservationService
def highest_total_price_reservations(date_from, date_to, reservations_count)
# normally it would be read from database/external service
reservations = [
Reservation.new(Date.new(2014, 5, 15), 100),
Reservation.new(Date.new(2017, 5, 15), 10),
Reservation.new(Date.new(2017, 1, 15), 50)
]
filtered_reservations = reservations.select do |reservation|
reservation.date.between?(date_from, date_to)
end
filtered_reservations.take(reservations_count)
end
end
class User
attr_reader :name
def initialize(can_see_reservations, name)
@can_see_reservations = can_see_reservations
@name = name
end
def can_see_reservations?
@can_see_reservations
end
end
Бытовое обслуживание:
class StatsService
def initialize(reservation_service)
@reservation_service = reservation_service
end
def year_top_100_reservations_average_total_price(year)
reservations = @reservation_service.highest_total_price_reservations(
Date.new(year, 1, 1),
Date.new(year, 12, 31),
100
)
if reservations.length > 0
sum = reservations.reduce(0) do |memo, reservation|
memo + reservation.total_price
end
sum / reservations.length
else
0
end
end
end
Тестовое задание:
def test(user, year)
reservations_service = Proxy.new(user, ReservationService.new)
stats_service = StatsService.new(reservations_service)
average_price = stats_service.year_top_100_reservations_average_total_price(year)
puts "#{user.name} will see: #{average_price}"
end
test(User.new(true, "John the Admin"), 2017)
test(User.new(false, "Guest"), 2017)
ВЫГОДЫ
- мы избегаем каких-либо изменений в
ReservationService
при изменении ограничений доступа. - мы не смешиваем данные, связанные с бизнесом (
date_from
,date_to
,reservations_count
) с не связанными с доменом понятиями (разрешениями пользователя) в сервисе. - Потребитель (
StatsService
) также свободен от связанной с разрешениями логики
ПРЕДОСТЕРЕЖЕНИЯ
- Интерфейс прокси всегда точно такой же, как и объект, который он скрывает, поэтому пользователь, который потребляет услугу, завернутый прокси-сервером, даже не знал о наличии прокси-сервера.