
//entity
@interface Order : NSObject
@property (nonatomic, readonly) NSInteger number;
@property (nonatomic, readonly) NSInteger total;
@property (nonatomic, readonly) NSInteger payable;
- (instancetype)initWithNumber:(NSInteger)number
total:(NSInteger)total;
@end
@implementation Order
- (instancetype)initWithNumber:(NSInteger)number
total:(NSInteger)total {
self = [super init];
if (self) {
_number = number;
_total = total;
}
return self;
}
- (NSInteger)payable {
return _total >= 200 ? _total - _total / 10 : _total;
}
@end
//gateway
@protocol Catalog <NSObject>
- (NSNumber *)priceOf:(NSString *)sku;
@end
//repository
@protocol Orders <NSObject>
- (void)save:(Order *)order;
- (NSUInteger)count;
@end
//output port: one job, so a block type
typedef void (^Report)(NSString *text);
//use case: asks for its parts, never builds them; they
//come in as properties, the way Cocoa wires outlets and
//delegates, and one forgotten would stay nil, silently:
//another reason to set them in exactly one place
@interface PlaceOrder : NSObject
@property (nonatomic, strong) id<Catalog> catalog;
@property (nonatomic, strong) id<Orders> orders;
@property (nonatomic, copy) Report report;
//a dictionary is fine here: only the sum is seen, never
//the order in which the lines were walked
- (void)place:(NSDictionary *)items;
@end
@implementation PlaceOrder
- (void)place:(NSDictionary *)items {
NSInteger total = 0;
for (NSString *sku in items) {
NSNumber *price = [self.catalog priceOf:sku];
if (price == nil) {
self.report([@"no "
stringByAppendingString:sku]);
return;
}
total += price.integerValue
* [items[sku] integerValue];
}
Order *order = [[Order alloc]
initWithNumber:1001 + self.orders.count
total:total];
[self.orders save:order];
self.report([NSString stringWithFormat:
@"order %ld for %ld", (long)order.number,
(long)order.payable]);
}
@end
//adapters
@interface MemoryCatalog : NSObject <Catalog>
@end
@implementation MemoryCatalog
- (NSNumber *)priceOf:(NSString *)sku {
return @{@"pen": @30, @"lamp": @190}[sku];
}
@end
@interface MemoryOrders : NSObject <Orders>
@end
@implementation MemoryOrders {
NSArray<Order *> *_rows;
}
- (void)save:(Order *)order {
NSArray *before = _rows ? _rows : @[];
_rows = [before arrayByAddingObject:order];
}
- (NSUInteger)count {
return _rows.count;
}
@end
//composition root: the only place that sees every layer
//at once, and the only place where an adapter is built;
//no NSClassFromString and no list of class names in a
//plist: the runtime could wire the app by strings, and
//the compiler would check none of it
@interface ShopApp : NSObject
@property (nonatomic, strong, readonly) PlaceOrder *useCase;
@property (nonatomic, strong, readonly) id<Orders> orders;
+ (instancetype)loudApp;
+ (instancetype)quietApp;
@end
@implementation ShopApp
- (instancetype)initWithReport:(Report)report {
self = [super init];
if (self) {
_orders = [[MemoryOrders alloc] init];
_useCase = [[PlaceOrder alloc] init];
_useCase.catalog = [[MemoryCatalog alloc] init];
_useCase.orders = _orders;
_useCase.report = report;
}
return self;
}
+ (instancetype)loudApp {
return [[ShopApp alloc] initWithReport:
^(NSString *text) { NSLog(@"%@", text); }];
}
//a report that says nothing ignores the text, and says
//so with (void): Apple's __unused is missing on GNUstep
+ (instancetype)quietApp {
return [[ShopApp alloc] initWithReport:
^(NSString *text) { (void)text; }];
}
@end
//client code
ShopApp *app = [ShopApp loudApp];
[app.useCase place:@{@"pen": @2, @"lamp": @1}];
[app.useCase place:@{@"chair": @1}];
//a second root builds the same use case differently
ShopApp *night = [ShopApp quietApp];
[night.useCase place:@{@"lamp": @1}];
NSLog(@"saved %lu", (unsigned long)night.orders.count);
//printed: order 1001 for 225
// no chair
// saved 1
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 moreThe 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. |