$30 off During Our Annual Pro Sale. View Details »

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.)

Jeremy Lindblom

October 12, 2019
Tweet

More Decks by Jeremy Lindblom

Other Decks in Programming

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

    View Slide

  2. 2
    Story Time

    View Slide

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

    View Slide

  4. 4
    Pause

    View Slide

  5. 5
    Acronyphilia?

    View Slide

  6. We ♥ Acronyms
    OOP
    DRY
    YAGNI
    KISS
    6

    View Slide

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

    View Slide

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

    View Slide

  9. SOLID
    9

    View Slide

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

    View Slide

  11. SOLID
    ▪ SRP
    ▪ OCP
    ▪ LSP
    ▪ ISP
    ▪ DIP
    11

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. SOLID
    ▪ Single Responsibility
    ▪ Open-Closed
    ▪ Liskov Substitution
    ▪ Interface Segregation
    ▪ Dependency Inversion
    17
    Rely on
    abstractions

    View Slide

  18. 18
    Why?

    View Slide

  19. 19

    View Slide

  20. 20

    View Slide

  21. 21

    View Slide

  22. 22

    View Slide

  23. Jeremy’s Theorem
    Good OO Design
    is like a nice set of
    Quick-Assembly
    Shelves
    23

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  27. 27

    View Slide

  28. 28

    View Slide

  29. 29

    View Slide

  30. 30
    Remember
    Our Story?

    View Slide

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

    View Slide

  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

    View Slide

  33. 33
    We might be switching
    to SQS, actually. Can
    you support both?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  39. 39
    Can you log messages
    that you send?

    View Slide

  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

    View Slide

  41. 41
    We are moving to SQS,
    but some systems
    haven’t yet. Can you
    send to both for now?

    View Slide

  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

    View Slide

  43. 43

    View Slide

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

    View Slide

  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;
    }

    View Slide

  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

    View Slide

  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

    View Slide

  48. 48
    We’d like to be able to
    turn events off via
    configuration.

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  52. 52
    We want different
    streams for some
    topics, so they can
    scale independently.

    View Slide

  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

    View Slide

  54. 54
    We’re moving to a
    custom streaming
    service now. Can you
    support that?

    View Slide

  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;
    }

    View Slide

  56. 56
    We want to send lots
    of messages at a time.
    Can we send them in
    batches?

    View Slide

  57. 57
    You’ve
    Got to Be
    Kidding
    Me!

    View Slide

  58. 58
    // Is There a Good Way?
    class CustomServiceBatchStream extends CustomServiceStream {
    func write(Message message): void
    func flush(): void;
    }
    OCP

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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?

    View Slide

  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!

    View Slide

  64. 64
    Thanks!
    You can find me at
    ▪ @jeremeamia
    [email protected]

    View Slide

  65. Credits
    ▪ Presentation template by SlidesCarnival
    ▪ Photographs by Unsplash
    65

    View Slide