Ruby Language
Designmönster och formspråk i Ruby
Sök…
Singleton
Ruby Standard Library har en Singleton-modul som implementerar Singleton-mönstret. Det första steget i att skapa en Singleton-klass är att kräva och inkludera Singleton
modulen i en klass:
require 'singleton'
class Logger
include Singleton
end
Om du försöker instansera den här klassen som du vanligtvis gör i en vanlig klass NoMethodError
undantag från NoMethodError
. Konstruktören görs privat för att förhindra att andra instanser skapas av misstag:
Logger.new
#=> NoMethodError: private method `new' called for AppConfig:Class
För att få tillgång till instansen för den här klassen måste vi använda instance()
:
first, second = Logger.instance, Logger.instance
first == second
#=> true
Loggerexempel
require 'singleton'
class Logger
include Singleton
def initialize
@log = File.open("log.txt", "a")
end
def log(msg)
@log.puts(msg)
end
end
För att använda Logger
objektet:
Logger.instance.log('message 2')
Utan Singleton inkludera
Ovanstående singletonimplementeringar kan också göras utan att Singleton-modulen inkluderas. Detta kan uppnås med följande:
class Logger
def self.instance
@instance ||= new
end
end
vilket är en kortfattad notation för följande:
class Logger
def self.instance
@instance = @instance || Logger.new
end
end
Tänk dock på att Singleton-modulen är testad och optimerad, varför det är det bästa alternativet att implementera din singleton med.
Observatör
Observatörsmönstret är ett mönster för mjukvarudesign i vilket ett objekt (kallat subject
) upprätthåller en lista över dess beroende (kallas observers
) och meddelar dem automatiskt om tillståndsförändringar, vanligtvis genom att ringa en av deras metoder.
Ruby tillhandahåller en enkel mekanism för att implementera Observer-designmönstret. Modulen Observable
tillhandahåller logiken för att meddela abonnenten om eventuella förändringar i det observerbara objektet.
För att detta ska fungera måste den observerbara hävda att den har förändrats och meddela observatörerna.
Objekt som observeras måste implementera en update()
, som kommer att vara återuppringning för Observer.
Låt oss genomföra en liten chatt där användare kan prenumerera på användare och när en av dem skriver något får abonnenterna ett meddelande.
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
Producerar följande utgång:
# Computer says: No
# Computer says: No
Vi har utlöst metoden att write
på Moderator-klassen två gånger och meddelat dess prenumeranter, i detta fall bara en.
Ju fler prenumeranter vi lägger till, desto mer kommer förändringarna att spridas.
Dekoratormönster
Dekoratörsmönster lägger till beteenden utan att påverka andra objekt i samma klass. Dekoratormönstret är ett användbart alternativ till att skapa underklasser.
Skapa en modul för varje dekoratör. Denna strategi är mer flexibel än arv eftersom du kan blanda och matcha ansvar i fler kombinationer. Eftersom transparensen gör att dekoratörer kan kapslas rekursivt, tillåter det dessutom ett obegränsat antal ansvarsområden.
Anta att pizzaklassen har en kostnadsmetod som returnerar 300:
class Pizza
def cost
300
end
end
Representera pizza med ett extra skikt ostbristning och kostnaden ökar med 50. Det enklaste tillvägagångssättet är att skapa en PizzaWithCheese
underklass som returnerar 350 i kostnadsmetoden.
class PizzaWithCheese < Pizza
def cost
350
end
end
Därefter måste vi representera en stor pizza som lägger till 100 till kostnaden för en normal pizza. Vi kan representera detta med hjälp av en LargePizza-underklass av Pizza.
class LargePizza < Pizza
def cost
400
end
end
Vi kan också ha en ExtraLargePizza som lägger till en ytterligare kostnad på 15 till vår LargePizza. Om vi skulle tänka på att dessa pizzatyper skulle kunna serveras med ost, skulle vi behöva lägga till LargePizzaWithChese och ExtraLargePizzaWithCheese-underklasser. Vi slutar med totalt 6 klasser.
För att förenkla tillvägagångssättet använder du moduler för att dynamiskt lägga till beteende i Pizza-klassen:
Modul + förlängning + superdekoratör: ->
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
Ombud
Proxy-objekt används ofta för att säkerställa skyddad åtkomst till ett annat objekt, vilken intern affärslogik vi inte vill förorena med säkerhetskraven.
Anta att vi vill garantera att endast användare med specifika behörigheter kan komma åt resursen.
Proxy-definition: (det säkerställer att endast användare som faktiskt kan se reservationer kan konsumentreservation_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
Modeller och 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
Konsumenttjänst:
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
Testa:
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)
FÖRDELAR
- vi undviker alla ändringar i
ReservationService
när åtkomstbegränsningar ändras. - Vi blandar inte affärsrelaterade data (
date_from
,date_to
,reservations_count
) med domänrelaterade koncept (användarrättigheter) i tjänsten. - Konsument (
StatsService
) är också fri från behörighetsrelaterad logik
TÄNK
- Proxy-gränssnittet är alltid exakt samma som objektet som det döljer, så att användare som konsumerar tjänster som är inslagna av proxy inte ens var medvetna om proxy-närvaro.