Skip to content

Latest commit

 

History

History
157 lines (104 loc) · 5.17 KB

design_patterns.org

File metadata and controls

157 lines (104 loc) · 5.17 KB

Design Patterns

Patters allow us to separate the part of the application that changes from the other parts, so that a new change does not break the old stuff. This is done via encapsulation, decoupling, delegation, composition.

composition over inheritance

There are some ideas that are widely applicable in software development. One such idea is, favoring composition over inheritance (java style inheritance for example).

The issue with java style inheritance is, it’s rigid; if the parent has a behavior (method), all children of that parent have to have that behavior.

Consider some class Foo:

assets/2022-06-11_12-21-46_screenshot.png

All the children it has, they must implement both a() and b(). Now, it is possible that even though all the subclasses of the same children, they might have some differences amongst themselves, let’s say some of them have another method c() as well.

There can be various ways to incorporate that method:

Using an interface

assets/2022-06-11_12-22-02_screenshot.png

This works, but we are losing code reusibility. If there are some multiple subclasses that need a particular type of behaviour, each has to implement it separately.

Adding c() to parent class - foo

assets/2022-06-11_12-22-19_screenshot.png

We can add that method to parent class, maybe with some default behaviour, and the children can override that with no-op if required.

This is bad because we broke previously working classes, and also have to remember to overwrite the implementation for all future subclasses.

Using delegation

We want to separate the behavior that changes from the behavior that remains constant, aka we want to encapsulate the changing behavior into a separate class.

In this case, we can add an instance variable to the Foo class, that is of the type C.

Adding an instance variable

assets/2022-06-11_12-23-27_screenshot.png

The type of the instance variable can be an interface. The method c() will just invoke the interface method on the instance variable. To make sure that the instance variable is always set, we can provide 2 constructors, one which accepts the instance variable and the other that sets the default value for it.

This way, we can even change the behavior of the class at runtime by using getters and setters for that instance variable.

Here, we are using delegation to delegate the behavior to another class. This gives us code reusibility and also doesn’t break existing code.

Composition

In instance variable example, we are using composition to compose our parent class with the right behaviors as required.

// abstract duck class, with an instance variable
public abstract class Duck {
  Flyable fly;

  abstract void squeak();

  void doFly() {
    this.fly.doFly();
  };
}


// interface defining the behavior
public interface Flyable {
  void doFly();
}


// slow fly is a kind of a flyable behavior
public class SlowFly implements Flyable {

  @Override
  public void doFly() {
    System.out.println("made a slow flight");
  }
}



// no fly is another kind of flyable behavior
public class NoFly implements Flyable {

  @Override
  public void doFly() {
    System.out.println("cannot fly");
  }
}


// rubber duck cannot fly, so is composed with nofly
public class RubberDuck extends Duck {
  @Override
  void squeak() {
    System.out.println("rubber duck squeaks");
  }

  public RubberDuck() {
    this.fly = new NoFly();
  }

}


// wood duck can fly if it is given a flyable
public class WoodDuck extends Duck {

  public WoodDuck(Flyable f) {
    this.fly = f;
  }

  public WoodDuck() {
    this.fly = new NoFly();
  }

  @Override
  void squeak() {
    System.out.println("wood duck grrs");
  }
}


public class DesignPatterns {
  public static void main(String[] args) {
    Flyable slowFly = new SlowFly();

    Duck woodDuck = new WoodDuck(slowFly); // giving the woodduck a flyable behavior
    woodDuck.squeak();
    woodDuck.doFly();

    Duck rubberDuck = new RubberDuck(); // whereas the rubber duck cannot fly
    rubberDuck.squeak();
    rubberDuck.doFly();
  }
}

Points to note:

  • type of the instance variable for the fly behavior is an interface, not a concrete class type - this gives us more flexibility. It is always good to program to an interface, not a concrete type. (the interface here can be a superclass too)
  • composition allows us to change the fly behavior dynamically, via the getters and setters on the instance variable
  • composition is a has a relationship (rubber duck has a flyable behavior), whereas inheritance is a is a relationship
  • composition allows us to use delegation
    • we delegated the logic of the flying to a separate class
    • this allows us to add new flying behaviors and assign it to old classes dynamically (decoupling b/w behavior and duck class)
  • this is called the strategy pattern, because we have a family of behaviors, as defined by the implementors of the flyable behavior and our strategy is to delegate some of our behavior to those classes