
//entity
class OrderLine {
//what the line is about is read by everyone, so it
//is a plain field. price and count carry the rule
//checked above, and a name with # is a real lock:
//nothing outside the class body can even mention it
#price;
#count;
constructor(sku, price, count) {
if (count <= 0) {
throw new Error("bad count " + sku);
}
this.sku = sku;
this.#price = price;
this.#count = count;
}
cost() {
return this.#price * this.#count;
}
}
//entity
class Order {
//private in TypeScript is a word for the compiler
//and a cast drops it; # is a lock the language keeps
#lines = [];
#paid = false;
constructor(id) {
this.id = id;
}
add(sku, price, count) {
if (this.#paid) {
throw new Error("order is paid");
}
this.#lines.push(new OrderLine(sku, price, count));
return this;
}
//the goods leave as a frozen copy, so a caller who
//pushes into them changes nothing here
goods() {
return Object.freeze(this.#lines.slice());
}
size() {
return this.#lines.length;
}
total() {
let sum = 0;
for (let line of this.#lines) {
sum += line.cost();
}
return sum;
}
//business rule: 10% off from 200
discount() {
let total = this.total();
return total >= 200 ? Math.trunc(total / 10) : 0;
}
payable() {
return this.total() - this.discount();
}
//business rule: an empty order is never paid
pay() {
if (this.#lines.length === 0) {
throw new Error("empty order");
}
this.#paid = true;
return this;
}
paid() {
return this.#paid;
}
}
//client code
//no database and no screen anywhere above
let order = new Order(1001);
order.add("pen", 30, 2);
order.add("lamp", 190, 1);
console.log("order " + order.id);
console.log("lines " + order.size());
console.log("total " + order.total());
console.log("discount " + order.discount());
console.log("payable " + order.payable());
order.pay();
console.log("paid " + order.paid());
//the same rule on a smaller order
let small = new Order(1002);
small.add("pen", 30, 2);
console.log("small total " + small.total());
console.log("small discount " + small.discount());
//the copy handed out refuses a new line loudly, and
//the order keeps its two either way
let taken = order.goods();
console.log("copy holds " + taken[0].sku +
" and " + taken[1].sku);
try {
taken.push(new OrderLine("chair", 1, 1));
} catch (error) {
console.log("refused: " + error.name + " on the copy");
}
console.log("still " + order.size());
//every refusal comes from the rules themselves:
//no storage and no screen took part in them
try {
order.add("pen", 30, 1);
} catch (error) {
console.log("refused: " + error.message);
}
try {
new Order(1003).pay();
} catch (error) {
console.log("refused: " + error.message);
}
try {
new OrderLine("pen", 30, 0);
} catch (error) {
console.log("refused: " + error.message);
}
//printed: order 1001
// lines 2
// total 250
// discount 25
// payable 225
// paid true
// small total 60
// small discount 0
// copy holds pen and lamp
// refused: TypeError on the copy
// still 2
// refused: order is paid
// refused: empty order
// refused: bad count pen
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。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。 何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。 常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。 |