Ruby Language
Ontwerp patronen en idioom in Ruby
Zoeken…
eenling
Ruby Standard Library heeft een Singleton-module die het Singleton-patroon implementeert. De eerste stap bij het maken van een Singleton-klasse is het vereisen en opnemen van de Singleton
module in een klasse:
require 'singleton'
class Logger
include Singleton
end
Als u deze klasse probeert te instantiëren zoals u normaal een normale klasse zou doen, wordt een uitzondering NoMethodError
opgeworpen. De constructor wordt privé gemaakt om te voorkomen dat andere instanties per ongeluk worden gemaakt:
Logger.new
#=> NoMethodError: private method `new' called for AppConfig:Class
Om toegang te krijgen tot de instantie van deze klasse, moeten we de instance()
:
first, second = Logger.instance, Logger.instance
first == second
#=> true
Logger-voorbeeld
require 'singleton'
class Logger
include Singleton
def initialize
@log = File.open("log.txt", "a")
end
def log(msg)
@log.puts(msg)
end
end
Om het Logger
object te gebruiken:
Logger.instance.log('message 2')
Zonder Singleton bevatten
De bovenstaande singleton-implementaties kunnen ook worden gedaan zonder de Singleton-module op te nemen. Dit kan worden bereikt met het volgende:
class Logger
def self.instance
@instance ||= new
end
end
wat een korte notatie is voor het volgende:
class Logger
def self.instance
@instance = @instance || Logger.new
end
end
Houd er echter rekening mee dat de Singleton-module is getest en geoptimaliseerd, en daarom de betere optie is om uw singleton mee te implementeren.
Waarnemer
Het waarnemerspatroon is een patroon van softwareontwerp waarin een object ( subject
) een lijst bijhoudt van zijn afhankelijken ( observers
) en hen automatisch op de hoogte brengt van eventuele statuswijzigingen, meestal door een van hun methoden aan te roepen.
Ruby biedt een eenvoudig mechanisme om het ontwerppatroon van de waarnemer te implementeren. De module Observable
biedt de logica om de abonnee op de hoogte te stellen van eventuele wijzigingen in het Observable-object.
Om dit te laten werken, moet de waarnemer beweren dat het is veranderd en de waarnemers op de hoogte brengen.
Observerende objecten moeten een update()
-methode implementeren, die voor de waarnemer wordt teruggebeld.
Laten we een kleine chat implementeren, waar gebruikers zich op gebruikers kunnen abonneren en wanneer een van hen iets schrijft, krijgen de abonnees een melding.
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
Het produceren van de volgende output:
# Computer says: No
# Computer says: No
We hebben de methode write
in de Moderator-klasse twee keer geactiveerd en de abonnees hiervan op de hoogte gebracht, in dit geval slechts één.
Hoe meer abonnees we toevoegen, hoe meer de wijzigingen zich zullen verspreiden.
Decorateur Patroon
Decorateurpatroon voegt gedrag toe aan objecten zonder andere objecten van dezelfde klasse te beïnvloeden. Het decorateurpatroon is een nuttig alternatief voor het maken van subklassen.
Maak een module voor elke decorateur. Deze aanpak is flexibeler dan overerving omdat u verantwoordelijkheden in meer combinaties kunt combineren en matchen. Omdat decorators door de transparantie recursief kunnen worden genest, biedt het bovendien een onbeperkt aantal verantwoordelijkheden.
Stel dat de Pizza-klasse een kostenmethode heeft die 300 oplevert:
class Pizza
def cost
300
end
end
Vertegenwoordig pizza met een toegevoegde laag barsten van kaas en de kosten stijgen met 50. De eenvoudigste aanpak is om een PizzaWithCheese
subklasse te maken die in de PizzaWithCheese
350 PizzaWithCheese
.
class PizzaWithCheese < Pizza
def cost
350
end
end
Vervolgens moeten we een grote pizza vertegenwoordigen die 100 toevoegt aan de kosten van een normale pizza. We kunnen dit vertegenwoordigen met behulp van een LargePizza-subklasse van Pizza.
class LargePizza < Pizza
def cost
400
end
end
We kunnen ook een ExtraLargePizza hebben die nog eens 15 extra kost voor onze LargePizza. Als we zouden overwegen dat deze pizzasoorten met kaas kunnen worden geserveerd, moeten we de subklassen LargePizzaWithChese en ExtraLargePizzaWithCheese toevoegen. We eindigen met in totaal 6 klassen.
Om de aanpak te vereenvoudigen, gebruikt u modules om dynamisch gedrag toe te voegen aan de Pizza-klasse:
Module + uitbreiden + super decorateur: ->
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
volmacht
Proxy-object wordt vaak gebruikt om beveiligde toegang tot een ander object te garanderen, welke interne bedrijfslogica we niet willen vervuilen met veiligheidseisen.
Stel dat we willen garanderen dat alleen gebruikers met specifieke machtigingen toegang hebben tot bronnen.
Proxy-definitie: (het zorgt ervoor dat alleen gebruikers die daadwerkelijk reserveringen kunnen zien, consumer_service kunnen gebruiken)
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
Modellen en reserveringsservice:
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
Klantenservice:
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)
VOORDELEN
- we vermijden wijzigingen in
ReservationService
wanneer toegangsbeperkingen worden gewijzigd. - we
date_from
geen bedrijfsgerelateerde gegevens (date_from
,date_to
,reservations_count
) met domeingerelateerde concepten (gebruikersrechten) in de service. - Consumer (
StatsService
) is ook vrij van logica met betrekking tot machtigingen
WAARSCHUWINGEN
- De proxy-interface is altijd exact hetzelfde als het object dat het verbergt, zodat de gebruiker die service verpakt door proxy gebruikt niet eens op de hoogte was van de aanwezigheid van proxy.