
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问题。商店对金额达到 200 的每笔订单减免 10%。如果这条规则写在网页里,又在数据库查询里写了一遍,两份副本很快就会不一致。要检查这条规则,就得启动网页或数据库。商店最重要的知识分散各处,难以测试。 思路。实体是一段数据,连同与它相关的业务规则:订单包含什么,总额如何计算,何时可以付款。它对数据库、界面或网络一无所知。整洁架构有一条主要规则,即依赖规则:外层代码可以了解内层代码,但内层代码从不了解外层代码。实体是最内层的代码,因此不依赖任何东西。 如何阅读示例。示例中有两个实体:订单行和订单本身。订单汇总各行,对 200 及以上的金额减免 10%,并计算应付金额。订单为空时,它拒绝付款。客户端代码构建含两个订单行的订单 1001,并打印其中的数字:总额 250,折扣 25,应付 225。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。 何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。 常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。 |