实体

设计模式 / 整洁架构模式
image

//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


An entity holds a business rule of the shop and knows nothing about a database, a screen or the network, so it can be tested without them.
Read more

问题。商店对金额达到 200 的每笔订单减免 10%。如果这条规则写在网页里,又在数据库查询里写了一遍,两份副本很快就会不一致。要检查这条规则,就得启动网页或数据库。商店最重要的知识分散各处,难以测试。

思路。实体是一段数据,连同与它相关的业务规则:订单包含什么,总额如何计算,何时可以付款。它对数据库、界面或网络一无所知。整洁架构有一条主要规则,即依赖规则:外层代码可以了解内层代码,但内层代码从不了解外层代码。实体是最内层的代码,因此不依赖任何东西。

如何阅读示例。示例中有两个实体:订单行和订单本身。订单汇总各行,对 200 及以上的金额减免 10%,并计算应付金额。订单为空时,它拒绝付款。客户端代码构建含两个订单行的订单 1001,并打印其中的数字:总额 250,折扣 25,应付 225。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。

何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。

常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。