Ruby Language
Patrones de diseño y modismos en Ruby
Buscar..
Semifallo
Ruby Standard Library tiene un módulo Singleton que implementa el patrón Singleton. El primer paso para crear una clase Singleton es requerir e incluir el módulo Singleton
en una clase:
require 'singleton'
class Logger
include Singleton
end
Si intenta crear una instancia de esta clase como lo haría normalmente en una clase normal, se NoMethodError
una excepción NoMethodError
. El constructor se hace privado para evitar que otras instancias se creen accidentalmente:
Logger.new
#=> NoMethodError: private method `new' called for AppConfig:Class
Para acceder a la instancia de esta clase, necesitamos usar la instance()
:
first, second = Logger.instance, Logger.instance
first == second
#=> true
Ejemplo de registrador
require 'singleton'
class Logger
include Singleton
def initialize
@log = File.open("log.txt", "a")
end
def log(msg)
@log.puts(msg)
end
end
Para usar el objeto Logger
:
Logger.instance.log('message 2')
Sin Singleton incluye
Las implementaciones singleton anteriores también se pueden realizar sin la inclusión del módulo Singleton. Esto se puede lograr con lo siguiente:
class Logger
def self.instance
@instance ||= new
end
end
que es una notación abreviada para lo siguiente:
class Logger
def self.instance
@instance = @instance || Logger.new
end
end
Sin embargo, tenga en cuenta que el módulo Singleton está probado y optimizado, por lo que es la mejor opción para implementar su Singleton.
Observador
El patrón de observador es un patrón de diseño de software en el que un objeto ( subject
llamado) mantiene una lista de sus dependientes (llamados observers
) y les notifica automáticamente cualquier cambio de estado, generalmente llamando a uno de sus métodos.
Ruby proporciona un mecanismo simple para implementar el patrón de diseño Observer. El módulo Observable
proporciona la lógica para notificar al suscriptor de cualquier cambio en el objeto Observable.
Para que esto funcione, el observable debe afirmar que ha cambiado y notificar a los observadores.
Los objetos que observan deben implementar un método update()
, que será la devolución de llamada para el observador.
Implementemos un pequeño chat, donde los usuarios pueden suscribirse a los usuarios y cuando uno de ellos escribe algo, los suscriptores reciben una notificación.
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
Produciendo la siguiente salida:
# Computer says: No
# Computer says: No
Hemos activado el método de write
en la clase Moderador dos veces, notificando a sus suscriptores, en este caso solo uno.
Cuantos más suscriptores agreguemos, más se propagarán los cambios.
Patrón decorador
El patrón de decorador agrega comportamiento a los objetos sin afectar a otros objetos de la misma clase. El patrón decorador es una alternativa útil para crear subclases.
Crea un módulo para cada decorador. Este enfoque es más flexible que la herencia porque puede combinar responsabilidades en más combinaciones. Además, debido a que la transparencia permite que los decoradores se aniden de forma recursiva, permite un número ilimitado de responsabilidades.
Supongamos que la clase de pizza tiene un método de costo que devuelve 300:
class Pizza
def cost
300
end
end
Represente la pizza con una capa adicional de ráfaga de queso y el costo aumenta en 50. El enfoque más simple es crear una subclase de PizzaWithCheese
que devuelva 350 en el método de costo.
class PizzaWithCheese < Pizza
def cost
350
end
end
A continuación, debemos representar una pizza grande que agregue 100 al costo de una pizza normal. Podemos representarlo usando una subclase LargePizza de Pizza.
class LargePizza < Pizza
def cost
400
end
end
También podríamos tener un ExtraLargePizza que agrega un costo adicional de 15 a nuestro LargePizza. Si tuviéramos que considerar que estos tipos de pizza podrían servirse con queso, tendríamos que agregar LargePizzaWithChese y ExtraLargePizzaWithCheese subclasses. Terminamos con un total de 6 clases.
Para simplificar el enfoque, use módulos para agregar dinámicamente el comportamiento a la clase Pizza:
Módulo + extender + super decorador: ->
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
Apoderado
El objeto proxy se usa a menudo para garantizar el acceso protegido a otro objeto, cuya lógica empresarial interna no queremos contaminar con los requisitos de seguridad.
Supongamos que queremos garantizar que solo los usuarios con permisos específicos puedan acceder a los recursos.
Definición de proxy: (garantiza que solo los usuarios que realmente puedan ver las reservas puedan reservar el servicio al consumidor)
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
Modelos y servicio de reserva:
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
Servicio al consumidor:
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
Prueba:
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)
BENEFICIOS
- estamos evitando cualquier cambio en
ReservationService
cuando se cambian las restricciones de acceso. - no estamos mezclando datos relacionados con la empresa (
date_from
,date_to
,reservations_count
) con conceptos de dominio no relacionados (permisos de usuario) en servicio. - El consumidor (
StatsService
) también está libre de la lógica relacionada con los permisos
CUEVAS
- La interfaz de proxy es siempre exactamente la misma que el objeto que oculta, por lo que el usuario que consume el servicio envuelto por el proxy ni siquiera estaba al tanto de la presencia del proxy.