JavaScript and Design Patterns - Chapter 5 πŸš€

Cover Image for JavaScript and Design Patterns - Chapter 5 πŸš€
Lazar Stankovic
Lazar Stankovic

πŸ€“ Intro

Welcome, hackers! Today, we are talking about yet another Design Pattern , the Adapter Pattern. πŸš€


πŸ”Œ ABOUT ADAPTER PATTERN

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It can convert an interface of the class into an interface that another class expects. That way it allows classes to function together which normally couldn't be possible due to their incompatible interfaces.


πŸ“Š STOCK MARKET EXAMPLE

Imagine that you need to create a stock market monitoring application. The app downloads the stock data from multiple sources in XML format and then displays nice-looking charts and diagrams for the user. At some point, you decide to improve the app by importing some 3rd-party analytics library. But, you encounter the problem. The 3rd-party library only works with data in JSON format.


πŸ€” What should we do?


Proposition: We could change the library to work with XML.


Yes, we could do it, but that might break some existing code that relies on the library, or worse, you might not have the library's source code in the first place, making this approach impossible.


πŸ’‘ REVELATION

To solve this problem, we could create an adapter. The special object that converts the interface of one object in a way that another object can understand it. An adapter will wrap one of the objects to hide the complexity of the conversion happening behind the scenes. The wrapped object isn't even aware of the adapter.


βš™ HOW DOES IT WORK?

- The adapter gets an interface, compatible with one of the existing objects

- Using this interface, the existing object can safely call the adapter's methods

- Upon receiving a call, the adapter passes the request to the second object, but in a format and order that the second object expects


In the Stock Market example, we could create XML-to-JSON adapters for every class of the analytics library that your code works with directly. Then, we can adjust our code to communicate with the library only via these adapters. When an adapter receives a call, it will translate all incoming XML data into a JSON structure and it will pass the call to the appropriate methods of a wrapped analytics object.


πŸ‘€ VISUAL REPRESENTATION

[@portabletext/react] Unknown block type "image", specify a component for it in the `components.types` prop


- TARGET - It defines the specific interface that is used by the Client class

- ADAPTER - It adapts the interface of the class Adaptee towards the interface of the class

- ADAPTEE - It defines an existing interface that should be adapted

- CLIENT - It looks after objects that require an interface of the Target class


Let's explain this by using an interesting real-world science example.


πŸ§ͺ CHEMICAL COMPOUND EXAMPLE

1//Target - It defines the specific interface that is used by the Client class
2class Compound {
3  //setting up initial valules - self explanatory :)
4  constructor(name) {
5    this.name = name;
6    this.bolingPoint = -1;
7    this.meltingPoint = -1;
8    this.molecularWeight = -1;
9    this.molecularFormula = -1;
10  }
11
12  //setting compound name
13  setCompound(name) {
14    this.name = name;
15  }
16
17  //name getter
18  display() {
19    return this.name;
20  }
21}
22
23//Adapter - It adapts the interface of the class Adaptee towards the interface of the class
24class RichCompound extends Compound {
25  constructor(name) {
26    super(name);
27  }
28  /* This function creates Chemical Databank for each 
29   new Rich compound that we are creating*/
30  display() {
31    //creating a new chemical databank
32    this.bank = new ChemicalDatabank();
33    //getting the boiling point based on the chemical name and indicator B === "Boiling"
34    let boilingPoint = this.bank.getCriticalPoint(this.name, 'B');
35    //getting the melting point based on the chemical name and indicator M === "Melting"
36    let meltingPoint = this.bank.getCriticalPoint(this.name, 'M');
37    //getting the molecular weight based on the chemical name
38    let molecularWeight = this.bank.getMolecularWeight(this.name);
39    //getting the molecular formula based on the chemical name
40    let molecularFormula = this.bank.getMolecularStructure(this.name);
41
42    //displaying all necessary information
43    console.log(`πŸ§ͺ Name: ${super.display()}`);
44    console.log(`πŸ‘©β€πŸ”¬ Formula: ${molecularFormula}`);
45    console.log(`πŸ‹οΈβ€β™€οΈ Weight: ${molecularWeight}`);
46    console.log(`❄ Melting Pt: ${meltingPoint}\u00B0C`);
47    console.log(`πŸ”₯ Boiling Pt: ${boilingPoint}\u00B0C`);
48  }
49}
50
51//Adaptee - It defines an existing interface that should be adapted
52class ChemicalDatabank {
53  //databank - taken from the 'legacy API'
54  getCriticalPoint(compound, point) {
55    let temperature = 0.0;
56    //freezing point
57    if (point == 'M') {
58      switch (compound.toLowerCase()) {
59        case 'water':
60          temperature = 0.0;
61          break;
62        case 'benzene':
63          temperature = 5.5;
64          break;
65        case 'alcohol':
66          temperature = -114.1;
67          break;
68      }
69    } else {
70      //boiling point
71      switch (compound.toLowerCase()) {
72        case 'water':
73          temperature = 100.0;
74          break;
75        case 'benzene':
76          temperature = 80.1;
77          break;
78        case 'alcohol':
79          temperature = 78.3;
80          break;
81      }
82    }
83    return temperature;
84  }
85
86  getMolecularStructure(compound) {
87    let structure = '';
88    switch (compound.toLowerCase()) {
89      case 'water':
90        structure = 'H2O';
91        break;
92      case 'benzene':
93        structure = 'C6H6';
94        break;
95      case 'alcohol':
96        structure = 'C2H6O2';
97        break;
98    }
99    return structure;
100  }
101
102  getMolecularWeight(compound) {
103    let weight = 0.0;
104    switch (compound.toLowerCase()) {
105      case 'water':
106        weight = 18.015;
107        break;
108      case 'benzene':
109        weight = 78.1134;
110        break;
111      case 'alcohol':
112        weight = 46.0688;
113        break;
114    }
115    return weight;
116  }
117}
118
119//unadapted compound
120const unadaptedCompound = new Compound('Unknown');
121console.log(`❌ Unadapted compound: ${unadaptedCompound.display()}`);
122
123//adapted compounds
124const water = new RichCompound('Water');
125water.display();
126
127const benzene = new RichCompound('Benzene');
128benzene.display();
129
130const alcohol = new RichCompound('Alcohol');
131alcohol.display();
132


Pretty interesting, right? 😎 Don't hesitate to play with the code.


βš’ APPLICABILITY

- You can use the Adapter pattern when you want to use some existing class, but its interface isn't compatible with the rest of your code. The adapter pattern, lets you create a middle-layer class that serves as a translator between your code and legacy class, a 3rd-party library, or any other class with a weird interface

- Use the pattern when you want to reuse several existing subclasses that lack some common functionality that can't be added to the superclass. You could extend each subclass and put the missing functionality into new child classes. However, you'll need to duplicate the code across all of these new classes, which is not good


βœ… PROS

- Single Responsibility Principle. You can separate the interface or data conversion code from the primary business logic of the program

- Open/Closed Principle. You can introduce new types of adapters into the program without breaking the existing client code, as long as they work with the adapters through the client interface


❌ CONS

- The overall complexity of the code increases because you need to introduce a set of new interfaces and classes. Sometimes it’s simpler just to change the service class so that it matches the rest of your code


πŸ™ THANK YOU FOR READING!