Applied OOP Design Patterns: Rolling with the Punches

Applied OOP Design Patterns: Rolling with the Punches

Applying established design patterns in your object-oriented codebase can help you to "roll with the punches" of evolving business requirements. In this presentation, we'll take a practical look at design principles and patterns as we iteratively build a package with increasingly challenging feature requests. The key acronym in our tool belt? SOLID. (NOTE: This presentation will use an OOP-style pseudo-code language to appeal to developers of more than just one language.)

Ca57a7cfac69ba3abf517470f3770aae?s=128

Jeremy Lindblom

October 12, 2019
Tweet

Transcript

  1. Applied Design Patterns: Rolling with the Punches O S L

    I D O P R Y S I K N G A Y S I R P D I
  2. 2 Story Time

  3. 3 We want to send JSON data through Kinesis streams.

  4. 4 Pause

  5. 5 Acronyphilia?

  6. We ♥ Acronyms OOP DRY YAGNI KISS 6

  7. Keep it Simple? ▪ To Understand? ▪ To Write? ▪

    To Instantiate? 7 ▪ To Maintain? ▪ To Test? ▪ To Change?
  8. Keep it Simple? ▪ To Understand? ▪ To Write? ▪

    To Instantiate? 8 ▪ To Maintain? ▪ To Test? ▪ To Change?
  9. SOLID 9

  10. SOLID ▪ S ▪ O ▪ L ▪ I ▪

    D 10
  11. SOLID ▪ SRP ▪ OCP ▪ LSP ▪ ISP ▪

    DIP 11
  12. SOLID ▪ Single Responsibility ▪ Open-Closed ▪ Liskov Substitution ▪

    Interface Segregation ▪ Dependency Inversion 12 }Principle
  13. SOLID ▪ Single Responsibility ▪ Open-Closed ▪ Liskov Substitution ▪

    Interface Segregation ▪ Dependency Inversion 13 Does one thing
  14. SOLID ▪ Single Responsibility ▪ Open-Closed ▪ Liskov Substitution ▪

    Interface Segregation ▪ Dependency Inversion 14 Extend without modification
  15. SOLID ▪ Single Responsibility ▪ Open-Closed ▪ Liskov Substitution ▪

    Interface Segregation ▪ Dependency Inversion 15 Child can replace a parent
  16. SOLID ▪ Single Responsibility ▪ Open-Closed ▪ Liskov Substitution ▪

    Interface Segregation ▪ Dependency Inversion 16 Small interfaces
  17. SOLID ▪ Single Responsibility ▪ Open-Closed ▪ Liskov Substitution ▪

    Interface Segregation ▪ Dependency Inversion 17 Rely on abstractions
  18. 18 Why?

  19. 19

  20. 20

  21. 21

  22. 22

  23. Jeremy’s Theorem Good OO Design is like a nice set

    of Quick-Assembly Shelves 23
  24. SOLID ▪ Single Responsibility ▪ Open-Closed ▪ Liskov Substitution ▪

    Interface Segregation ▪ Dependency Inversion 24 }Principle
  25. Applied Design Patterns: Rolling with the Punches O S L

    I D O P R Y S I K N G A Y S I R P D I
  26. Applied Design Patterns: Rolling with the Punches O S L

    I D O P R Y S I K N G A Y S I R P D I
  27. 27

  28. 28

  29. 29

  30. 30 Remember Our Story?

  31. 31 We want to send JSON data through Kinesis streams.

  32. 32 // The Simple Approach class KinesisStream { func new(KinesisClient

    client, string streamName); func write(Message message): void; } class Message { func new(map data); func getData(): map; func toJson(): string; } // Example stream = new KinesisStream(client, "app-events"); stream.write(new Message(["foo": "bar"])); YAGNI SRP
  33. 33 We might be switching to SQS, actually. Can you

    support both?
  34. 34 // Adding New Functionality? class Stream { func new(KinesisClient

    kinesisClient, SqsClient sqsClient, string streamName, string streamType ); func write(Message message): void { if (this.streamType === "kinesis") this.kinesisClient.(…) elif (this.streamType === "sqs") this.sqsClient.(…) else throw new Exception("…"); } } SRP OCP
  35. 35 // Adding New Functionality? class Stream { func new(KinesisClient

    kinesisClient, SqsClient sqsClient, string streamName, string streamType ); func write(Message message): void { if (this.streamType === "kinesis") this.kinesisClient.(…) elif (this.streamType === "sqs") this.sqsClient.(…) else throw new Exception("…"); } } SRP OCP
  36. 36 // Interfaces And Multiple Implementations interface Stream { func

    write(Message message): void; } class KinesisStream implements Stream { func new(KinesisClient client, string streamName); func write(Message message): void; } class SqsStream implements Stream { func new(SqsClient client, string queueUrl); func write(Message message): void; } SRP ISP
  37. 37 // Factories Are Your Friends class EventsFactory { func

    createEventStream(): Stream { switch (this.config.get("events.stream.type")) { case "kinesis": return this.createKinesisStream(); case "sqs": return this.createSqsStream(); // ... } func createKinesisStream(): KinesisStream; func createSqsStream(): SqsStream; } SRP LSP
  38. 38 // Polymorphism Can Be Easy class OrdersController { func

    new(Stream stream, OrdersRepository orders) {/* ... */} // ... func doSomethingWithAnOrder(string orderId) { // ... order = this.orders.get(orderId); this.stream.write(new Message([ "order_id": order.id, "item_count": order.items.count(), ])); // ... } } DIP
  39. 39 Can you log messages that you send?

  40. 40 // Decorators Are Cool class LoggingStream implements Stream {

    func new(Stream stream, LoggerInterface logger); func write(Message message): void { this.stream.write(message); this.logger.info('Message sent', message.getData()); }; } // Example stream = new LoggingStream(kinesisStream, logger); stream.write(message); SRP OCP LSP DIP
  41. 41 We are moving to SQS, but some systems haven’t

    yet. Can you send to both for now?
  42. 42 // Aggregates Can Work Too class MultiStream implements Stream

    { func new(Stream ...streams); func write(Message message): void; } // Example stream = new LoggingStream( new MultiStream(kinesisStream, sqsStream), logger ); stream.write(message); SRP OCP LSP DIP
  43. 43

  44. 44 We’ve come up with a specific format for messages

    that contains metadata.
  45. 45 // Messages Get a Structure class Message extends JsonSerializable

    { func new(Header header, array data); func getHeader(): Header; func getData(): map; func toJson(): string; } class Header extends JsonSerializable { func new(string id, …, DateTime timestamp); func getId(): string; func getSource(): string; func getTopic(): string; func getAction(): string; func getTimestamp(): DateTime; func toJson(): string; }
  46. 46 // A New Layer of Abstraction class EventPublisher {

    func new(Stream stream, string source); func publish(Event event): void; } interface Event { func getTopic(): string; func getAction(): string; func getData(): map; } publisher.publish(event); SRP DIP
  47. 47 // A New Layer of Abstraction class OrderEvent implements

    Event { func new(Order order, string action); // ... } class OrderCreatedEvent extends OrderEvent { func new(Order order); // ... } publisher.publish(new OrderCreatedEvent(order)); OCP LSP
  48. 48 We’d like to be able to turn events off

    via configuration.
  49. 49 // Polymorphism Instead of Branching class EventPublisher { func

    new(Stream stream, string source, bool on = true) { // … } func publish(Event event): void { if (this.on) { // Do stuff. } }; } OCP
  50. 50 // Polymorphism Instead of Branching interface Publisher { func

    publish(Event event): void; } class EventPublisher { func new(Stream stream, string source); func publish(Event event): void; } class NullPublisher { func publish(Event event): void { // Do nothing. }; } LSP DIP
  51. 51 // Conditionalize Your Factory Instead of Code class EventsFactory

    { // ... func createEventPublisher(): Publisher { if (!this.config.get("events.enabled")) { return new NullPublisher(); } return new EventPublisher( this.createEventStream(), this.config.get("events.source") ); } } SRP LSP
  52. 52 We want different streams for some topics, so they

    can scale independently.
  53. 53 // Under the Hood Changes interface TopicMapper { func

    mapTopic(string topic): string; } class KinesisStream implements Stream { func new(KinesisClient client, TopicMapper tm); func write(Message message): void; } class SqsStream implements Stream { func new(SqsClient client, TopicMapper tm); func write(Message message): void; } SRP LSP ISP
  54. 54 We’re moving to a custom streaming service now. Can

    you support that?
  55. 55 // New Streams? No Biggie! class HttpStream implements Stream

    { func new(HttpClient client, string url); func write(Message message): void; } class CustomServiceStream implements Stream { func new(CustomServiceClient client); func write(Message message): void; }
  56. 56 We want to send lots of messages at a

    time. Can we send them in batches?
  57. 57 You’ve Got to Be Kidding Me!

  58. 58 // Is There a Good Way? class CustomServiceBatchStream extends

    CustomServiceStream { func write(Message message): void func flush(): void; } OCP
  59. 59 // Is There a Good Way? interface Stream {

    func write(Message message): void; func flush(): void; } class CustomServiceBatchStream implements Stream { func write(Message message): void func flush(): void; } ISP
  60. 60 // Multiple Interfaces & Additive Changes interface Stream {

    func write(Message message): void; } interface BatchStream extends Stream { func flush(): void; } class CustomServiceBatchStream implements BatchStream { func new(CustomServiceClient client); func write(Message message): void; func flush(): void; } ISP
  61. Final Thoughts ▪ Software is complicated, because life is complicated.

    ▪ You can’t predict the future, but you can prepare for it. ▪ Learn from others’ experiences, but experience it for yourself. 61 O S L I D I P P P P P R C S S
  62. 62 class MultiStream implements Stream { func new(Stream ...streams); func

    write(Message message): void { }; } // Example stream = new LoggingStream( new MultiStream(kinesisStream, sqsStream), logger ); stream.write(message); Can anyone guess the programming language?
  63. 63 class MultiStream implements Stream { public function __construct(Stream ...$streams)

    { }; public function write(Message $message): void { }; } // Example $stream = new LoggingStream( new MultiStream($kinesisStream, $sqsStream), $logger ); $stream->write($message); It’s PHP!
  64. 64 Thanks! You can find me at ▪ @jeremeamia ▪

    jeremeamia@gmail.com
  65. Credits ▪ Presentation template by SlidesCarnival ▪ Photographs by Unsplash

    65