Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Practically SOLID

Practically SOLID

Practical usage of SOLID principles in the iOS world

Danny Hertz

July 14, 2015
Tweet

More Decks by Danny Hertz

Other Decks in Programming

Transcript

  1. A little bit about me • Born and raised in

    Queens! • Fullstack engineer since I was a fetus • Started iOS dev 2+ years ago • Front-end/iOS @ Twitter for 5 years • iOS Platform @ UBER for 2 months • I ❤ pugs & behavioral economics
  2. 5 principles intended to make it more likely that a

    programmer will create a system that is easy to maintain and extend over time1. 1 https://en.wikipedia.org/wiki/SOLID(object-orienteddesign)
  3. SOLID Benefits • Increases confidence in your code • Facilitates

    a common language to describe patterns with others • Promotes system that can be easily extended in the future • Helps isolate areas of code base that change frequently • Makes testing a breeeeeze
  4. Single responsibility principle • Every class should be responsible for

    doing one thing • Ask yourself what this class does • You shouldn't have to use the word "and"
  5. Open-closed principle • Every class should be open for extension,

    but closed for modification • You shouldn't have to rip open a class to add more expected behavior • Dependency Injection really shines here
  6. Liskov substitution principle • Subclasses shouldn't break expectations from the

    parent class • You shouldn't have to introspect an object to see if it behaves a certain way • Square, rectangle, blah blah blah
  7. Interface segragation principle • Horrible name • Break up interfaces

    based on expected needs of consumers • Helps avoid giving consumers too much power
  8. Dependency inversion principle • High-level modules should not depend on

    low-level modules. • Both modules should depend on abstractions • High Level Classes -> Abstraction Layer -> Low Level Classes • Decouples your classes and allows for easy behavior replacement
  9. The problem We would like to make a realtime Twitter

    client that automatically updates the home timeline and user profile while browsing the app. We would also like to have the ability to change the frequencies of these updates in case our servers get overloaded.
  10. Scheduler - implementation @implementation DSHServiceScheduler : NSObject - (void)start {

    self.serviceTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(_timerDidFire:) userInfo:nil repeats:YES]; } - (void)stop { [self.serviceTimer invalidate]; self.serviceTimer = nil; } - (void)_timerDidFire:(NSTimer *)timer { self.timerTick++; [self _runServicesWithTick:self.timerTick]; } - (void)_runServicesWithTick:(NSInteger)tick { if (tick % 1 == 0) { [self _runTimelineUpdateService]; } else if (tick % 2 == 0) { [self _runProfileUpdateService]; } } - (void)_runTimelineUpdateService { [DSHTimelineService fetchTimelineForUserID:@"123"]; } - (void)_runProfileUpdateService { [DSHProfileService fetchTimelineForUserID:@"123"]; } @end
  11. Scheduler - start timer - (void)start { self.serviceTimer = [NSTimer

    scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(_timerDidFire:) userInfo:nil repeats:YES]; } - (void)stop { [self.serviceTimer invalidate]; self.serviceTimer = nil; } - (void)_timerDidFire:(NSTimer *)timer { self.timerTick++; [self _runServicesWithTick:self.timerTick]; }
  12. Scheduler - decide frequencies - (void)_runServicesWithTick:(NSInteger)tick { if (tick %

    1 == 0) { [self _runTimelineUpdateService]; } else if (tick % 2 == 0) { [self _runProfileUpdateService]; } }
  13. Scheduler - execute services - (void)_runTimelineUpdateService { [DSHTimelineService fetchTimelineForUserID:@"123"]; }

    - (void)_runProfileUpdateService { [DSHProfileService fetchProfileForUserID:@"123"]; }
  14. Single Responsiblity - FAIL ! • It knows how to

    schedule services • It knows how tweets are updated • It knows how profiles are updated • It knows how often each service should be requested
  15. Open-Closed - FAIL ! • Adding a 3rd service requires

    opening up the scheduler • Changing frequencies requires opening up the scheduler
  16. Dependency Inversion - FAIL ! • Scheduler depends on specific

    implementation of service APIs • If you change how profiles update or tweets are fetched you have to update the scheduler • Very tight coupling
  17. How about some tests? (1/3) // Mock the runLoop and

    timer __unsafe_unretained __block NSTimer *testTimer = nil; self.runLoopMock = OCMPartialMock([NSRunLoop mainRunLoop]); OCMStub([self.runLoopMock addTimer:[OCMArg any] forMode:[OCMArg any]]).andDo(^(NSInvocation *invocation) { [invocation getArgument:&testTimer atIndex:2]; self.testTimer = testTimer; });
  18. How about some tests? (2/3) // Mock high frequency service

    OCMExpect(self.timelineServiceMock fetchTimelineForUserID:[OCMArg any]); OCMExpect(self.timelineServiceMock fetchTimelineForUserID:[OCMArg any]); // Mock low frequency service OCMExpect(self.profileServiceMock fetchProfileForUserID:[OCMArg any]);
  19. How about some tests? (3/3) // Tick twice [self.testTimer fire];

    [self.testTimer fire]; // Verify all the things OCMVerifyAll(self.APIMock);
  20. That was horrible ! • Mocking run loop is error-prone

    (potential for side effects) • Required knowledge of individual service APIs (change will be hard) • No easy way to tweak/control frequencies
  21. Scheduler - interface @interface DSHServiceScheduler : NSObject - (instancetype)initWithTimer:(id<DSHTimer>)timer; -

    (void)registerService:(id<DSHService>)service; - (void)start; - (void)stop; @end
  22. Scheduler - start/stop timer - (void)start { [self.timer onTick:^(NSInteger tick){

    [self _timerDidTick:tick]; }]; [self.timer start]; } - (void)stop { [self.timer stop]; } - (void)_timerDidTick:(NSIntger)tick { [self _runServicesWithTick:tick]; }
  23. Scheduler - execute services - (void)_runServicesWithTick:(NSInteger)tick { for (id<DSHService> service

    in self.services) { if ([self _shouldExecuteFrequency:service.frequency onTick:tick]) { [service execute]; } } }
  24. Service - interface @protocol DSHService <NSObject> @property (nonatomic, readonly) DSHServiceFrequency

    frequency; - (instancetype)initWithFrequency:(DSHServiceFrequency)frequency executionBlock:(void(^)())executionBlock; - (void)execute; @end
  25. Service - init - (instancetype)initWithFrequency:(DSHServiceFrequency)frequency executionBlock:(void(^)())executionBlock; { self = [super

    init]; if (self) { _frequency = frequency; _executionBlock = [executionBlock copy]; } return self; }
  26. Timer - interface @protocol DSHTimer <NSObject> @property (nonatomic, readonly) NSInteger

    currentTick; - (void)onTick:(void (^)(NSInteger))tickBlock; - (void)start; - (void)stop; @end
  27. • Help keep your domain specific logic isolated and grouped

    • Tuck away those nitty gritty initialization details • Help isolate parts of code base that change frequently to one spot
  28. Service Factory + (id<DSHService>)twitterTimelineServiceWithUserID:(NSString *)userID { return [DSHService initWithFrequency:DSHServiceFrequencyHigh executionBlock:^{

    [DSHTimelineService fetchTimelineForUserID:userID]; }] } + (id<DSHService>)twitterProfileServiceWithUserID:(NSString *)userID { return [DSHService initWithFrequency:DSHServiceFrequencyLow executionBlock:^{ [DSHProfileService fetchProfileForUserID:userID]; }] }
  29. Scheduler Factory + (id<DSHServiceScheduler>)twitterServiceSchedulerWithUserID:(NSString *)userID { id<DSHTimer> timer = [self

    clockTimer]; DSHServiceScheduler *twitterScheduler = [DSHServiceScheduler alloc] initWithTimer:timer]; [twitterScheduler registerService:[DSHServiceFactory twitterTimelineServiceWithUserID:userID]]; [twitterScheduler registerService:[DSHServiceFactory twitterProfileServiceWithUserID:userID]]; return twitterScheduler; }
  30. Single Responsiblity - PASS ! • Each class has one

    job • Scheduler knows how to schedule services • Service knows how to execute a service • Timer knows how timing works
  31. Open-Closed - PASS ! • Adding a 3rd service just

    means passing a method to the scheduler (no disection needed) • Changing the frequency of a service only requires a change to the factory • All core non-domain specific classes remain untouched
  32. Dependency Inversion - PASS ! • Scheduler depends on abstractions

    (protocols in our case) • Services depends on abstractions • You can change the actual service logic all you want and the system wont feel a thing
  33. Scheduler test - (void)testSchedulerExecutesRegisteredServices { id<DSHTimer> testTimer = [[DSHTestTimer alloc]

    init]; DSHServiceScheduler *testScheduler = [[DSHServiceScheduler alloc] initWithTimer:testTimer]; XCTestExpectation *serviceExpectation = [self expectationWithDescription:@"Service executed"]; id<DSHService> testService = [[DSHSchedulableService alloc] initWithFrequency:DSHSchedulableFrequencyHigh executionBlock:^{ [serviceExpectation fulfill] }; [testScheduler registerSchedulableService:testService]; [testScheduler start]; [self waitForExpectationsWithTimeout:0.1 handler:nil]; }
  34. Service test - (void)testServicesExecutesExecutionBlock { XCTestExpectation *serviceExpectation = [self expectationWithDescription:@"Service

    executed"]; id<DSHService> testService = [[DSHSchedulableService alloc] initWithFrequency:DSHSchedulableFrequencyHigh executionBlock:^{ [serviceExpectation fulfill] }]; [testService execute]; [self waitForExpectationsWithTimeout:0.1 handler:nil]; }
  35. Timer test - (void)testTimerFiresBlocks { id<DSHTimer> testTimer = [[DSHTestTimer alloc]

    init]; __block NSInteger fireCount = 0; [testTimer onTick:^(NSInteger tick) { fireCount++; }]; [timerTicker start]; [testTimer fire]; [testTimer fire]; [testTimer fire]; XCTAssertEqual(fireCount, 3); }
  36. That felt much better! ! • Each test only tested

    its single responsibility • Protocols allowed us to create dummy helper classes to isolate the system under test • Component tests had no mention of domain knowledge • Behavior-focused so less likely to change if implementation changes
  37. Go through your existing classes and ask: • How many

    responsibilities does this class have? • Do I need to change this class to extend its behavior? • What parts of this class change frequency and which rarely change? • Does this class depend on concrete dependencies? • Does my class need THIS many imports?
  38. Additional resources • Watch every @sandimetz video you can find

    on Youtube • Tutsplus has a great SOLID guide/tutorial • Bob's site - http://objectmentor.com/