
//a port method with no body names the class that left
//it out: JavaScript has no interfaces, so a port here
//is a base class and extends says who serves it
function notWritten(owner, method) {
throw new Error(owner.constructor.name + "." + method);
}
//entity
class Order {
#total;
constructor(id, total) {
this.id = id;
this.#total = total;
}
payable() {
let cut = this.#total >= 200 ? this.#total / 10 : 0;
return this.#total - Math.trunc(cut);
}
}
//gateway
class Catalog {
price(sku) {
notWritten(this, "price");
}
}
//repository
class Orders {
save(order) {
notWritten(this, "save");
}
count() {
notWritten(this, "count");
}
}
//output port
class Report {
say(text) {
notWritten(this, "say");
}
}
//use case: asks for its parts, never builds them
class PlaceOrder {
#catalog;
#orders;
#report;
#nextId = 1001;
constructor(catalog, orders, report) {
this.#catalog = catalog;
this.#orders = orders;
this.#report = report;
}
place(items) {
let total = 0;
for (let [sku, count] of items) {
let price = this.#catalog.price(sku);
if (price === undefined) {
this.#report.say("no " + sku);
return;
}
total += price * count;
}
let order = new Order(this.#nextId++, total);
this.#orders.save(order);
this.#report.say("order " + order.id +
" for " + order.payable());
}
}
//adapters
class MemoryCatalog extends Catalog {
#prices = new Map([["pen", 30], ["lamp", 190]]);
price(sku) {
return this.#prices.get(sku);
}
}
class MemoryOrders extends Orders {
#rows = new Map();
save(order) {
this.#rows.set(order.id, order);
}
count() {
return this.#rows.size;
}
}
class ConsoleReport extends Report {
say(text) {
console.log(text);
}
}
class SilentReport extends Report {
say(text) {
}
}
//composition root: the only place that sees every layer
//at once, and the only place where new stands next to an
//adapter. No container is needed; a function is the root
function build(report) {
let orders = new MemoryOrders();
return {
useCase: new PlaceOrder(
new MemoryCatalog(), orders, report),
orders: orders
};
}
function loud() {
return build(new ConsoleReport());
}
function quiet() {
return build(new SilentReport());
}
//client code
let app = loud();
app.useCase.place(new Map([["pen", 2], ["lamp", 1]]));
app.useCase.place(new Map([["chair", 1]]));
//a second root builds the same use case differently, and
//neither of them ever named a class inside the use case
let night = quiet();
night.useCase.place(new Map([["lamp", 1]]));
console.log("night saved " + night.orders.count());
console.log("loud saved " + app.orders.count());
//printed: order 1001 for 225
// no chair
// night saved 1
// loud saved 1
Корень композиции - единственное место, где слои соединяются и строятся адаптеры; остальное приложение только просит свои части.Read moreЗадача. Сценарию нужны каталог цен, хранилище заказов и способ выдать отчёт. Если сценарий сам создаёт эти части, он называет адаптеры внешнего слоя и зависит от них. Тест не может подставить ему другие части. А если каждая часть программы сама собирает свои части, никто не видит, как соединено приложение. Идея. Корень композиции - единственное место, где соединяются слои и создаются адаптеры. Адаптер - это внешний код, который реализует порт, то есть интерфейс, объявленный внутренним слоем. Сценарий только просит свои части через порты и никогда не создаёт их сам. Корень знает все слои сразу, и в этом его работа. Весь остальной код соблюдает правило зависимостей: внутренний код никогда не знает внешнего. Ни фреймворк, ни контейнер для этого не нужны: корень - обычный код в начале программы. Как читать пример. Сценарий просит три порта: шлюз для цен, репозиторий для заказов и выходной порт для отчётов. Во внешнем слое для них есть адаптеры: каталог и хранилище в памяти, отчёт, который печатает, и отчёт, который молчит. Пример собирает приложение дважды. В первой сборке отчёт печатает: клиентский код оформляет заказ 1001 с суммой к оплате 225 и заказ на неизвестный товар. Во второй сборке отчёт молчит, поэтому её заказы ничего не печатают. Сохранены ли они, видно только по числу заказов в хранилище. Сценарий в обеих сборках один и тот же. Когда применять. У каждого приложения один корень композиции, в самом начале; у теста может быть свой маленький корень с поддельными адаптерами. Короткому скрипту без портов корень не нужен. Частая ошибка. Создать адаптер внутри сценария, пусть даже один раз. Тогда сценарий зависит от внешнего слоя, а корень больше не показывает, как соединено приложение. |