
# entity: the rules of the shop speak with an error of
# their own, never with a bare string
class ShopRule < StandardError; end
# entity
class OrderLine
attr_reader :sku, :price, :count
def initialize(sku, price, count)
raise ShopRule, "bad count" if count <= 0
@sku = sku
@price = price
@count = count
# a line never changes after it is born
freeze
end
def cost = price * count
end
# entity
class Order
attr_reader :id
def initialize(id)
@id = id
@lines = []
@paid = false
end
def add(sku, price, count)
raise ShopRule, "order is paid" if @paid
@lines << OrderLine.new(sku, price, count)
self
end
# the inner list is never handed out as it is
def lines = @lines.dup.freeze
def size = @lines.size
def total = @lines.sum(&:cost)
# business rule: 10% off from 200
def discount = total >= 200 ? total / 10 : 0
def payable = total - discount
# business rule: an empty order is never paid
def pay
raise ShopRule, "empty order" if @lines.empty?
@paid = true
self
end
def paid? = @paid
end
# client code
# no database and no screen anywhere above
order = Order.new(1001)
order.add("pen", 30, 2)
order.add("lamp", 190, 1)
puts "order #{order.id}"
puts "lines #{order.size}"
puts "total #{order.total}"
puts "discount #{order.discount}"
puts "payable #{order.payable}"
order.pay
puts "paid #{order.paid?}"
# every refusal comes from the rules themselves:
# no storage and no screen took part in them
begin
order.add("pen", 30, 1)
rescue ShopRule => error
puts "refused: #{error.message}"
end
begin
Order.new(1002).pay
rescue ShopRule => error
puts "refused: #{error.message}"
end
begin
OrderLine.new("pen", 30, 0)
rescue ShopRule => error
puts "refused: #{error.message}"
end
# the list handed out is frozen, so nobody adds a line
# behind the back of the rules above
begin
order.lines << OrderLine.new("chair", 1, 1)
rescue FrozenError
puts "refused: lines are frozen"
end
# printed: order 1001
# lines 2
# total 250
# discount 25
# payable 225
# paid true
# refused: order is paid
# refused: empty order
# refused: bad count
# refused: lines are frozen
Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.Read moreЗадача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно. Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят. Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же. Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры. Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно. |