
//entity: the rules say no with their own error type
enum OrderRule: Error {
case badCount
case emptyOrder
case orderPaid
var reason: String {
switch self {
case .badCount: return "bad count"
case .emptyOrder: return "empty order"
case .orderPaid: return "order already paid"
}
}
}
//entity
struct OrderLine {
let sku: String
let price: Int
let count: Int
//an invariant guarded where the data is born
init(sku: String, price: Int, count: Int) throws {
guard count > 0 else { throw OrderRule.badCount }
self.sku = sku
self.price = price
self.count = count
}
var cost: Int { price * count }
}
//entity
//a struct, not a class: no screen holding the order
//can change yesterday's order behind the rules' back
struct Order {
let id: Int
private(set) var lines: [OrderLine] = []
private(set) var paid = false
init(id: Int) {
self.id = id
}
var size: Int { lines.count }
var total: Int {
lines.reduce(0) { sum, line in sum + line.cost }
}
//business rule: 10% off from 200
var discount: Int {
total >= 200 ? total / 10 : 0
}
var payable: Int { total - discount }
//business rule: a paid order is closed for changes
mutating func add(_ sku: String, _ price: Int,
_ count: Int) throws {
guard !paid else { throw OrderRule.orderPaid }
lines.append(try OrderLine(sku: sku, price: price,
count: count))
}
//business rule: an empty order is never paid
mutating func pay() throws {
guard !lines.isEmpty else {
throw OrderRule.emptyOrder
}
paid = true
}
}
func refuse(_ work: () throws -> Void) {
do {
try work()
} catch let rule as OrderRule {
print("refused: \(rule.reason)")
} catch {
print("refused: \(error)")
}
}
//client code
//no database and no screen anywhere above
var order = Order(id: 1001)
try order.add("pen", 30, 2)
try order.add("lamp", 190, 1)
print("order \(order.id)")
print("lines \(order.size)")
print("total \(order.total)")
print("discount \(order.discount)")
print("payable \(order.payable)")
try order.pay()
print("paid \(order.paid)")
//a smaller order stays under the discount rule
var draft = Order(id: 1002)
try draft.add("pen", 30, 1)
print("order \(draft.id)")
print("total \(draft.total)")
print("discount \(draft.discount)")
//the rules refuse bad data, and nobody else has to
refuse { try order.add("pen", 30, 1) }
refuse { try draft.add("chair", 30, 0) }
refuse { var empty = Order(id: 1003); try empty.pay() }
//printed: order 1001
// lines 2
// total 250
// discount 25
// payable 225
// paid true
// order 1002
// total 30
// discount 0
// refused: order already paid
// refused: bad count
// refused: empty order
Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.Read moreЗадача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно. Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят. Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же. Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры. Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно. |