Composition Root

Design patterns / Clean architecture patterns
image

uses System.SysUtils, System.Generics.Collections;

type
  TItem = TPair<string, Integer>;

  //entity
  TOrder = record
    Id, Total: Integer;
    function Payable: Integer;
  end;

  //gateway
  ICatalog = interface
    function TryPrice(const Sku: string;
      out Price: Integer): Boolean;
  end;

  //repository
  IOrders = interface
    procedure Save(const Order: TOrder);
    function Count: Integer;
  end;

  //output port
  IReport = interface
    procedure Say(const Text: string);
  end;

  //input port
  IPlaceOrder = interface
    procedure Place(const Items: array of TItem);
  end;

  //use case: asks for its parts, never builds them
  TPlaceOrder = class(TInterfacedObject, IPlaceOrder)
  private
    FCatalog: ICatalog;
    FOrders: IOrders;
    FReport: IReport;
  public
    constructor Create(const Catalog: ICatalog;
      const Orders: IOrders; const Report: IReport);
    procedure Place(const Items: array of TItem);
  end;

  //adapters
  TPriceList = class(TInterfacedObject, ICatalog)
    function TryPrice(const Sku: string;
      out Price: Integer): Boolean;
  end;

  TMemoryOrders = class(TInterfacedObject, IOrders)
  private
    FRows: TArray<TOrder>;
  public
    procedure Save(const Order: TOrder);
    function Count: Integer;
  end;

  TConsoleReport = class(TInterfacedObject, IReport)
    procedure Say(const Text: string);
  end;

  TSilentReport = class(TInterfacedObject, IReport)
    procedure Say(const Text: string);
  end;

  //composition root: the only place that sees every layer
  //at once, and the only place where an adapter is built;
  //a record of interfaces, so reference counting frees
  //every part it built and nobody calls Free
  TShopApp = record
    UseCase: IPlaceOrder;
    Orders: IOrders;
    class function Loud: TShopApp; static;
    class function Quiet: TShopApp; static;
  end;

function TOrder.Payable: Integer;
begin
  Result := Total;
  if Total >= 200 then
    Dec(Result, Total div 10);
end;

constructor TPlaceOrder.Create(const Catalog: ICatalog;
  const Orders: IOrders; const Report: IReport);
begin
  FCatalog := Catalog;
  FOrders := Orders;
  FReport := Report;
end;

procedure TPlaceOrder.Place(const Items: array of TItem);
var
  Order: TOrder;
  Item: TItem;
  Price: Integer;
begin
  Order := Default(TOrder);
  for Item in Items do
    if FCatalog.TryPrice(Item.Key, Price) then
      Inc(Order.Total, Price * Item.Value)
    else
    begin
      FReport.Say('no ' + Item.Key);
      Exit;
    end;
  //the repository numbers the orders by its rows
  Order.Id := 1001 + FOrders.Count;
  FOrders.Save(Order);
  FReport.Say(Format('order %d for %d',
    [Order.Id, Order.Payable]));
end;

function TPriceList.TryPrice(const Sku: string;
  out Price: Integer): Boolean;
begin
  Price := 0;
  if Sku = 'pen' then
    Price := 30
  else if Sku = 'lamp' then
    Price := 190;
  Result := Price > 0;
end;

procedure TMemoryOrders.Save(const Order: TOrder);
begin
  FRows := FRows + [Order];
end;

function TMemoryOrders.Count: Integer;
begin
  Result := Length(FRows);
end;

procedure TConsoleReport.Say(const Text: string);
begin
  WriteLn(Text);
end;

procedure TSilentReport.Say(const Text: string);
begin
end;

class function TShopApp.Loud: TShopApp;
begin
  Result.Orders := TMemoryOrders.Create;
  Result.UseCase := TPlaceOrder.Create(TPriceList.Create,
    Result.Orders, TConsoleReport.Create);
end;

class function TShopApp.Quiet: TShopApp;
begin
  Result.Orders := TMemoryOrders.Create;
  Result.UseCase := TPlaceOrder.Create(TPriceList.Create,
    Result.Orders, TSilentReport.Create);
end;

//client code
var
  App: TShopApp;
begin
  App := TShopApp.Loud;
  App.UseCase.Place([TItem.Create('pen', 2),
    TItem.Create('lamp', 1)]);
  App.UseCase.Place([TItem.Create('chair', 1)]);

  //a second root builds the same use case differently;
  //the first one and all its parts are freed right here
  App := TShopApp.Quiet;
  App.UseCase.Place([TItem.Create('lamp', 1)]);
  WriteLn('saved ', App.Orders.Count);
  //printed: order 1001 for 225
  //         no chair
  //         saved 1
end.


A composition root is the only place where the layers are connected and the adapters are built; the rest of the application only asks for its parts.
Read more

The problem. A use case needs a catalogue of prices, a store for orders and a way to report. If the use case creates these parts itself, it names adapters of the outer layer and depends on them. A test cannot give it other parts. If every part of the program builds its own parts, nobody sees how the application is connected.

The idea. A composition root is the only place where the layers are connected and the adapters are built. An adapter is outer code that implements a port, the interface that the inner layer declares. The use case only asks for its parts through ports and never builds them. The root knows every layer at once, and that is its job. All other code keeps the dependency rule: inner code never knows outer code. No framework or container is needed: the root is plain code at the start of the program.

How to read the example. The use case asks for three ports: a gateway for prices, a repository for orders and an output port for reports. The outer layer has adapters for them: a catalogue and a store in memory, a report that prints and a report that says nothing. The example builds the application twice. In the first build the report prints: the client code places order 1001, with 225 to pay, and an order for an unknown item. The second build has the silent report, so its orders print nothing. Only the count of saved orders shows that they were saved. The use case is the same in both builds.

When to use it. Every application has one composition root, at its start; a test may have its own small root with fake adapters. A short script without ports needs no root.

A common mistake. Creating an adapter inside a use case, even once. The use case then depends on the outer layer, and the root no longer shows how the application is connected.