Ruby Language
Design Patterns e Idioms in Ruby
Ricerca…
Singleton
Ruby Standard Library ha un modulo Singleton che implementa il pattern Singleton. Il primo passo nella creazione di una classe Singleton è richiedere e includere il modulo Singleton
in una classe:
require 'singleton'
class Logger
include Singleton
end
Se provi a creare un'istanza di questa classe come faresti normalmente con una classe regolare, viene sollevata un'eccezione NoMethodError
. Il costruttore viene reso privato per impedire che altre istanze vengano create accidentalmente:
Logger.new
#=> NoMethodError: private method `new' called for AppConfig:Class
Per accedere all'istanza di questa classe, è necessario utilizzare l' instance()
:
first, second = Logger.instance, Logger.instance
first == second
#=> true
Esempio di logger
require 'singleton'
class Logger
include Singleton
def initialize
@log = File.open("log.txt", "a")
end
def log(msg)
@log.puts(msg)
end
end
Per utilizzare l'oggetto Logger
:
Logger.instance.log('message 2')
Senza Singleton include
Le suddette implementazioni singleton possono anche essere eseguite senza l'inclusione del modulo Singleton. Questo può essere ottenuto con il seguente:
class Logger
def self.instance
@instance ||= new
end
end
che è una notazione abbreviata per quanto segue:
class Logger
def self.instance
@instance = @instance || Logger.new
end
end
Tuttavia, tieni presente che il modulo Singleton è testato e ottimizzato, pertanto rappresenta l'opzione migliore per implementare il tuo singleton con.
Osservatore
Il modello di osservatore è un modello di progettazione software in cui un oggetto (chiamato subject
) mantiene un elenco dei suoi dipendenti (chiamati observers
) e li notifica automaticamente di eventuali cambiamenti di stato, di solito chiamando uno dei loro metodi.
Ruby fornisce un semplice meccanismo per implementare il modello di progettazione di Observer. Il modulo Observable
fornisce la logica per notificare all'abbonato eventuali modifiche nell'oggetto Observable.
Perché funzioni, l'osservabile deve affermare che è cambiato e avvisare gli osservatori.
Gli oggetti che osservano devono implementare un metodo update()
, che sarà il callback per l'Observer.
Implementiamo una piccola chat, in cui gli utenti possono iscriversi agli utenti e quando uno di loro scrive qualcosa, gli utenti ricevono una notifica.
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
Producendo il seguente output:
# Computer says: No
# Computer says: No
Abbiamo attivato il metodo di write
due volte nella classe Moderatore, notificando i suoi iscritti, in questo caso solo uno.
Più iscritti aggiungiamo, più le modifiche si propagheranno.
Decoratore
Il pattern Decorator aggiunge un comportamento agli oggetti senza influenzare altri oggetti della stessa classe. Il pattern decoratore è un'alternativa utile alla creazione di sottoclassi.
Crea un modulo per ogni decoratore. Questo approccio è più flessibile dell'ereditarietà perché puoi combinare le responsabilità in più combinazioni. Inoltre, poiché la trasparenza consente ai decoratori di essere annidati in modo ricorsivo, consente un numero illimitato di responsabilità.
Supponiamo che la classe Pizza abbia un metodo di costo che restituisce 300:
class Pizza
def cost
300
end
end
Rappresenta la pizza con uno strato aggiuntivo di scoppio di formaggio e il costo aumenta di 50. L'approccio più semplice consiste nel creare una sottoclasse di PizzaWithCheese
che restituisca 350 nel metodo di costo.
class PizzaWithCheese < Pizza
def cost
350
end
end
Successivamente, dobbiamo rappresentare una pizza grande che aggiunge 100 al costo di una pizza normale. Possiamo rappresentarlo utilizzando una sottoclasse LargePizza di Pizza.
class LargePizza < Pizza
def cost
400
end
end
Potremmo anche avere un ExtraLargePizza che aggiunge un ulteriore costo di 15 al nostro LargePizza. Se dovessimo considerare che questi tipi di pizza potevano essere serviti con formaggio, dovremmo aggiungere le sottoclassi LargePizzaWithChese e ExtraLargePizzaWithCheese. Finiremo con un totale di 6 classi.
Per semplificare l'approccio, utilizzare i moduli per aggiungere dinamicamente il comportamento alla classe Pizza:
Modulo + estensione + super decoratore: ->
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
delega
L'oggetto proxy viene spesso utilizzato per garantire l'accesso protetto a un altro oggetto, che la logica aziendale interna non vogliamo inquinare con i requisiti di sicurezza.
Supponiamo di voler garantire che solo l'utente con autorizzazioni specifiche possa accedere alla risorsa.
Definizione del proxy: (garantisce che solo gli utenti che effettivamente possono vedere le prenotazioni saranno in grado di prenotare il servizio di prenotazione)
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
Modelli e ReservationService:
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
Servizio al consumo:
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
Test:
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)
BENEFICI
- stiamo evitando qualsiasi modifica in
ReservationService
quando vengono modificate le restrizioni di accesso. - non stiamo mescolando i dati relativi al business (
date_from
,date_to
,reservations_count
) con concetti non collegati al dominio (permessi dell'utente) in servizio. - Anche Consumer (
StatsService
) è libero dalla logica relativa alle autorizzazioni
CAVEATS
- L'interfaccia proxy è sempre esattamente uguale all'oggetto che nasconde, quindi l'utente che utilizza il servizio fornito dal proxy non era nemmeno a conoscenza della presenza del proxy.