
# entity
class Order
attr_reader :id, :total
def initialize(id, total)
@id = id
@total = total
end
def payable
total >= 200 ? total - total / 10 : total
end
end
# gateway
module Catalog
def price(sku)
raise NotImplementedError, "#{self.class}#price"
end
end
# repository
module Orders
def save(order)
raise NotImplementedError, "#{self.class}#save"
end
def count
raise NotImplementedError, "#{self.class}#count"
end
end
# output port
module Report
def say(text)
raise NotImplementedError, "#{self.class}#say"
end
end
# use case: asks for its parts, never builds them
class PlaceOrder
def initialize(catalog, orders, report)
@catalog = catalog
@orders = orders
@report = report
@next_id = 1001
end
def place(items)
total = 0
items.each do |sku, count|
price = @catalog.price(sku)
unless price
@report.say("no #{sku}")
return
end
total += price * count
end
order = Order.new(@next_id, total)
@next_id += 1
@orders.save(order)
text = "order #{order.id} for #{order.payable}"
@report.say(text)
end
end
# adapters
class MemoryCatalog
include Catalog
def price(sku) = {"pen" => 30, "lamp" => 190}[sku]
end
class MemoryOrders
include Orders
def initialize
@rows = []
end
def save(order) = @rows << order
def count = @rows.size
end
class ConsoleReport
include Report
def say(text) = puts(text)
end
class SilentReport
include Report
def say(text) = nil
end
# composition root: the only place that sees every layer
# at once, and the only place where an adapter is built.
# the two ready builds are class methods, because the
# root also hands out the parts it has put together
class ShopApp
attr_reader :use_case, :orders
def initialize(catalog, orders, report)
@orders = orders
@use_case = PlaceOrder.new(catalog, orders, report)
end
def self.loud
new(MemoryCatalog.new, MemoryOrders.new,
ConsoleReport.new)
end
def self.quiet
new(MemoryCatalog.new, MemoryOrders.new,
SilentReport.new)
end
end
# client code
app = ShopApp.loud
app.use_case.place({"pen" => 2, "lamp" => 1})
app.use_case.place({"chair" => 1})
puts "saved #{app.orders.count}"
# a second root builds the same use case differently,
# and neither the scenario nor the entity knows it
night = ShopApp.quiet
night.use_case.place({"lamp" => 1})
night.use_case.place({"pen" => 2})
puts "saved at night #{night.orders.count}"
# no adapter is built outside the root: the client asks
# for a ready application and gets one, and the scenario
# it got is the same class in both builds
puts "use case #{app.use_case.class}"
same = app.use_case.class == night.use_case.class
puts "same class #{same}"
# printed: order 1001 for 225
# no chair
# saved 1
# saved at night 2
# use case PlaceOrder
# same class true
Корень композиции - единственное место, где слои соединяются и строятся адаптеры; остальное приложение только просит свои части.Read moreЗадача. Сценарию нужны каталог цен, хранилище заказов и способ выдать отчёт. Если сценарий сам создаёт эти части, он называет адаптеры внешнего слоя и зависит от них. Тест не может подставить ему другие части. А если каждая часть программы сама собирает свои части, никто не видит, как соединено приложение. Идея. Корень композиции - единственное место, где соединяются слои и создаются адаптеры. Адаптер - это внешний код, который реализует порт, то есть интерфейс, объявленный внутренним слоем. Сценарий только просит свои части через порты и никогда не создаёт их сам. Корень знает все слои сразу, и в этом его работа. Весь остальной код соблюдает правило зависимостей: внутренний код никогда не знает внешнего. Ни фреймворк, ни контейнер для этого не нужны: корень - обычный код в начале программы. Как читать пример. Сценарий просит три порта: шлюз для цен, репозиторий для заказов и выходной порт для отчётов. Во внешнем слое для них есть адаптеры: каталог и хранилище в памяти, отчёт, который печатает, и отчёт, который молчит. Пример собирает приложение дважды. В первой сборке отчёт печатает: клиентский код оформляет заказ 1001 с суммой к оплате 225 и заказ на неизвестный товар. Во второй сборке отчёт молчит, поэтому её заказы ничего не печатают. Сохранены ли они, видно только по числу заказов в хранилище. Сценарий в обеих сборках один и тот же. Когда применять. У каждого приложения один корень композиции, в самом начале; у теста может быть свой маленький корень с поддельными адаптерами. Короткому скрипту без портов корень не нужен. Частая ошибка. Создать адаптер внутри сценария, пусть даже один раз. Тогда сценарий зависит от внешнего слоя, а корень больше не показывает, как соединено приложение. |