DEV Community

Cover image for Unpacking JavaScript 02: OOJS part 4 - Design Patterns
sk
sk

Posted on

Unpacking JavaScript 02: OOJS part 4 - Design Patterns

The more complex classes or objects become the harder they are to manage, and/or extend. Design patterns are common solutions to those common problems in OOP.

There exist great books out there about design patterns and excellent blogs, but most of them cover typed languages, however dofactory.com has a dedicate series covering 23 designs patterns in JS. If I were to cover design patterns in this Article I will definitely not do them justice, they deserve their own book, that is how rich and extensive they are, just to wet your buds I will cover one from each category.

Design patterns are categorized into Structural, Creational and Behavioral design patterns

Creational – efficient and flexible creation of objects

Creational provide means/patterns to create efficient, flexible and reusable objects. I will cover only the factory method, one pattern you may be already familiar with in this category is the prototypical method - creating objects from other objects,

Factory Method

client delegates object creation to a factory

think of the client as a user of the object and factory as a provider.

The client only knows what object it wants and can instruct the factory to make and return that particular object, the client does not need to know how and where the object was made, as long as it conforms to the expected API by the client. In some sense it is a real factory.

Let’s say our client is a Nike store and Factory method well a Nike factory, the store puts an order for a specific Nike shoe to the factory, what the factory does the store does not know, not even care, however the store is expecting a type of Nike, that conforms to their description and need as per the order.

  // Factory 
 // create Nike Airforce 1 object
class Nike_Air_Force1 {

   constructor(name, type){
      this.name = name;
      this.type = type;

    }

 print(){

  console.log(this.name, this.type)

 }

}



// class to create Air Max object 

class Nike_Air_MAX1{

 constructor(name, type){

  this.name = name

  this.type = type

 }

 print(){

 console.log(this.name, this.type)

 }

}

// Factory, receives a message from the client, and decide which shoe(object to create and return)

// actual factory method

class factory{

 // factory api, all the client provides is the type of object needed

// the factory handles the rest and return a relevant object

 createNike = (type) => {

 switch(type){

   case "AirForce":

      return new Nike_Air_Force1("Air force 1", "low")

   case "Max":

      return new Nike_Air_MAX1("Air Max 1", "THE ORIGINAL")

   default:

     break

   }

 }

}


// end of factory 


// client 

let fact = new factory()  // client access to the factory

let force = fact.createNike("AirForce")  // place order and a
// relevant object(created from the class) is returned

force.print();

Enter fullscreen mode Exit fullscreen mode

behavioral - facilitates communication between objects

probably the most useful patterns, I've used them a lot, they open new possibilities when working with multiple objects that need to talk to each without knowing about each other(loosely coupled)

Observer pattern –

found this explanation on wiki and the implementation follows that spec also - defined as a pattern where an object named "subject", maintains a list of dependents called observers, and notifies them automatically of any state changes, by calling one of their methods

The wiki implementation is kinda lacking thou, we going to make it a little better here.

class Subject{
    subs = [] // maintains a list of its dependents called observers

    state = {name: "The 100"}  // when it changes subs will be notified

     subscribe(fn){

        this.subs.push(fn) // subscribing

      // we return a function that has a reference to our function we

      //subscribed with(fn), the returned function can be used to unsubscribe

   // which is basically removing the pushed function out of the subs list

  return (

      () => {

     console.log(fn, 'unsubing')

       this.subs = this.subs.filter(l => l !== fn) // unsubscribing

       }

  )

 }


// notifying the subscribers of any change in the state

 change(){

   this.subs.forEach((subscriberFn)=> {

     subscriberFn(this.state); // notifying all subs and passing new state

 })

 }

// changing/setting state 
setName(name){

 this.state.name = name // change state

 this.change()  // notify all subs of the change

}

}


// usage 
 let sub = new Subject()

// returns an unsubing function

let unsub = sub.subscribe((state)=> {

 console.log(state.name) // code here will be executed when the state of "subject" changes

})

// changing state

sub.setName("GOT") // GOT will be logged

sub.setName("House of The Dragon") // same

// unsubscribing

unsub()

console.log(sub.subs) // should log an empty array

Enter fullscreen mode Exit fullscreen mode

Simple right? yet powerful. From the above you can create a simple state manager, of course there are nuances to observables like subscribing to continuous streams etc and there are packages like RxJS dedicate to such.

Structural –

bringing different objects together to form a meaningful, cohesive, and flexible structure.

Façade pattern –

creating an encompassing object, a single point API encapsulating many related objects.

Façade groups related objects, objects that usually interact with each other or called in sequence.

For a comprehensive explanation, I’ll suggest this article, which also implements a façade on react hooks

Adopting from that example, how about objects that work together to make a perfect coffee.


// objects to be called/used in sequence
 class Ratio{

  coffee_ratio(){

    console.log("picking perfect coffee ration")

  }

 }

  class Brewing{

     pick_method(){

        console.log("picking brewing method")

    }

}


 class Water{

     Ideal_temperature(){

      console.log("reached ideal temp")

      this.brew_with_ideal_temp()

    }

    brew_with_ideal_temp(){

        console.log("brewing with ideal water temp")

    }

 }

 class Time{

    brew_time(){

    console.log("brewing the correct amount of time")

    }

 }


Enter fullscreen mode Exit fullscreen mode

To make coffee it is obvious you have to create each object and call each necessary method every time you make a new coffee, façade pattern says why not create a single object that will do all these steps because they are repetitive and follow a similar pattern all the time.

 class CoffeeFacade{
                 // con take objects to be used
        constructor(Ratio, Brewing, Water, Time ){

                this.Ratio = Ratio;

                this.Brewing = Brewing;

                this.Water = Water;

                this.Time = Time

        }

        // using the objects in sequence
    makeCoffe(){

            this.Ratio.coffee_ratio();

            this.Brewing.pick_method();

            this.Water.Ideal_temperature();

            this.Time.brew_time()

    }

 }


// usage

Enter fullscreen mode Exit fullscreen mode

To make coffee you just call the single make coffee method in the façade anytime you need a coffee, instead of starting afresh and creating all necessary objects every time, this is powerful.


 let coffeMaker = new CoffeeFacade(new Ratio(), new Brewing(), new Water(), new Time())

 coffeMaker.makeCoffe()  // to make a new coffee

Enter fullscreen mode Exit fullscreen mode

Conclusion

Designs patterns will improve your code drastically, I do suggest getting familiar with them.

next up in the series we will cover inheritance as a last article in OOJS and then move to async code.

Top comments (0)