Ruby Language
Rubyのデザインパターンとイディオム
サーチ…
シングルトン
Ruby標準ライブラリには、シングルトンパターンを実装するSingletonモジュールがあります。 Singletonクラスを作成するための最初のステップは、クラスにSingleton
モジュールを必要とすることです。
require 'singleton'
class Logger
include Singleton
end
通常のクラスと同じようにこのクラスをインスタンス化しようとすると、 NoMethodError
例外が発生します。他のインスタンスが誤って作成されないように、コンストラクタはプライベートにします。
Logger.new
#=> NoMethodError: private method `new' called for AppConfig:Class
このクラスのインスタンスにアクセスするには、 instance()
を使用する必要がありinstance()
。
first, second = Logger.instance, Logger.instance
first == second
#=> true
ロガーの例
require 'singleton'
class Logger
include Singleton
def initialize
@log = File.open("log.txt", "a")
end
def log(msg)
@log.puts(msg)
end
end
Logger
オブジェクトを使用するには:
Logger.instance.log('message 2')
シングルトンを含まない場合
上記のシングルトン実装は、シングルトンモジュールを含めることなく行うこともできます。これは次のようにして実現できます。
class Logger
def self.instance
@instance ||= new
end
end
これは次の表記法です。
class Logger
def self.instance
@instance = @instance || Logger.new
end
end
しかし、Singletonモジュールはテストされ最適化されているため、シングルトンを実装するためのより良いオプションであることに注意してください。
観察者
オブザーバー・パターンは、オブジェクト( subject
と呼ばれる)がその従属物( observers
と呼ばれる)のリストを保持し、通常はそれらのメソッドの1つを呼び出すことによって、どのような状態の変化も自動的に通知するソフトウェア設計パターンです。
RubyはObserverデザインパターンを実装するための簡単なメカニズムを提供します。 Observable
モジュールは、Observableオブジェクトの変更をサブスクライバに通知するロジックを提供します。
これが機能するためには、観測者はそれが変更されたと主張し、オブザーバーに通知しなければならない。
観察するオブジェクトは、Observerのコールバックになるupdate()
メソッドを実装する必要があります。
小さなチャットを実装して、ユーザーがユーザーを登録できるようにしましょう。そのユーザーの1人が何かを書くと、そのユーザーに通知が届きます。
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
次の出力を生成します。
# Computer says: No
# Computer says: No
Moderatorクラスのメソッドwrite
を2回トリガして、サブスクライバ(この場合は1つ)に通知します。
追加する加入者が増えるほど、変更が広がります。
デコレータパターン
Decoratorパターンは、同じクラスの他のオブジェクトに影響を与えることなく、オブジェクトに動作を追加します。デコレータパターンは、サブクラスを作成するのに便利です。
デコレータごとにモジュールを作成します。このアプローチは継承よりも柔軟性があります。なぜなら、より多くの組み合わせで責任を混在させることができるからです。さらに、透過性により、デコレータを再帰的にネストすることができるため、無制限の数の責任が可能になります。
ピザクラスに300を返すコストメソッドがあるとします。
class Pizza
def cost
300
end
end
チーズバーストが追加されたピザをPizzaWithCheese
、コストは50 PizzaWithCheese
します。最も簡単な方法は、コストメソッドで350を返すPizzaWithCheese
サブクラスを作成することです。
class PizzaWithCheese < Pizza
def cost
350
end
end
次に、通常のピザのコストに100を加えた大きなピザを表現する必要があります。ピザのLargePizzaサブクラスを使用して表現できます。
class LargePizza < Pizza
def cost
400
end
end
また、LargePizzaにさらに15のコストを追加するExtraLargePizzaを用意することもできます。これらのピザタイプにチーズを添えることができたら、LargePizzaWithCheseとExtraLargePizzaWithCheeseサブクラスを追加する必要があります。合計で6つのクラスがあります。
アプローチを簡素化するために、モジュールを使用してPizzaクラスにビヘイビアを動的に追加します。
モジュール+拡張+スーパーデコレータ: - >
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
プロキシ
プロキシオブジェクトは、他のオブジェクトへのアクセスを確実に保護するためによく使用されます。内部的なビジネスロジックは、安全上の必要性によって汚染されたくありません。
特定のアクセス許可のユーザーだけがリソースにアクセスできることを保証したいとします。
プロキシ定義:(実際には予約を見ることができるユーザーだけがconsumer_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
モデルと予約サービス:
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
消費者サービス:
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
テスト:
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)
利点
- アクセス制限が変更されたときの
ReservationService
変更は避けています。 - 私たちはビジネス関連のデータ(
date_from
、date_to
、reservations_count
)をドメインの無関係な概念(ユーザー権限)と組み合わせてサービスしていません。 - Consumer(
StatsService
)にはパーミッション関連のロジックもありStatsService
警告
- プロキシインターフェイスは、常に非表示のオブジェクトとまったく同じです。したがって、プロキシによってラップされたサービスを使用するユーザーは、プロキシの存在を認識していませんでした。