Object Oriented Design (OOD)

Let’s start this article with going over the SOLID design principles for Object Oriented design and then seeing how the listed design pattern captures the some of the principles defined in it.

  1. S: Single Responsibility Based on this principle, a class definition should focus on single responsibility. A class should not try to encapsulate the business logic for doing multiple things, rather it should focus on doing just one thing and delegate other logic to objects of classes responsible for doing just that.

  2. O: Open & Closed Based on this principle, an entity or class should be defined in such a way that future enhancements are based on the idea of extensibility of the class rather than modification to the class. This kind of pulls down from the first point, if the class tries to do multiple things, then future enhancements will lead to modifications in the class. Rather it means that business logic should be separated out using composition and inheritance to enhance the functionality of the class.

  3. L: Liskov Substitution This principle talks about ability to substitute a parent class or base type, with their child and sub type. Basically it talks about the usage of polymorphism when defining the variables in functions or arguments or return types. It says to use base types during encapsulation, rather than using concrete types. And to say code around interfaces and types rather than concrete classes. This helps to provide extensibility of the code, when new implementations are available, moreover it lets to change the implementation at runtime using polymorphism. Following this principle provides lot of control, in creating feature flags around choosing a particular implementation and easier roll back and testing for different implementations.

  4. I: Interface Segregation This principle talks about creating smaller interfaces, and segregating their behavior as much as possible. This helps in development of concrete implementations, where they have to implement minimal needed behavior for their implementation. Rather than implementing a bigger interface, it is better to implement two separate interfaces. For example in golang it is done by providing separate interfaces for Reader and Writer. A Writer just cares about writing the bytes, and there could be class which just implements the Writer for example when implementing custom template on an object.

  5. D: Dependency Inversion This principle follows the Liskov Substitution principle. Basically saying that object entities should encapsulate or depend upon abstractions i.e. interface types rather than concrete class implementations.

Overall I think all these five principles can be summarized while designing code as such:

Define simple segregated interfaces, which leads to creating concrete classes which have single responsibility and have the ability to be enhanced and extended in the future. Such extensions or enhancements are decoupled from the implementations by substituting the usage of concrete implementations with the interfaces in the code, such that dependencies in system are only defined in terms of abstractions and not level implementations

Another principle, I would like to add here is:

Favor composition, over inheritance

UNIX philosophy#

  • Each subcommand should do one thing and do it well : Commands should be of singular responsibility.

  • Write components that work together like with piping in unix or redirection of input and output

  • Make it easy to read, write, and, run components : reduce complexity in the components.

  • Design patterns

Structural Design patterns#

Decorator or Wrapper Pattern#

A Decorator pattern adds a layer of decoration or enhancement to original functionality. A typical example would be to enhance a Shape type with Color or Shade. So that the behavior from the Shape type like draw can updated with enhancements like color type or shade type.

The decorator class/function is created with both has and is relationship, meaning that class/func has or contains the entity it is decorating, and also that it is of the same type as the entity it is decorating. This decoration can be with functional injection as well as with Object based injection.

Let’s look at examples:

  1. Object based decoration

Let’s say there is a shape type, with a behavior draw, which draws the shape to a Screen Context.

type Shape interface {
  draw(sc ScreenContext)
}

This Shape can have multiple concrete implementations like Circle or Rectangle.

type Circle struct {
  radius int
}

func (c *Circle) draw(sc ScreenContext) {
  sc.Print(c.addCircle())
}

Now if we want to add color to the every Shape possible. One of the ways could be to update the draw for every implementation of Shape, to add a given color to the ScreenContext. But this means changing concrete implementation and also, updating their tests based on the color. Also a color is an optional enhancement to the Shape so if we add in the draw this will probably be with If clause. Also there can be future enhancements like Shade or Shadow or Hue etc. Every time adding this in the concrete Shape type will be too much work.

So here is where a Decorator or Wrapper pattern shines. To do this, we add a new Shape called as ColorShape, which has a Shape and Color and it’s own draw behavior, which adds the color first and then delegates the drawing of the Shape it encapsulates.

type ColorShape struct {
  Shape s,
  Color c,
}

func (cs *ColorShape) draw(sc ScreenContext) {
  sc.setColor(c)
  s.draw(sc)
}

Now to use to decorate any shape with color is pretty straight forward.

type Color string

const (
  RED Color = "RED"
)

func main() {
    sc := &ScreenContext{}
    circle := &Circle{
      radius: 5,
    }
    colorCircle := &ColorShape{
      s: circle,
      c: RED,
    }
    // Draws the colored shape (circle)
    colorCircle.draw(sc)
}

Strategy Design Pattern#

