
//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 moreThe problem. A shop gives 10% off every order from 200. If this rule lives in a web page, and again in a database query, the two copies soon disagree. To check the rule, you must start the page or the database. The most important knowledge of the shop is scattered and hard to test. The idea. An entity is a piece of data together with the business rules about it: what an order contains, how its total is counted, when it may be paid. It knows nothing about a database, a screen or the network. Clean architecture has one main rule, the dependency rule: outer code may know inner code, but inner code never knows outer code. Entities are the innermost code, so they depend on nothing. How to read the example. There are two entities: a line of an order and the order itself. The order sums its lines, gives 10% off from 200 and counts the amount to pay. It refuses to be paid while it is empty. The client code builds order 1001 with two lines and prints its numbers: the total is 250, the discount 25 and the amount to pay 225. It also tries to pay an empty order, and the entity refuses. In some languages the entity is a plain value with functions, not a class; its role is the same. When to use it. Make an entity for each concept of the business that has its own rules, such as an order or an account. Data without rules needs no entity: a simple structure is enough. A common mistake. An entity that saves itself to the database or formats itself for the screen. Then it depends on outer code, and its rules can no longer be tested alone. |