Entity

Design patterns / Clean architecture patterns
image

//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

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.