A strategy pattern is used to define implementation of different strategies. As common examples of this could be implementation of different payment gateways or could be different implementation of operations in a calculator or way to write bytes to an entity like buffer, file or network connection.

To implement the strategy design patterns, create an interface which can define which defines the behavior of the strategy. For example in case of Payment Gateways this could be payment or in case of calculator this could be execute. The concrete implementations of these strategies, implement the interface and implement the behavior of the strategies.

This pattern can be useful in adding iterations over the course of time, for example Initially on Credit Card payment gateway is present. If we add that directly in the PaymentManager class, then when a new gateway is added, that will require update to the payment class logic to add another gateway. Similarly if more gateways are added, then the logic becomes more complicated and moreover it breaks the Open Close principle of SOLID. So a better way would be to implement a GatewayStrategy interface with payment behavior and then selecting the Strategy based on the user selection in the PaymentManager class.

We also see this in golang with the Writer interface and then classes implementing the strategy, like Buffer or File. Now if you want to write bytes to a Buffer or File, should not make a difference at the client end, which is expecting just a Writer.

For example, lets take example of a calculator:

// strategy interface, defining the strategy behavior
type Operation interface {
  execute(float32, float32) float32
}

// Two strategies implement the strategy interface Operation
type AddOperation struct {}

func (o *AddOperation) execute(x, y float32) float32 {
  return x + y
}

type MulOperation struct {}

func (o *MulOperation) execute(x, y float32) float32 {
  return x * y
}

State design pattern#

Now let’s look at state design pattern and how to use it. state design pattern is really useful designing state automation. Usually in workflow automation system has this DAG which is a state flow automation along the DAG. Describing the DAG with if else clause in one file becomes really complicated as the number of state increases and also becomes really hard to debug. Also when new states get added, the complexity increases multifold.

A good way to break down this would be to use state design pattern. Where each state class describes the work and the transitions to next states based on the conditions described in their concrete state classes.

Let’s take an example of a Upgrade Workflow transition for example:

A upgrade workflow transition system contains many states, for example:

  1. Muting of Alerts (MuteState)
  2. Upgrade Software (UpgradeState)
  3. Unmute Alerts (UnmuteState)

Idea here is each state implements a common State interface, is defined as a separate entity, responsible for defining the steps for that state and also defining the next transition from that state.

The StateManager or WorkflowContext is responsible for executing the states and calling the state dependencies.

import (
	"fmt"
	"time"
)

type State interface {
	Execute() (State, error)
}

type MuteState struct{}

func (s *MuteState) Execute() (State, error) {
	fmt.Println("Doing muting of alerts")
	// Do transition to next state, one way is to return the next state
	nextState := &UpgradeState{}
	return nextState, nil
}

type UpgradeState struct {}

func (s *UpgradeState) Execute() (State, error) {
	fmt.Println("Starting upgrade of the system")
	fmt.Println("Wait for some time...")
	time.Sleep(1 * time.Second)
	// Do transition to next state, one way is to return the next state
	nextState := &UnmuteState{}
	return nextState, nil
}

type UnmuteState struct{}

func (s *UnmuteState) Execute() (State, error) {
	fmt.Println("Unmuting the alerts after upgrade done")
	// No next state to transition
	return nil, nil
}

type UpgradeWorkflowContext struct {
	currentState State
}

func (s *UpgradeWorkflowContext) ExecuteWorkflow() error {
  // This logic depends upon how we want to do transition.
	for s.currentState != nil {
		nextState, err := s.currentState.Execute()
		if err != nil {
			return err
		}
		s.currentState = nextState
	}
	return nil
}

func (s *UpgradeWorkflowContext) SetState(state State) {
	s.currentState = state
}

func main() {
	// Upgrade Worflow
	upgradeworflow := state.UpgradeWorkflowContext{}
	startState := &state.MuteState{}
	upgradeworflow.SetState(startState)
	err := upgradeworflow.ExecuteWorkflow()
	if err != nil {
		log.Fatalln(err)
	}
}

This design pattern amplifies the principles of:

  1. Single Responsibility: Each state is responsible for its own execution and dependencies.
  2. Open & Closed: Adding new state, doesn’t change other states in the whole execution, also states can be reused and extended further in different workflows.
  3. Liskov Substitution & dependency Inversion: Rather than defining the complex if condition to define the state management. A Manager uses a common state interface to run different states and move through the workflow.

Note: Although this pattern is quite powerful, care should be taken to apply it when the states are too many to manage. If there are 2-3 fixed states in the system then it might be overkill.

More patterns to cover later:

Proxy Design Pattern:#

Facade Design Pattern#

Composite Design Pattern#

Behavior Design Patterns:#

Command Design Pattern#

Visitor Design Pattern#

comments powered by Disqus