Сущность

Паттерны проектирования / Паттерны чистой архитектуры
image

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


Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.
Read more

Задача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно.

Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят.

Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же.

Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры.

Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно.