
//entity: a refusal is an NSError, not an exception;
//under ARC a thrown exception leaks what it unwinds
BOOL refuse(NSError **error, NSString *reason) {
if (error) {
*error = [NSError errorWithDomain:@"shop" code:1
userInfo:@{NSLocalizedDescriptionKey: reason}];
}
return NO;
}
//entity
@interface OrderLine : NSObject
//copy, not strong: a mutable string handed in stays
//the caller's, and a later change of it cannot reach
//into the order
@property (nonatomic, copy, readonly) NSString *sku;
@property (nonatomic, readonly) NSInteger price;
@property (nonatomic, readonly) NSInteger count;
@property (nonatomic, readonly) NSInteger cost;
- (instancetype)initWithSku:(NSString *)sku
price:(NSInteger)price
count:(NSInteger)count;
@end
@implementation OrderLine
- (instancetype)initWithSku:(NSString *)sku
price:(NSInteger)price
count:(NSInteger)count {
self = [super init];
if (self) {
_sku = [sku copy];
_price = price;
_count = count;
}
return self;
}
- (NSInteger)cost {
return self.price * self.count;
}
@end
//entity
@interface Order : NSObject
@property (nonatomic, readonly) NSInteger number;
//an immutable array, replaced on every add: what the
//outside was given never changes behind the rules
@property (nonatomic, copy, readonly)
NSArray<OrderLine *> *lines;
@property (nonatomic, readonly) NSInteger total;
@property (nonatomic, readonly) NSInteger discount;
@property (nonatomic, readonly) NSInteger payable;
@property (nonatomic, readonly) BOOL paid;
- (instancetype)initWithNumber:(NSInteger)number;
- (BOOL)addSku:(NSString *)sku
price:(NSInteger)price
count:(NSInteger)count
error:(NSError **)error;
- (BOOL)pay:(NSError **)error;
@end
@implementation Order
- (instancetype)initWithNumber:(NSInteger)number {
self = [super init];
if (self) {
_number = number;
_lines = @[];
}
return self;
}
- (BOOL)addSku:(NSString *)sku
price:(NSInteger)price
count:(NSInteger)count
error:(NSError **)error {
if (count <= 0) {
return refuse(error, @"bad count");
}
OrderLine *line = [[OrderLine alloc] initWithSku:sku
price:price
count:count];
_lines = [_lines arrayByAddingObject:line];
return YES;
}
//a key path sums the lines without a loop; the price
//is that the compiler does not check the word "cost"
- (NSInteger)total {
return [[_lines valueForKeyPath:@"@sum.cost"]
integerValue];
}
//business rule: 10% off from 200
- (NSInteger)discount {
return self.total >= 200 ? self.total / 10 : 0;
}
- (NSInteger)payable {
return self.total - self.discount;
}
//business rule: an empty order is never paid
- (BOOL)pay:(NSError **)error {
if (_lines.count == 0) {
return refuse(error, @"empty order");
}
_paid = YES;
return YES;
}
@end
//client code
//no database and no screen anywhere above
Order *order = [[Order alloc] initWithNumber:1001];
NSMutableString *sku = [@"pen" mutableCopy];
[order addSku:sku price:30 count:2 error:NULL];
[order addSku:@"lamp" price:190 count:1 error:NULL];
[sku appendString:@"cil"];
NSLog(@"order %ld", (long)order.number);
NSLog(@"lines %lu", (unsigned long)order.lines.count);
NSLog(@"first %@, caller has %@",
order.lines.firstObject.sku, sku);
NSLog(@"total %ld", (long)order.total);
NSLog(@"discount %ld", (long)order.discount);
NSLog(@"payable %ld", (long)order.payable);
[order pay:NULL];
NSLog(@"paid %d", order.paid);
//the rules refuse bad data, and nobody else has to
NSError *error = nil;
if (![order addSku:@"pen" price:30 count:0 error:&error]) {
NSLog(@"refused: %@", error.localizedDescription);
}
Order *empty = [[Order alloc] initWithNumber:1002];
if (![empty pay:&error]) {
NSLog(@"refused: %@", error.localizedDescription);
}
//printed: order 1001
// lines 2
// first pen, caller has pencil
// total 250
// discount 25
// payable 225
// paid 1
// refused: bad count
// refused: empty order
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。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。 何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。 常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。 |