
//entity
class OrderLine {
constructor(
readonly sku: string,
readonly price: number,
readonly count: number) {
if (count <= 0) {
throw new Error("bad count " + sku)
}
}
cost(): number {
return this.price * this.count
}
}
//entity
class Order {
private lines: OrderLine[] = []
private isPaid = false
constructor(readonly id: number) {
}
add(sku: string, price: number, count: number) {
this.lines.push(new OrderLine(sku, price, count))
}
//the goods leave as a copy: readonly is a word for
//the compiler and guards nothing while running
goods(): ReadonlyArray<OrderLine> {
return this.lines.slice()
}
size(): number {
return this.lines.length
}
total(): number {
let sum = 0
for (let line of this.lines) {
sum += line.cost()
}
return sum
}
//business rule: 10% off from 200
discount(): number {
let total = this.total()
return total >= 200 ? Math.trunc(total / 10) : 0
}
payable(): number {
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.isPaid = true
}
paid(): boolean {
return this.isPaid
}
}
//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())
//a cast drops readonly, but the copy keeps the order
//whole: the rule lives inside, not in the type
let taken = order.goods() as OrderLine[]
taken.pop()
console.log("still " + order.size())
try {
new Order(1003).pay()
} catch (error) {
console.log("refused: " + (error as Error).message)
}
try {
order.add("pen", 30, 0)
} catch (error) {
console.log("refused: " + (error as Error).message)
}
//printed: order 1001
// lines 2
// total 250
// discount 25
// payable 225
// paid true
// small total 60
// small discount 0
// still 2
// refused: empty order
// refused: bad count pen
Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.Read moreЗадача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно. Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят. Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же. Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры. Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно. |