Design Patterns

Inheritance is not for code reuse; Composition is preferred in order to share behavior. Segregation Principle: Clients should not be forced to depend on methods that they do not use. For example Books should not be force to depend on the method sing just because Artist wants to have a method sing. Primitive obsession: the fact of being obsessed in passing primitives like numbers and booleans etc. rather than objects created by the user. It is the act of extracting the primitives out of objects, for example the string message instead of passing the entire object and calling getMessage(). It is a good thing because you reduce the dependencies, but it hinders your ability to use polymorphism. Polymorphism is the act of letting different types of objects have different behaviors. You want to avoid if statements by calling a method on an object of which you don't know the exact type in advance (only that it has that method). And by calling the method on that unknown object, you will get the desired behavior which is implemented within the object, instead of testing out with if statements. If you use primitive obsession, then you cannot use polymorphism.

Introduction

Design patterns can be divided into 3 different areas:

  • Creational: focuses on optimizing the creation of new objects. The basic method for object creation might add some design complexities. These patterns help remove the complexities.
    • Constructor: prepare the object for use, and initialize members.
    • Prototype: based on prototypal inheritance; we create objects which act as prototypes to other objects. The prototype is the blueprint (use of extends).
    • Module: a group of small independent components that we can reuse in our application. A module can be a class / function / variable. Helps in encapsulation and exposes only a public API, which internally manage the private components. (import, export, export default) "use strict" may be required.
    • Singleton: IMPORTANT: some people argue that you should never use the singleton pattern. But if you use it, it is used to restrict an object to a single instance. Can return the instance stored statically from the constructor. The main critique against singletons is that globals are bad. And a singleton is somehow a global. Another critique is that you will never know in advance whether you will only need a single instance forever. So it is better not to do that assumption. One man's constant, is another mans variable. And in testing you will often need to mock that single instance which is not a single instance anymore etc. It is ok to have a single instance of an object in your project, but it is not a good idea to disable the ability to create another.
    • Factory Method: abstract object creation by providing an interface. Where we specify what kind of object we want. Can be useful in:
      • Object setup requires a high level of complexity (e.g. it strongly depends on dynamic factors or external)
      • When we need to easily generate different instances of objects depending on the environment.
      • When different objects are generated from a common configuration, the factory can hold it, and allow for customization in the factory.
      • When composing objects with instances of other objects that need only satisfy a certain API contract (duck typing) to work. Good for decoupling.
    • Abstract Factory: creates an interface for creating families of related or dependent objects, without specifying their concrete classes. So the main difference between the factory pattern and the Abstract factory pattern is that the factory is mean to construct a single object, whereas the Abstract factory constructs multiple objects. These objects are related to each other. Example UI Alert dialog and Ok button. The abstract factory asks the factory implementation to construct the pair of objects for it, and the factory implementation can be for Windows, Linux, macOS, the abstract factory does not know which it is, and it does not care. It cares only that both objects complying to some interface or type can be constructed by the factory implementation.
  • Structural: When one part of the system changes, the entire system does not need to change too. Composition of objects, how they are made up, and how they relate to each other.
    • Strategy: "defines a family of algorithms, encapsulates each one and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it". Meaning that algorithm A, B and C become interchangeable and pluggable into the clients, but the clients need not change and need not know about the inner details of A, B or C. Strategy is using composition instead of inheritance. It is understanding that inheritance is not meant for code reuse. The problem with inheritance arises when you are forced to inherit from a class to make usage of some behavior, but then need to deviate in some of the behavior, and that deviation is partly shared by another class which also inherits from the superclass, but differs in other ways. Encapsulating each piece of behavior and plug it as needed is much simpler than making assumptions over a general behavior requirement (unless it is really required...).
    • Adapter: is taking a bunch of incompatible interfaces and making the compatible.
    • Decorator: adds functionality (additional responsibilities) to the base object by wrapping it into another object, which can be wrapped itself etc. Decorators present a successful alternative to subclassing. The thing is you add functionality at runtime instead of at compile time. Meaning that you do not alter the base object, you just pass it to a wrapper (decorator) which adds whatever is needed. The decorator has a component, and is a component itself (onion like). And the decorator can be passed around as the base object (meaning it has the same interface). The decorator has a decorated thing, to which it adds. For example if we have a beverage that we decorate with toppings, the getCost() method of the CreamToppgingDecorator could be written like: this.getDecoratedEntity().getCost() + 1. So it internally defers to the decorated entity to return it's cost, and adds on to it then returns. You can decorate a decorator itself, which would add into the call chain (inception).
      • Facade: hide complex code under smaller interface
      • Flywieght: used when you need to generate a large number of similar objects. You reduce memory usage by sharing objects that are similar in some way, rather than always creating new ones. For example drawing pixels, the size of the pixel is always the same, whereas colors change. So the intrinsic state is the color and the extrinsic state is the size. The extrinsic state can be extracted and reused across objects.
    • Proxy: provides a surrogate or placeholder for another object, in order to control access to it. It's a way to introduce a level of indirection. The adapter pattern changes the interface, but the proxy is a way of controlling the access, but does not change the interface.
      • Remote Proxy: when you need access to some thing that lives outside your application, for example when you fetch something from the server and you use a proxy to fetch it and return you a promise.
      • Virtual Proxy: controls access to a resource that is expensive to create. For example for caching.
      • Protection Proxy: controls access to a resource that you want to add ACL to.
    • Bridge: intends to decouple an abstraction from the implementation, so that, the two can vary independently. The adapter patter can resemble this, but the difference is that the adapter is added ex-post (after the fact) whereas the bridge is added precautiously to give your self the flexibility for the future.
  • Behavioral: focuses on assignment of responsibilities between objects, and how they communicate.
    • Mediator: When too many interactions happen between different objects, it is time to use a central mediator to which all talk to. The "mediator" is the central point, and the objects talking to each other through it are called the "Colleagues". A good analogy is an airport control tower and the planes talk to it. Colleagues have a reference to the Mediator to call its methods. And the mediator has references to all Colleagues in order to forward information.
    • Observer: an object changes its internal state and some other objects want to know that that happened. There are two ways of doing this: Poll vs Push. The Poll way, is to let each "Observer" ask the source whether it has changed; this poses the problem of too many requests to a single object (the "Subject") if many are polling all the time. The observer pattern is the switch from Poll to Push. Push lets the source tell the listeners that it has changed. How do you do this? There are many implementations possible. But basically, you hold a list of Observers, and whenever the source (the Observable or the Subject) has changed, it notifies it to each Observer in the list.
    • Command: is an attempt to encapsulate a command from one object (the sender) to another (the receiver) into an object. So that now, you can pass that command object around. Then we can even have a bunch of such commands (a queue or list of commands, (it's a combination with the composite pattern)). So we have the receiver and the sender, and we encapsulate the command into some kind of object and it can be done and undone. You have the invoker, which triggers the command, and then the receiver is altered by that command (the command acts upon the receiver). The invoker requires a simple interface from the commands it must have: do and undo.