
using System;
using System.Collections.Generic;
//client code
var app = ShopApp.Loud();
app.UseCase.Place([("pen", 2), ("lamp", 1)]);
app.UseCase.Place([("chair", 1)]);
//a second root builds the same use case differently
var night = ShopApp.Quiet();
night.UseCase.Place([("lamp", 1)]);
Console.WriteLine($"saved {night.Orders.Count}");
//printed: order 1001 for 225
// no chair
// saved 1
//entity
record Order(int Id, int Total)
{
public int Payable =>
Total >= 200 ? Total - Total / 10 : Total;
}
//gateway
interface ICatalog
{
int? Price(string sku);
}
//repository
interface IOrders
{
void Save(Order order);
int Count { get; }
}
//output port
interface IReport
{
void Say(string text);
}
//use case: asks for its parts, never builds them
sealed class PlaceOrder(
ICatalog catalog,
IOrders orders,
IReport report)
{
private int nextId = 1001;
public void Place(
IReadOnlyList<(string Sku, int Count)> items)
{
int total = 0;
foreach (var (sku, count) in items)
{
if (catalog.Price(sku) is not int price)
{
report.Say($"no {sku}");
return;
}
total += price * count;
}
var order = new Order(nextId++, total);
orders.Save(order);
report.Say(
$"order {order.Id} for {order.Payable}");
}
}
//adapter
sealed class MemoryCatalog : ICatalog
{
public int? Price(string sku) => sku switch
{
"pen" => 30,
"lamp" => 190,
_ => null,
};
}
//adapter
sealed class MemoryOrders : IOrders
{
private readonly List<Order> rows = [];
public void Save(Order order) => rows.Add(order);
public int Count => rows.Count;
}
//adapter
sealed class ConsoleReport : IReport
{
public void Say(string text) =>
Console.WriteLine(text);
}
//adapter
sealed class SilentReport : IReport
{
public void Say(string text)
{
}
}
//composition root: the only place that sees every layer
//at once, and the only place where an adapter is built
sealed class ShopApp
{
private ShopApp(
ICatalog catalog,
IOrders orders,
IReport report)
{
Orders = orders;
UseCase = new PlaceOrder(catalog, orders, report);
}
public PlaceOrder UseCase { get; }
public IOrders Orders { get; }
public static ShopApp Loud() => new(
new MemoryCatalog(),
new MemoryOrders(),
new ConsoleReport());
public static ShopApp Quiet() => new(
new MemoryCatalog(),
new MemoryOrders(),
new SilentReport());
}
Корень композиции - единственное место, где слои соединяются и строятся адаптеры; остальное приложение только просит свои части.Read moreЗадача. Сценарию нужны каталог цен, хранилище заказов и способ выдать отчёт. Если сценарий сам создаёт эти части, он называет адаптеры внешнего слоя и зависит от них. Тест не может подставить ему другие части. А если каждая часть программы сама собирает свои части, никто не видит, как соединено приложение. Идея. Корень композиции - единственное место, где соединяются слои и создаются адаптеры. Адаптер - это внешний код, который реализует порт, то есть интерфейс, объявленный внутренним слоем. Сценарий только просит свои части через порты и никогда не создаёт их сам. Корень знает все слои сразу, и в этом его работа. Весь остальной код соблюдает правило зависимостей: внутренний код никогда не знает внешнего. Ни фреймворк, ни контейнер для этого не нужны: корень - обычный код в начале программы. Как читать пример. Сценарий просит три порта: шлюз для цен, репозиторий для заказов и выходной порт для отчётов. Во внешнем слое для них есть адаптеры: каталог и хранилище в памяти, отчёт, который печатает, и отчёт, который молчит. Пример собирает приложение дважды. В первой сборке отчёт печатает: клиентский код оформляет заказ 1001 с суммой к оплате 225 и заказ на неизвестный товар. Во второй сборке отчёт молчит, поэтому её заказы ничего не печатают. Сохранены ли они, видно только по числу заказов в хранилище. Сценарий в обеих сборках один и тот же. Когда применять. У каждого приложения один корень композиции, в самом начале; у теста может быть свой маленький корень с поддельными адаптерами. Короткому скрипту без портов корень не нужен. Частая ошибка. Создать адаптер внутри сценария, пусть даже один раз. Тогда сценарий зависит от внешнего слоя, а корень больше не показывает, как соединено приложение. |