Ruby Language
Design Patterns and Idioms in Ruby
Recherche…
Singleton
Ruby Standard Library possède un module Singleton qui implémente le modèle Singleton. La première étape de la création d'une classe Singleton consiste à exiger et à inclure le module Singleton
dans une classe:
require 'singleton'
class Logger
include Singleton
end
Si vous essayez d'instancier cette classe comme vous le feriez normalement avec une classe normale, une exception NoMethodError
est NoMethodError
. Le constructeur est rendu privé pour empêcher que d'autres instances soient créées accidentellement:
Logger.new
#=> NoMethodError: private method `new' called for AppConfig:Class
Pour accéder à l'instance de cette classe, nous devons utiliser l' instance()
:
first, second = Logger.instance, Logger.instance
first == second
#=> true
Exemple de Logger
require 'singleton'
class Logger
include Singleton
def initialize
@log = File.open("log.txt", "a")
end
def log(msg)
@log.puts(msg)
end
end
Pour utiliser l'objet Logger
:
Logger.instance.log('message 2')
Sans Singleton inclure
Les implémentations singleton ci-dessus peuvent également être effectuées sans l'inclusion du module Singleton. Cela peut être réalisé avec les éléments suivants:
class Logger
def self.instance
@instance ||= new
end
end
qui est une notation abrégée pour les éléments suivants:
class Logger
def self.instance
@instance = @instance || Logger.new
end
end
Cependant, gardez à l'esprit que le module Singleton est testé et optimisé, ce qui en fait la meilleure option pour implémenter votre singleton.
Observateur
Le modèle d'observateur est un modèle de conception logicielle dans lequel un objet (appelé subject
) conserve une liste de ses dépendants (appelés observers
) et les avertit automatiquement de tout changement d'état, généralement en appelant l'une de ses méthodes.
Ruby fournit un mécanisme simple pour implémenter le modèle de conception Observer. Le module Observable
fournit la logique pour informer l'abonné de tout changement dans l'objet Observable.
Pour que cela fonctionne, l'observable doit affirmer qu'il a changé et en informer les observateurs.
Les objets observant doivent implémenter une méthode update()
, qui sera le rappel de l'observateur.
Implémentons une petite discussion, où les utilisateurs peuvent s'abonner aux utilisateurs et quand l'un d'eux écrit quelque chose, les abonnés sont avertis.
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
Produire la sortie suivante:
# Computer says: No
# Computer says: No
Nous avons déclenché la méthode write
à la classe Moderator deux fois, en avertissant ses abonnés, dans ce cas-ci un seul.
Plus nous ajoutons d'abonnés, plus les changements se propageront.
Motif Décorateur
Le motif de décorateur ajoute un comportement aux objets sans affecter les autres objets de la même classe. Le motif de décorateur est une alternative utile à la création de sous-classes.
Créez un module pour chaque décorateur. Cette approche est plus flexible que l'héritage, car vous pouvez combiner et associer des responsabilités dans davantage de combinaisons. De plus, la transparence permet aux décorateurs d’être imbriqués récursivement, ce qui permet un nombre illimité de responsabilités.
Supposons que la classe Pizza a une méthode de coût qui renvoie 300:
class Pizza
def cost
300
end
end
Représenter la pizza avec une couche supplémentaire de fromage et le coût augmente de 50. L'approche la plus simple consiste à créer une sous-classe PizzaWithCheese
qui renvoie 350 dans la méthode du coût.
class PizzaWithCheese < Pizza
def cost
350
end
end
Ensuite, nous devons représenter une grande pizza qui ajoute 100 au coût d'une pizza normale. Nous pouvons représenter cela en utilisant une sous-classe LargePizza de Pizza.
class LargePizza < Pizza
def cost
400
end
end
Nous pourrions également avoir un ExtraLargePizza qui ajoute un coût supplémentaire de 15 à notre LargePizza. Si nous considérions que ces types de pizza pouvaient être servis avec du fromage, nous devions ajouter les sous-classes LargePizzaWithChese et ExtraLargePizzaWithCheese.
Pour simplifier l'approche, utilisez des modules pour ajouter dynamiquement un comportement à la classe Pizza:
Module + extend + super décorateur: ->
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
Procuration
L'objet proxy est souvent utilisé pour garantir un accès sécurisé à un autre objet, quelle logique métier interne nous ne voulons pas polluer avec les exigences de sécurité.
Supposons que nous souhaitons garantir que seul l'utilisateur des autorisations spécifiques puisse accéder aux ressources.
Définition du proxy: (il garantit que seuls les utilisateurs capables de voir les réservations pourront utiliser le service reservation_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
Modèles et service de réservation:
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
Service client:
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
Tester:
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)
AVANTAGES
- nous évitons tout changement dans
ReservationService
lorsque les restrictions d'accès sont modifiées. - Nous ne
date_from
pas les données liées à l'entreprise (date_from
,date_to
,reservations_count
) avec les concepts non liés au domaine (autorisations utilisateur) en service. - Consumer (
StatsService
) est également exempt de logique liée aux autorisations
CAVEATS
- L'interface proxy est toujours exactement identique à l'objet qu'elle cache, de sorte que l'utilisateur qui consomme le service encapsulé par un proxy n'était même pas au courant de la présence de proxy.