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();
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
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")
}
}
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
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
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)