Entity

Design patterns / Clean architecture patterns
image

uses System.SysUtils, System.Generics.Collections;

type
  //entity: a value with no identity of its own
  TOrderLine = record
    Sku: string;
    Price: Integer;
    Count: Integer;
    function Cost: Integer;
  end;

  //entity: an identity and the rules around it
  TOrder = class
  private
    FId: Integer;
    FLines: TList<TOrderLine>;
    FPaid: Boolean;
  public
    constructor Create(AId: Integer);
    destructor Destroy; override;
    procedure Add(const Sku: string;
      Price, Count: Integer);
    function Size: Integer;
    function Total: Integer;
    function Discount: Integer;
    function Payable: Integer;
    procedure Pay;
    property Id: Integer read FId;
    property Paid: Boolean read FPaid;
  end;

function TOrderLine.Cost: Integer;
begin
  Result := Price * Count;
end;

constructor TOrder.Create(AId: Integer);
begin
  FId := AId;
  FLines := TList<TOrderLine>.Create;
end;

destructor TOrder.Destroy;
begin
  FLines.Free;
  inherited;
end;

procedure TOrder.Add(const Sku: string;
  Price, Count: Integer);
var
  Line: TOrderLine;
begin
  if Count <= 0 then
    raise EArgumentException.Create('bad count');
  Line.Sku := Sku;
  Line.Price := Price;
  Line.Count := Count;
  FLines.Add(Line);
end;

function TOrder.Size: Integer;
begin
  Result := FLines.Count;
end;

function TOrder.Total: Integer;
var
  Line: TOrderLine;
begin
  Result := 0;
  for Line in FLines do
    Inc(Result, Line.Cost);
end;

//business rule: 10% off from 200
function TOrder.Discount: Integer;
begin
  Result := 0;
  if Total >= 200 then
    Result := Total div 10;
end;

function TOrder.Payable: Integer;
begin
  Result := Total - Discount;
end;

//business rule: an empty order is never paid
procedure TOrder.Pay;
begin
  if FLines.Count = 0 then
    raise EInvalidOpException.Create('empty order');
  FPaid := True;
end;

//client code
//no database and no screen anywhere above
var
  Order: TOrder;
begin
  Order := TOrder.Create(1001);
  try
    Order.Add('pen', 30, 2);
    Order.Add('lamp', 190, 1);
    WriteLn('order ', Order.Id);
    WriteLn('lines ', Order.Size);
    WriteLn('total ', Order.Total);
    WriteLn('discount ', Order.Discount);
    WriteLn('payable ', Order.Payable);
    Order.Pay;
    WriteLn('paid ', Order.Paid);
  finally
    Order.Free;
  end;

  //a line that breaks the rule never gets into the order,
  //so the order below stays empty and is refused too
  Order := TOrder.Create(1002);
  try
    try
      Order.Add('pen', 30, 0);
    except
      on E: EArgumentException do
        WriteLn('refused: ', E.Message);
    end;
    try
      Order.Pay;
    except
      on E: EInvalidOpException do
        WriteLn('refused: ', E.Message);
    end;
  finally
    Order.Free;
  end;
  //printed: order 1001
  //         lines 2
  //         total 250
  //         discount 25
  //         payable 225
  //         paid TRUE
  //         refused: bad count
  //         refused: empty order
end.


An entity holds a business rule of the shop and knows nothing about a database, a screen or the network, so it can be tested without them.
Read more

The problem. A shop gives 10% off every order from 200. If this rule lives in a web page, and again in a database query, the two copies soon disagree. To check the rule, you must start the page or the database. The most important knowledge of the shop is scattered and hard to test.

The idea. An entity is a piece of data together with the business rules about it: what an order contains, how its total is counted, when it may be paid. It knows nothing about a database, a screen or the network. Clean architecture has one main rule, the dependency rule: outer code may know inner code, but inner code never knows outer code. Entities are the innermost code, so they depend on nothing.

How to read the example. There are two entities: a line of an order and the order itself. The order sums its lines, gives 10% off from 200 and counts the amount to pay. It refuses to be paid while it is empty. The client code builds order 1001 with two lines and prints its numbers: the total is 250, the discount 25 and the amount to pay 225. It also tries to pay an empty order, and the entity refuses. In some languages the entity is a plain value with functions, not a class; its role is the same.

When to use it. Make an entity for each concept of the business that has its own rules, such as an order or an account. Data without rules needs no entity: a simple structure is enough.

A common mistake. An entity that saves itself to the database or formats itself for the screen. Then it depends on outer code, and its rules can no longer be tested alone.