JavaScript Design Patterns Explained
JavaScript Design Patterns Explained
JavaScript Design
Patterns
Build scalable applications
with object-oriented
programming and advanced
pattern techniques
[Link]
First Edition 2025
eISBN: 978-93-65898-842
[Link]
Dedicated to
This book has been a long time in the making, with its
journey spanning both professional challenges and personal
growth. It would not have been possible without the
unwavering support of several remarkable individuals. To
begin with, I must express my deepest gratitude to my
family—Mr. Mulraj Dipchand Shah, my father; Mrs.
Harsha Mulraj Shah, my mother; and Mrs. Unnati
Rushabh Shah, my wife. Coming from a conservative
family, my parents had the courage to defy societal norms
and encourage me to pursue a career in technology—a path
no one in my extended family had dared to tread. Their
sacrifices and steadfast belief in my potential have been the
cornerstone of everything I have achieved. My wife, Unnati,
has been my rock through this journey. A dear friend for
over a decade before we married, she has always stood by
me with unshakable faith, cheering me on through minor
victories and major setbacks alike.
I owe a significant part of my career to John Thomas, a
former colleague and mentor who became a lighthouse
during one of the most turbulent phases of my corporate
journey. For an introverted individual with limited
connections, navigating the corporate world felt like scaling
a mountain, but John’s guidance helped me find direction
and purpose. I also want to thank Supriya Adep, a close
friend who, with her sharp judgment and candid advice—no
matter how unvarnished—has consistently helped me make
sound decisions. Both of you have left an indelible mark on
my personal and professional growth, and I am deeply
grateful.
Finally, none of this would have been possible without the
incredible team at BPB Publications. Their diligence,
encouragement, and constant follow-ups played a vital role
in bringing this book to life. I hope this work serves as a
resource for readers and helps ignite meaningful change in
the world of technology.
Preface
[Link]
The code bundle for the book is also hosted on GitHub at
[Link]
JavaScript-Design-Patterns. In case there’s an update to
the code, it will be updated on the existing GitHub
repository.
We have code bundles from our rich catalogue of books and
videos available at [Link]
Check them out!
Errata
We take immense pride in our work at BPB Publications and
follow best practices to ensure the accuracy of our content
to provide with an indulging reading experience to our
subscribers. Our readers are our mirrors, and we use their
inputs to reflect and improve upon human errors, if any, that
may have occurred during the publishing processes
involved. To let us maintain the quality and help us reach
out to any readers who might be having difficulties due to
any unforeseen errors, please write to us at :
errata@[Link]
Your support, suggestions and feedbacks are highly
appreciated by the BPB Publications’ Family.
Did you know that BPB offers eBook versions of every book published, with
PDF and ePub files available? You can upgrade to the eBook version at
[Link] and as a print book customer, you are entitled to a
discount on the eBook copy. Get in touch with us at :
business@[Link] for more details.
At [Link], you can also read a collection of free technical
articles, sign up for a range of free newsletters, and receive exclusive
discounts and offers on BPB books and eBooks.
Piracy
If you come across any illegal copies of our works in any form on the internet,
we would be grateful if you would provide us with the location address or
website name. Please contact us at business@[Link] with a link to
the material.
Reviews
Please leave a review. Once you have read and used this book, why not leave
a review on the site that you purchased it from? Potential readers can then
see and use your unbiased opinion to make purchase decisions. We at BPB
can understand what you think about our products, and our authors can see
your feedback on their book. Thank you!
For more information about BPB, please visit [Link].
Index
CHAPTER 1
Introduction to JavaScript
Design Patterns
Introduction
In the digital world of the 21st century, JavaScript has
scripted itself to be the cornerstone that powers almost the
entire web landscape. Understanding and implementing
JavaScript is vital for meaningful contributions in today’s
Internet-centric society. However, mastery of JavaScript
does not solely lie in rote-learning its syntax or
functionalities. To truly master JavaScript, one needs to
learn the art of optimizing and structuring the code
effectively. In this chapter, we are going to get a brief
understanding of JavaScript and its rise to prominence,
which makes it a pivotal part of building the web. We will
then move ahead to understand the common pitfalls a
JavaScript developer might run into. These pitfalls will
finally lead us to our primary focus – Design Patterns.
Throughout this chapter, you will gain insight into the idea
of Design Patterns, its origins, its contribution in helping us
code JavaScript better, and its monumental significance in
helping build scalable applications.
Structure
This chapter will cover the following topics:
• JavaScript: Origins
• Common pitfalls in JavaScript
• Design Patterns
• Origins of Design Patterns
• Advantages of Design Patterns
• Solutions to problem statements
Objectives
Upon completing this chapter, you will possess a
comprehensive grasp of Design Patterns and their
paramount importance in coding practices. Additionally,
you will refresh your understanding of JavaScript’s roots
and its latest advancements, as of May 2024. You will also
gain profound insights into prevalent pitfalls encountered
by JavaScript developers and discern how the
implementation of Design Patterns can effectively rectify
these issues.
JavaScript: Origins
Before we begin this section, please note that this section
of the chapter will be strictly talking about the origin of
JavaScript and its ascension to become the de facto
language of the web. We will not be doing a detailed
understanding of the core concepts of JavaScript and its
affiliations. The primary reason to understand JavaScript’s
origins is to ascertain the problems faced by the web
community before its inception and set the base for
understanding Design Patterns for JavaScript. If you wish
to refresh your knowledge of JavaScript, we recommend
you go through the basics of the language before restarting
this book. It is of paramount importance to have a crystal-
clear understanding of the language before we embark on
our journey to learn Design Patterns.
Inception of JavaScript
In the early 90s, Netscape Communications Corporation
developed and released the Netscape Navigator, one of the
first web browsers with a graphical user interface. While its
popularity soared, it was still bound to display static
websites with limited interactivity. To address this
limitation, Netscape brought in Brendan Eich, an American
computer programmer, to develop a language which can
introduce website interactivity. A year later, Eich
successfully created a new scripting language that
perfectly served this purpose. Cashing in on the dot-com
boom, the rise of the Internet, and the popularity of Java,
Eich allegedly decided to call this new scripting language
JavaScript.
Revival of JavaScript
It would take ten more years and the introduction of some
fascinating new technologies like AJAX and jQuery to finally
revive JavaScript from its impending doom. In 2008, Google
debuted its Chrome browser, which surpassed the records
for usage statistics set before. Its brand-new V8 JavaScript
engine and just-in-time (JIT) compilation made it
imperative for other browser vendors to revamp their
engines. It ultimately culminated in all disparate parties
coming together for a conference, and the ECMA officially
released the ECMAScript 5 (ES5) in December 2009.
While ES5 played a pivotal role in standardizing JavaScript
and remains a widely supported version, modern JavaScript
has evolved significantly with newer standards like ES6
(2015) and beyond, which introduce advanced features and
capabilities to the language. The interest in client-side
scripting increased manifold, which led to the release of
the popular ECMAScript 6 (ES6) in 2015. At the time of
writing, in 2024, popular web frameworks like ReactJS,
[Link], and several other libraries are heavily built on ES6.
Since 2015, the World Wide Web Consortium (W3C)
organization meets once every year to release an improved
version of ECMAScript that makes writing JavaScript easier
and more efficient.
Common pitfalls in JavaScript
Being the de facto language of the web, JavaScript is
ubiquitous and versatile in its approach. However, writing
JavaScript code also presents challenges. Novice and
expert developers can often find themselves running into
problems they might not anticipate.
To demonstrate this, we will look at a few simple but
common problems every developer might have faced while
coding. We will first look at the problematic code and then
try to understand what the issue with the code could be. To
make it challenging, try to figure out the problem with the
code on yourself without looking at the description below
it.
Problem statement 1
In the following code, we have declared a global variable by
the name counter:
1. var counter = 0;
2.
3. function incrementCounter() {
4. counter++;
5. }
6.
7. function logCounter() {
8. [Link](counter);
9. }
10.
11. incrementCounter();
12. logCounter();
In the subsequent functions logCounter and
incrementCounter, we attempt to access and modify this
global variable counter. The problem with using a global
variable is that it is accessible to the entire application.
Intentionally or unintentionally, you might access or modify
this global variable in another file. In the case of large-
scale applications, it would become extremely difficult to
track changes and maintain global variables.
Problem statement 2
You might have encountered the following code while
studying asynchronous JavaScript and ES6:
1. getData(function(response1) {
2. getMoreData(response1, function(response2) {
3. getEvenMoreData(response2, function(response3) {
4. // ...and so on
5. });
6. });
7. });
There is no upper limit on how many callback functions you
can use in your code. As you can see from the coding
example above, the readability of the code decreases with
the increase in the number of nested callback functions. At
one point, it might reach a junction where the developer
might get lost with the number of requests to handle.
Problem statement 3
The following example looks pretty innocuous:
1. function createUser(name, age) {
2. return {
3. name: name,
4. age: age,
5. greet: function() {
6. return `Hello, my name is ${[Link]} and I am
${[Link]} years old.`;
7. }
8. };
9. }
10.
11. const user1 = createUser('Alice', 25);
12. const user2 = createUser('Bob', 30);
We have a function createUser that takes two arguments
name and age and returns an object. While this looks like
a proper solution, there are several problems:
• If multiple functions or parts of your code create the
same Person object, every function would need to
replicate or maintain consistency to create these
objects. A slight inconsistency might break your entire
application.
• The above code exposes the creation logic of the
object, and it is not encapsulated within a proper class
or a constructor. It is against the principles of object-
oriented programming (OOP) concepts.
Problem statement 4
Here, we have another example related to objects in the
following code:
1. const user = {
2. name: 'John',
3. age: 30,
4. };
5.
6. function updateUser(newUser) {
7. [Link](user, newUser);
8. }
9.
10. updateUser({ age: 31 });
11. [Link](user);
In this example, we have already declared an object. We
then create a function that takes the original object and
assigns it a new value. In this case, we are directly
mutating an object. It could be potentially dangerous. We
might not be able to track the mutations effectively, and a
wrong value might even crash the application.
Problem statement 5
In the following example, we have a function taking two
arguments and dividing them:
1. function divide(a, b) {
2. return a / b;
3. }
4.
5. try {
6. const result = divide(10, 0);
7. [Link](result);
8. } catch (error) {
9. [Link]('Error:', [Link]);
10. }
Design Patterns
Now that we have seen a few live examples of problem
statements and how a common theme or pattern can help
resolve them, let us go ahead and formally describe Design
Patterns. We can think of Design Patterns as a vocabulary
for learning a language. Design Patterns provide a proven
solution for a particular set of problems. Having an in-
depth knowledge of Design Patterns gives us an insight into
common issues faced by developers over the years. As a
famous saying goes, Prevention is better than cure, that is,
by having prior knowledge about these recurring problems,
we are already one step ahead by not falling into the same
trap. Moreover, possessing a solution carved and shaped
over the years by developers who have constantly faced
this problem and figured out a solution by applying several
permutations, combinations, and logic, gives us an added
advantage in writing robust code that will not crash our
application.
To sum up the above statement, a common definition that
you will find across the web when you search for Design
Patterns would be something along these lines:
Reusability
Design Patterns encapsulate solutions to recurring
problems in a generalized way. Once a pattern is
implemented and tested, it can be reused in different
scenarios, saving development time and effort. For
example, the Singleton Pattern provides a way to ensure a
class has only one instance, which can be reused across the
application.
Maintainability
Design Patterns promote a structured approach to
development. By following Design Patterns, code becomes
far more organised and modular. Since your code will be
more modular, it calls for separation of concerns. It will be
easier to maintain your code and handle errors in a better
fashion.
Scalability
Design Patterns encourage users to follow a module
approach, that is, to compartmentalise or divide your code
into smaller meaningful and independent sections. This
practice of dividing your code into meaningful atomic
pieces, in turn helps make your code scalable. Since your
code is meaningfully compartmentalised, it would be easier
to add new code or update the existing code without
making major modifications to the existing code, thereby
avoiding serious application crashes.
Common vocabulary
As you might have observed in the section Types of Design
Patterns, Design Patterns basically help in creating a
common set of words or phrases that can be easily
communicated between teams. In simpler words, it is
creating an instance of technical language that the
developers in a team can use to explain each other the
problems they intend to resolve. It is easier to communicate
with larger teams by using pattern names (like Factory,
Singleton) instead of trying to explain a lengthy piece of
code optimisation. It helps reduce misunderstandings and
improves collaboration within the team.
Performance improvement
Let us take the example of a Design Pattern, Flyweight,
that we will be covering in upcoming chapters. Flyweight
Design Pattern calls for objects to share common data
resources. This can improve performance significantly by
avoiding duplication while dealing with a large number of
similar objects.
Proven solutions
The primary purpose behind employing Design Patterns is
their status as verified solutions. Formulated through years
of experimentation, refinement, and addressing recurring
challenges, these patterns bestow robustness upon your
code by relying on established and proven practices.
Faster development
Design Patterns offer tried and tested solutions to
recurring issues. This spares developers of the added time
and effort required to constantly seek resolutions for
repetitive problems, enabling them to focus more on
implementing business logic and resolving other critical
issues.
Problem statement 1
We have seen that in the following code, by directly
modifying the global variable, we are introducing possible
inconsistencies to the global object that could potentially
crash the application:
1. var counter = 0;
2.
3. function incrementCounter() {
4. counter++;
5. }
6.
7. function logCounter() {
8. [Link](counter);
9. }
10.
11. incrementCounter();
12. logCounter();
This particular problem can be resolved by Module Pattern
(Prototype Pattern). Let us first look at the solution before
understanding it in detail.
1. var CounterModule = (function() {
2. var counter = 0; // Encapsulated variable
3.
4. function incrementCounter() {
5. counter++;
6. }
7.
8. function logCounter() {
9. [Link](counter);
10. }
11.
12. return {
13. increment: incrementCounter,
14. log: logCounter
15. };
16. })();
17.
18. [Link]();
19. [Link]();
Solution explanation: The Module Pattern utilizes an
Immediately Invoked Function Expression (IIFE) to
create a private scope. In this example, the variable
counter is encapsulated within the function, making it
inaccessible from the outer scope. Only the explicitly
returned methods (increment and log) are exposed to the
outside, allowing controlled interaction with the private
counter variable. This approach helps prevent unintended
global variable modifications and promotes clean, modular
code.
Problem statement 2
In the following code, we have seen that deep nesting of
code can result in a call back hell that makes code difficult
to read, thereby increasing the chances of errors creeping
in:
1. getData(function(response1) {
2. getMoreData(response1, function(response2) {
3. getEvenMoreData(response2, function(response3) {
4. // ...and so on
5. });
6. });
7. });
Here is a solution to handle this niggling problem:
1. getData()
2. .then(response1 => getMoreData(response1))
3. .then(response2 => getEvenMoreData(response2))
4. .then(response3 => {
5. // ...and so on
6. })
7. .catch(error => {
8. // Handle errors
9. });
Solution explanation: This solution demonstrates the use
of Promises, introduced in ES6, to write asynchronous code
in a readable and sequential manner. While Promises
themselves are not directly part of the Module or Prototype
Patterns, they support modular and maintainable coding
practices by chaining operations and centralizing error
handling through the catch block. This approach improves
code clarity and minimizes the likelihood of errors being
overlooked, especially in complex asynchronous workflows.
Problem statement 3
In the preceding section, we have seen that while the code
might look appropriate, there are two major problems with
it: maintaining consistency for the object creation and the
exposure of the crucial object creation logic. Refer to the
following code:
1. // Creating objects without encapsulation
2. function createUser(name, age) {
3. return {
4. name: name,
5. age: age,
6. greet: function() {
7. return `Hello, my name is ${[Link]} and I am
${[Link]} years old.`;
8. }
9. };
10. }
11.
12. const user1 = createUser('Alice', 25);
13. const user2 = createUser('Bob', 30);
Now, let us have a look at the solution:
1. // Constructor function for creating objects
2. function User(name, age) {
3. [Link] = name;
4. [Link] = age;
5. }
6.
7. [Link] = function() {
8. return `Hello, my name is ${[Link]} and I am
${[Link]} years old.`;
9. };
10.
11. const user1 = new User('Alice', 25);
12. const user2 = new User('Bob', 30);
Solution explanation: In this above case, we use an
instance of the Creational Design Pattern called the
Constructor Pattern and a Behavioural Design Pattern
called the Observer Pattern. By introducing a constructor
in the above code, we add encapsulation to the object
instance. Now, developers no longer need to create
separate functions to generate an object as the User
instance will effectively do the job. Additionally, by directly
using the prototype property of an object, it allows us to
add the greet method directly to the object’s constructor.
Problem statement 4
We have seen that in the following code, we are making the
mistake of directly mutating an object:
1. // Mutable object
2. const user = {
3. name: 'John',
4. age: 30,
5. };
6.
7. function updateUser(newUser) {
8. [Link](user, newUser); // Mutating the original
object
9. }
10.
11. updateUser({ age: 31 });
12. [Link](user);
This could be hazardous and it will be difficult to track
these mutations that could potentially crash an application.
1. // Using [Link] to create a new object (shallow
copy)
2. const updatedUser = [Link]({}, user, { age: 31
});
3. [Link](updatedUser); // Output: { name: 'John',
age: 31 }
Solution explanation: To resolve the problem of directly
mutating an object, we have to ensure that we first create a
shallow copy of the object by passing an empty object {} as
the first argument in the [Link] method. This
ensures that we do not mutate the original object and avoid
situations that might crash the application.
Problem statement 5
In the following code, we will see that improper error
handling might cause an application to crash:
1. // Inadequate error handling
2. function divide(a, b) {
3. return a / b;
4. }
5.
6. try {
7. const result = divide(10, 0);
8. [Link](result); // This line will not execute due to
the error
9. } catch (error) {
10. [Link]('Error:', [Link]);
11. }
To resolve this problem, we have made the following
modifications to the code:
1. // Improved error handling with Try-Catch
2. function divide(a, b) {
3. if (b === 0) {
4. throw new Error('Division by zero is not allowed.');
5. }
6. return a / b;
7. }
8.
9. try {
10. const result = divide(10, 0);
11. [Link](result); // This line will not execute due to
the caught error
12. } catch (error) {
13. [Link]('Error:', [Link]);
14. }
Solution explanation: In the above solution, we have
ensured that the error is handled properly. In the original
code, the statement [Link](result); would not have
been executed because the application would crash
showing either an improper message on the UI or a blank
screen. Either of the scenarios would not be good from the
user experience perspective. We handled this problem by
introducing a condition in the original function which will
throw an error if the function attempts to divide by zero.
With this additional logic, we will avoid crashing the
application but directly navigate the code to the error
handling piece where we can gracefully avoid crashing the
application.
Having observed how Design Patterns effectively address
frequently encountered issues, further chapters will delve
into these categories extensively. We will explore the
diverse range of patterns in depth, comprehending their
applications in different scenarios and elucidating how they
contribute to enhancing our code.
Conclusion
In this chapter, we started by understanding the origins of
JavaScript and traversing through the common pitfalls
encountered by developers while coding in JavaScript. This
laid the groundwork for introducing Design Patterns, a
concept that has existed in the software engineering
industry for a long time but was formally introduced in
1994 by the Gang of Four in their seminal book, Design
Patterns: Elements of Reusable Object-Oriented Software.
We explored the high-level classification of these Design
Patterns and finally understood the benefits of introducing
them in our coding.
As we delve deeper into specific patterns and their
applications in upcoming chapters, remember that
mastering these principles is not merely about memorizing
solutions but embracing a mindset that fosters elegant,
adaptable, and scalable software design. This journey into
Design Patterns is an invitation to harness the collective
wisdom, empowering us to craft software systems that
stand the test of time and evolve gracefully in response to
changing requirements and technological advancements.
Points to remember
• Design Patterns for JavaScript are proven solutions
that have been carved and shaped over the years by
developers who have figured out solutions to commonly
recurring problems in JavaScript.
• Design Patterns are broadly divided into three
categories based on their complexity and the extent to
which they are used: Creational Patterns, Structural
Patterns, and Behavioural Patterns.
• These patterns can facilitate easy communication
between developers in the same team and provide a
common language for them to architect a software
solution effectively.
• Design Patterns can make your code reusable, and easy
to maintain, and assist you in scaling your applications
by improving their performance.
• Design Patterns are not about memorizing a solution
but embracing a mindset that fosters elegant,
adaptable, and scalable software design.
Exercises
1. What are behavioural Design Patterns primarily
concerned with?
a. Object creation mechanism
b. Understanding the relationships between multiple
objects.
c. Packaging of objects to be shared across the Internet.
d. Effective communication between objects.
2. Which of the following statement is true?
a. Design Patterns cannot be applied to object-oriented
programming languages.
b. Design Patterns are fairly new concepts that have
never been utilised or proven for their efficiency.
c. Effective modularisation achieved by implementing
Design Patterns can make your code scalable.
d. The implementation of Design Patterns is restricted to
only one problem at a given time and it can never be
reused ever again.
3. Which of the following statements depict a proper
usage of Design Patterns in JavaScript?
a. Declaring a global variable at the start of the code
and then directly accessing it to modify its content.
b. Handling errors properly to avoid a situation where a
line of code is not reachable.
c. There is no need to use a constructor to create
properties or keys for an object. We can make use of
functions to achieve this purpose.
d. Directly mutating an object without making its
shallow or deep copy.
Answers
1. b
2. c
3. b
Introduction
Creational Design Patterns hold a prominent position in the
bedrock of JavaScript, a language firmly grounded in the
principles of OOP. Object creation stands as a critical yet
often underestimated facet of JavaScript programming.
Incorrectly creating objects not only amplifies code
complexity but also serves as a potential trigger for
application crashes. In this chapter, we aim to demystify the
intricacies surrounding object creation. We will explore
pivotal Creational Design Patterns such as Singleton,
Factory Method, Abstract Factory, Builder, and Prototype.
This exploration will provide a deep insight into establishing
adaptable and scalable object creation processes. These
patterns encapsulate creation logic, fostering the values of
code reusability, maintainability, and adaptability.
Structure
This chapter will cover the following topics:
• Object-oriented programming
• Introduction to Creational Design Patterns
• Singleton Pattern
• Factory Method Pattern
• Abstract Factory Pattern
• Builder Pattern
• Prototype Pattern
Objectives
Upon completing this chapter, you will have a greater
understanding of the role object creation plays in building
an application. With the help of the above Design Patterns,
you will be able to enhance your code quality and make your
application robust. By adopting proven object creation
mechanisms, you will foster code reusability and increase
the flexibility of your code.
Object-oriented programming
Before we explore Creational Design Patterns, it is crucial
to comprehend the context in which these patterns are
used. To achieve this understanding, it is imperative to
delve into the specifics of JavaScript as a language and how
it operates. The following sections will be based on two
programming paradigms supported by JavaScript. This
exploration will lay the groundwork for our deeper
investigation into the paradigm that ultimately leads us to a
comprehensive understanding of Creational Design
Patterns.
JavaScript supports both functional programming and OOP.
Functional programming, as the name suggests, is a
declarative programming paradigm based purely on
functions. It involves implementing functions sequentially to
solve complex problems.
OOP is a programming paradigm based on classes and
objects. In this style of coding, properties and actions are
grouped together. OOP was developed to make coding more
flexible and easier to maintain. It is the combination of
these paradigms that makes JavaScript such a robust
language for the Web.
With an eye on future reference, this section serves as a
refresher on some intricate aspects of JavaScript. Our focus
begins with a broader exploration of OOP and subsequently
delves into how OOP is applied in JavaScript. To underscore
the latter aspect, we will specifically delve into topics like
object prototypes and constructors. A solid understanding of
OOP is paramount as it forms the foundation for
comprehending Design Patterns in their entirety. If you feel
confident in your grasp of OOP concepts and their
implementation in JavaScript, you are welcome to skip the
following sections and directly proceed to the Singleton
Pattern, initiating our exploration into Creational Design
Patterns.
Objects
To better understand the concept of objects from an OOP
perspective, let us use a real-life analogy of cars depicted in
the following figure:
Classes
To comprehend the concept of JavaScript classes, let us
imagine a real-life analogy of a factory that produces cars:
Figure 2.2: A class acts like a factory for producing similar objects
It’s established that objects hold data resembling real-life
entities. Now, envision classes as factories responsible for
producing these objects. Alternatively, you can liken classes
to blueprints that guide the creation of objects. In essence,
classes serve as templates, and objects represent instances
generated from these templates. It is important to note that
numerous objects can be instantiated using a single class,
demonstrating the flexibility and efficiency of this OOP
approach.
Principles of OOP
OOP works on these four principles:
• Abstraction: Abstraction involves presenting solely the
crucial features or functionalities to the user while
concealing implementation specifics that are irrelevant
to the user. To give you a real-life analogy, a user is
unconcerned about the intricacies of how a car starts or
the internal mechanisms responsible for its movement
upon ignition. The user’s primary concern lies in the
action: pressing a button or turning the ignition key
should initiate the vehicle’s operation.
• Encapsulation: Encapsulation refers to the practice of
confining properties and methods within a class,
ensuring they remain inaccessible from outside the
class. To illustrate encapsulation, consider a real-life
scenario such as a bank account. Sensitive information
like the user’s account balance or account number is not
directly accessible. Instead, access to this data is
mediated through well-defined interfaces like withdraw
or deposit. These interfaces act as protective barriers,
preventing unauthorized access and safeguarding the
user’s confidential information.
• Inheritance: Inheritance embodies the idea of enabling
properties and methods from a parent class to be
accessible within its child class. An analogy that
illustrates inheritance in real life is the concept of a
family. Much like how children inherit physical
attributes such as eye color, hair color, or certain
Behavioral traits from their parents, in programming,
child classes inherit attributes and behaviors from their
parent class.
• Polymorphism: Polymorphism, derived from the Greek
words poly meaning many and morph meaning forms,
refers to the ability to take on different forms. A
straightforward example to understand polymorphism is
through musical instruments. Consider how the same
musical notes played on a piano and a guitar produce
distinct sounds. Likewise, in programming, an object or
a class can exhibit various behaviors and results while
referencing the same interface, demonstrating the
concept of polymorphism.
Object prototype
Put simply, prototypes serve as the mechanisms through
which JavaScript objects inherit features from one another.
Let us try a simple coding exercise to figure out this
enigmatic concept.
Open your preferred browser’s console and type in the
following code:
1. const person = {
2. name: "Rushabh",
3. greet() {
4. [Link](`Hi, my name is ${[Link]}!`);
5. }
6. }
7.
8. [Link]();
9. [Link](person);
Code explanation: In the above coding example, we can
see that we have a simple object called person. This object
has a property called name whose value is Rushabh (feel
free to replace it with your name) and a method called
greet which prints the property name in a template literal
format. We then call the greet method of the person object
and in the next line, we print the person object itself. When
you press enter or execute the above coding example, you
will get a result that looks something like this:
Figure 2.3: Viewing prototype structure on console
In Figure 2.3, we observe that the greet method has been
successfully called and it prints the correct result of Hi, my
name is Rushabh! However, in the next line, we can see
that although our object person had only one property and
one method, the object that has been printed on the console
shows several methods and properties that we have not
defined in our object initialization. Moreover, all these
properties and methods are accessible to us. For instance,
let us try calling the valueOf method shown in Figure 2.3
and the response that we receive looks like this:
Figure 2.4: Viewing the result of valueOf method execution of the person object
prototype on console
As illustrated in Figure 2.4, the execution of
[Link]() was successful, producing the exact
response observed in Figure 2.3. This demonstrates that the
object initialized earlier in this section acquired several pre-
defined properties and methods immediately upon creation.
The term used for these pre-defined properties and methods
integrated into the structure of an object is prototypes.
To restate it formally, every JavaScript object inherently
possesses a built-in property known as its prototype. In
accordance with its English definition, a prototype is an
original model upon which something is patterned.
In the upcoming section, we will delve into the concept of
constructors in OOP. By the conclusion of this section, we
will examine how the object prototype implements the
principle of inheritance in JavaScript.
Constructors
In the preceding sections of this chapter, we have observed
that classes in OOP serve as a kind of factory method for
generating objects. Similar to traditional OOP languages
like Java, classes in JavaScript feature a special method
called a constructor. This constructor is employed for
initializing an object instance of that particular class. Before
delving into a more detailed exploration, let us take a brief
look at an example of a constructor in JavaScript:
1. class Person {
2. constructor(name) {
3. [Link] = name;
4. }
5.
6. greet() {
7. [Link](`Hi, my name is ${[Link]}!`);
8. }
9. }
10.
11. const teacher = new Person("Rushabh");
12.
13. [Link]();
Drawing inspiration from the example utilized in the
preceding section to grasp object prototypes, we have
transformed the person variable from that example into a
class named Person. As evident from the above code
example, we have incorporated the greet method previously
used in the object prototype example. The notable
distinction between the two code examples lies in the
inclusion of a method named constructor. This
constructor method takes an argument called name and
assigns it to the local data variable name within the class.
This local variable name can subsequently be utilized
throughout the entire class.
In formal terms, a constructor is a special method invoked
during the initialization of a class. Its purpose is to facilitate
any custom initialization tasks that need to be executed
before any of the object’s methods are called.
The structure of a constructor changes if it is a derived
class or a class which is extending or inheriting its
properties from a parent class. In case of a derived class,
this is what the code looks like:
1. class Person {
2. constructor(name) {
3. [Link] = name;
4. }
5.
6. introduce() {
7. return (`Name of person: ${[Link]}`);
8. }
9. }
10. class Student extends Person {
11. constructor(name, id) {
12. super(name);
13. [Link] = id;
14. }
15.
16. introduce() {
17. return (`${[Link]()}, Student ID:
${[Link]}`);
18. }
19. }
20. let student1 = new Student('Mukul', 22);
21. [Link]([Link]());
When we execute this code in the browser’s console, we
receive the following output:
Inheritance in JavaScript
In the preceding section, we saw a code example depicting
derived classes. In that example, we created two classes –
Person and Student. The Student class extends the
Person class, thereby deriving all its properties and
methods.
By using the extends keyword and the super method in the
code example from the preceding section, we essentially
implemented the OOP principle of inheritance in JavaScript.
To help you comprehend the importance of prototype and
constructor in the JavaScript brand of OOP, let us enter the
previous code again in our browser console with a minor
difference. Instead of executing the introduce method of
student1, we will instead print the student1 variable on
our web console:
1. class Person {
2. constructor(name) {
3. [Link] = name;
4. }
5.
6. introduce() {
7. return (`Name of person: ${[Link]}`);
8. }
9. }
10. class Student extends Person {
11. constructor(name, id) {
12. super(name);
13. [Link] = id;
14. }
15.
16. introduce() {
17. return (`${[Link]()}, Student ID:
${[Link]}`);
18. }
19. }
20. let student1 = new Student('Mukul', 22);
21. [Link](student1);
When we run this code, our output looks like this:
Figure 2.6: Viewing the structure of a derived class instance on the web
console
In Figure 2.6, we can find the object instance created by the
Student class. More importantly, the object has two
prototypes assigned to it. The first one possesses the
constructor method and the introduce method belonging
to the Student class, while the second one possesses the
constructor and the introduce method belonging to the
Person class.
This illustrates the intricate hierarchy and the levels of
inheritance accessible to an object derived from a parent
class. Such an object not only encapsulates the data and
methods specific to its own instance but also inherits the
data and actions from the entire hierarchy above it.
When you try to access a property of an object, JavaScript
will try to find that property in the object itself. If it is not
able to find that property on the object, it will then search
for this property on the prototype of the object. If the
property still cannot be found, it will search the prototype of
the prototype, and so on, until the value is found. In case
the property is not found until the end, JavaScript then
returns undefined.
Polymorphism in JavaScript
Polymorphism is a key concept in object-oriented
programming, allowing a single method name to be used in
different forms based on the context. In JavaScript,
polymorphism is achieved through method overriding in
classes that involve inheritance. For example, consider the
following:
1. class Person {
2. constructor(name) {
3. [Link] = name;
4. }
5.
6. introduce() {
7. return `Hello, my name is ${[Link]}.`;
8. }
9. }
10.
11. class Student extends Person {
12. constructor(name, age) {
13. super(name);
14. [Link] = age;
15. }
16.
17. introduce() {
18. return `Hi, I'm ${[Link]}, and I'm ${[Link]}
years old.`;
19. }
20. }
21.
22. // Example usage:
23. const student = new Student("Mukul", 22);
24. const person = new Person("Raj");
25. const introducePeople = [student, person];
26.
27. [Link](person =>
[Link]([Link]()));
In this example, both the Person and Student classes have
an introduce method, but their implementations differ. The
introduce method in the Person class provides a generic
greeting, whereas the introduce method in the Student
class adds age-specific details. This demonstrates
polymorphism, as the method behaves differently depending
on the object type. This flexibility makes the code adaptable
and allows you to handle diverse objects in a unified
manner.
Memory leak
Incorrectly managed object creation, such as not properly
releasing resources or forgetting to unregister event
listeners, can lead to memory leaks. Over time, this can
cause the application to consume excessive memory and
eventually crash.
1. function EventListener() {
2. [Link] = [];
3.
4. [Link] = function (handler) {
5. [Link](handler);
6. };
7.
8. [Link] = function () {
9. [Link](handler => handler());
10. };
11. }
12.
13. // Usage with memory leak
14. const listener = new EventListener();
15. [Link](() => [Link]('Event
triggered'));
In the preceding code example, we add an event listener to
the object listener. However, we fail to unregister this event
listener even when it is no longer needed. Over time, if
several objects are created using this instance, there will be
several open event listeners which can consume excessive
memory and crash.
Circular dependencies
Poorly structured object creation may result in circular
dependencies, where objects depend on each other in a
loop. This can lead to initialization issues, making it
challenging to create instances of objects without
encountering errors.
1. // Module A
2. const A = require('./B');
3.
4. function ModuleA() {
5. [Link] = 'Module A';
6. [Link] = new [Link](); // Circular
dependency
7. }
8.
9. // Module B
10. const B = require('./A');
11.
12. function ModuleB() {
13. [Link] = 'Module B';
14. [Link] = new [Link](); // Circular
dependency
15. }
16.
17. // Usage
18. const instanceA = new [Link](); // Results in a
circular dependency error
In this code, modules A and B have a circular dependency
on each other. This will eventually cause problems when
creating instances.
Uninitialized variables
Objects that are not properly initialized can contain
undefined or null properties. Accessing these properties
without checking for their existence can result in runtime
errors and application crashes.
1. function UserProfile(name, age) {
2. [Link] = name;
3. [Link] = age;
4. }
5.
6. // Usage with uninitialized variable
7. const user = new UserProfile();
In the above code example, we are not passing any
arguments to the initialization of the class instance.
Therefore, the object user is essentially a ticking bomb. If
the user tries to access any of its properties, it might crash
the application.
These were a few of the potential problems that can be
faced by incorrect object creation. This segment basically
helps us understand the understated importance of creating
objects in the correct fashion. Creational Design Patterns
will help us achieve that.
Singleton Pattern
Let us begin our Creational Design Pattern journey by
learning about one of the most basic Design Patterns –
Singleton. Singleton Pattern is a Design Pattern that
postulates that a class should have only one instance, and
this instance can be accessed globally.
Problem scenario
There are two common problems faced by every developer
at some point in his/her career that can be potentially
solved by Singleton.
Scenario #1
The developer has introduced a global variable to store
crucial objects and properties, enabling accessibility
throughout the entire application. While this facilitates
widespread access to data, it also introduces a potential
issue. As the application grows in size, there arises the risk
of unintended modifications to the global object code. For
instance, consider a global object named user storing vital
data for an e-commerce application, including personal
details, delivery addresses, contact information, transaction
history, order details, and payment information such as card
details and CVV codes.
In a large-scale e-commerce application, numerous smaller
files collectively form the extensive codebase required for
the application’s operation. Imagine a scenario where a new
function is introduced, and the developer inadvertently
accesses the global variable user unintentionally overriding
it with experimental analytical data. This inadvertent
modification could lead to the deletion of all user
information, replacing it with irrelevant data. The
consequences of such accidental changes have the potential
to crash the application, as JavaScript may fail to access
properties stored in the user variable that were essential to
the application’s functioning.
Scenario #2
Consider a scenario where you are developing a web
application that involves managing user sessions. Each user
session corresponds to a unique object that encapsulates
the user’s data and state. However, you want to ensure that
there is only one session object per user, and subsequent
requests for the same user should return the existing
session object rather than creating a new one.
Without controlling the number of instances, you might
encounter issues such as duplicate user sessions,
inconsistent data, or unnecessary resource consumption. By
managing the number of instances, you can efficiently
handle user sessions and avoid potential problems related to
duplicate session data.
In essence, controlling the number of instances in this
scenario allows you to ensure that each user has only one
session object throughout their interaction with the web
application, maintaining consistency and efficient resource
usage.
In both the above scenarios, we can see that having a single
instance of an object which cannot be directly accessed or
modified can make our code robust and efficient. This is the
exact problem solved by the Singleton Pattern.
Implementation
There are several methods or approaches to implement the
Singleton Pattern in your code. In this segment, we will be
looking at one of the most efficient and effective ways to
add the Singleton Pattern to your JavaScript code. Let us
break down the implementation of Singleton Pattern into
the following steps:
1. Step 1: Private instance variable
Create a private variable inside the singleton module to
store the single instance.
2. Step 2: Private constructor function
Next, we will create a private constructor function that
creates a single instance. We need to add the logic for
creating the singleton instance in this function.
3. Step 3: Get instance method
Now that we have successfully encapsulated the
instance using private variables and a private
constructor method, now we need to discreetly provide
access to the single instance. To achieve this, we will
create a method, often named getInstance, which can
provide access to this singular instance. In this method,
we will check if the instance already exists. If it already
exists, we will not create a new instance.
4. Step 4: Usage
Finally, we will use this getInstance method to fetch the
instance of our singleton module. Subsequent calls to
this method should return the same instance created
initially.
It is important to note here that this is a basic framework or
a list of steps to create a Singleton Pattern to achieve our
purposes. However, JavaScript being a versatile language,
there can be several different ways to achieve the Singleton
Pattern, including but not limited to closures, object literals,
or classes. Feel free to use the above method or any of the
alternatives on the basis of your specific use case.
Code example
Now that we have listed down the steps to implement
Singleton Pattern in our code, let us go ahead and apply
these steps on an actual coding example:
1. class SingletonUser {
2. static #instance; // Creating a private static variable
3.
4. constructor(name) {
5. if(!SingletonUser.#instance) { //Ensuring that not
more than one instance can be created
6. SingletonUser.#instance = this;
7. [Link] = name;
8. }
9. return SingletonUser.#instance;
10. }
11.
12. static getInstance() { //Method to get instance of the
class
13. if(!SingletonUser.#instance) {
14. SingletonUser.#instance = this;
15. }
16. return SingletonUser.#instance;
17. }
18. }
19.
20. let user1 = new SingletonUser('Rushabh');
21. let user2 = new SingletonUser('Unnati');
22. [Link]("user1: ", user1);
23. [Link]("user2: ", user2);
24. [Link]("user1 === user2: ", user1 === user2);
25.
26. let user3 = [Link]();
27. let user4 = [Link]();
28. [Link]("user3: ", user3);
29. [Link]("user4: ", user4);
30. [Link]("user3 === user4: ", user3 === user4);
In the code example above, we have implemented the
Singleton Pattern by creating the SingletonUser class
following the outlined steps.
The initial step in our code is the creation of a private
variable named instance, utilizing the # notation provided
by JavaScript to ensure its privacy.
Subsequently, we establish the constructor, which is made
private through the introduction of an if condition. The
condition specifies that if an instance of the class already
exists, the code within the conditional is skipped, preventing
the reinitialization of properties and methods. Instead, it
returns the pre-existing instance.
Lastly, we have included a getInstance method for
developers seeking access to the class instance.
Let us run this code and see what output is generated:
Figure 2.7: Output for Singleton Pattern code
As depicted in Figure 2.7, despite the attempts to create
new instances of the class with different arguments passed
to the constructor, the output consistently retains and
displays the original instance created. Consequently, our
objective of establishing a single instance of a class using
the Singleton Pattern has been successfully achieved.
Applications
As we have seen in the above scenarios, the Singleton
Pattern is most useful in scenarios where you need to
ensure that a class has only one instance, and a global
access is provided to that instance. Here are some scenarios
where a Singleton Pattern may be appropriate to use:
Global configuration settings
Consider the example of spend management software like
Airbase where a user can keep track of the company
expenses, manage reimbursements, or handle international
transactions.
In online applications, maintaining a global configuration
settings feature is vital, allowing users to establish various
configurations applicable throughout the application. Within
these settings, users might define the base currency, select
a theme for the application’s appearance, determine the list
of approvers, and more. In such a scenario, applying the
Singleton Pattern becomes instrumental in ensuring that
these settings remain consistent and are not inadvertently
overridden.
Caching
In case of e-commerce applications, often there is a need to
store cached data to improve the performance and the
speed of the application. Applications often use caching to
store product information, product data, images, prices and
so on. Singleton Patterns can help maintain a single
instance of cache which can be overridden only in certain
situations.
Problem scenario
Consider the example of a versatile logging system to
elucidate the problem addressed by the Factory Method
Design Pattern. For the purposes of this illustration, let us
assume we are developing a web application that
accommodates various types of log outputs. For example,
the application could support logging to a console, logging
to a file, and logging to a remote server.
Upon close examination of this scenario, it becomes
apparent that a substantial portion of the code within the
three distinct categories would share similarities. The
divergence arises primarily in the final stage where the
logging needs to be directed to a specific output.
Simultaneously, for scalability, it is desirable to construct an
application that allows the addition of new log formats in
the future without requiring extensive modifications to the
original code. This is where the factory method Design
Pattern comes into play, addressing the need for a flexible
and extensible logging system.
Implementation
In this section, we will outline the steps typically involved in
implementing a factory method Design Pattern in code. It is
understandable that these steps might initially appear
confusing and intimidating. However, as we delve into the
code example in the next section and correlate these steps
to it, the process will become clearer and more
comprehensible.
In this segment, we have three characters – a creator, a
product, and a client.
1. Step 1: Define the product interface
Create an interface or an abstract class that declares the
common interface for all the concrete products.
2. Step 2: Implement concrete products
Create concrete classes that implement the product
interface, representing the different types of products.
3. Step 3: Define the creator interface
Create an interface or an abstract class that declares the
factory method for creating products. Traditionally, you
can name it as createProduct. This class may also
include other methods that operate on the products.
Think of the factory method as a template or an empty
method that will be overridden by the creators to
generate a method suitable to their needs. The
important thing to note here is that all creators should be
referencing this common factory method.
4. Step 4: Implement concrete creators
Create concrete classes that implement the creator
interface. Each concrete creator class overrides the
factory method to produce a specific type of product.
5. Step 5: Client code
In the client code, create instances of concrete creator
classes and use them to create products. The client code
should call the factory method to obtain products
without specifying their concrete classes.
In this segment, we have now defined the five steps
required to create a factory method. Now, let us look at a
code example to understand the method to implement these
five steps in code.
Code example
In the previous section, we listed down the steps to
implement factory method Design Pattern for our codebase.
Now, let us try to correlate those steps with the help of a
code example. To grasp the implementation of the factory
method Design Pattern, let us initially acquaint ourselves
with the key players or main characters integral to this
Design Pattern. To facilitate our comprehension of these
characters and the Design Pattern as a whole, let us draw
parallels with the example of a real-life factory.
Applications
As we have seen in the above scenario, the factory method
is an intricate Design Pattern extremely useful for scenarios
where you need to have a base plan for creating several
spinoffs. In other words, a common factory that will serve to
create several products having some similar properties but
created in a different individual way. Let us have a look at
some of the common applications of the Factory Method
Pattern.
UI Component Libraries
One of the best places where the factory method Design
Pattern shines is probably, the case of building UI
Component Libraries. The Factory Method Pattern can be
used to allow users to create various UI elements (e.g.,
buttons, forms, dialogs) without exposing the complexities
of the underlying implementation.
Game development
In game development, the Factory Method Pattern can be
useful for creating different types of game objects (e.g.,
characters, weapons, enemies) based on the current game
state.
Internationalization
For applications that support multiple languages, a factory
method can be used to create language-specific instances or
components.
Problem scenario
To grasp the implementation of the Abstract Factory
Pattern, let us delve into the example of a furniture shop.
Picture a furniture shop that specializes in crafting products
specifically designed for a living room. In this scenario, the
shop excels in creating remarkable and durable chairs,
couches, and coffee tables tailored for a living room setting.
Let us better understand the example of an Abstract Factory
Pattern using a set of furniture depicted in the following
figure:
Figure 2.8: Different styles of the same object
The furniture shop produces three distinct styles or variants
for its chairs, couches, and coffee tables: Art Deco, Modern,
and Bohemian. Each of these styles or variants is visually
unique, as illustrated in the image above.
Consider a scenario where a customer places an order for a
set of Art Deco chairs for their living room. It becomes
evident that, to maintain a cohesive and harmonious style in
the living room, the customer would prefer to order a couch
in the Art Deco style as well. A modern-styled couch might
not complement the artistic chairs of the Art Deco style as
effectively.
Therefore, we need to find a way to group these individual
furniture objects as a loosely-couple family. From a
technical standpoint, there is always a chance that the
furniture store would wish to add newer variants and styles
of furniture. Therefore, we would like to build a robust code
that would not require major changes if new products or
new styles are added to the system.
Implementation
In this section, we will delineate the steps typically
associated with implementing an Abstract Factory Design
Pattern. Given our prior coverage of the Factory Method
Pattern earlier in this chapter, you may find the steps
outlined below to be less intricate in nature. Nevertheless,
in the upcoming section, we will once again utilize a real-life
analogy to enhance our understanding of these steps.
Code example
In the previous section, we looked at the five-step process to
implement the Abstract Factory Design Pattern in our code.
In this segment, we will look at the example of a UI
component library to drill home the point clearly.
First things first, let us revise the main characters that we
have in this Design Pattern. We have the following
characters:
• Products
• A base factory having common properties for building
products
• Variants of products
• Concrete or specialized factories for building variants of
products
Now that we have the list of characters at our disposable,
let us write a code that implements Abstract Factory Design
Pattern!
Applications
Due to the similarities between the Abstract Factory and the
Factory Method Design Patterns, their application shares
several commonalities. Much like the Factory Method
Design Pattern, the Abstract Factory facilitates the creation
of a foundational template that can be overridden to
produce concrete classes. The key distinction lies in the
Abstract Factory’s ability to generate families of products, a
capability beyond the narrower scope of the Factory Method
Pattern. In fact, the Factory Method Pattern can, to some
extent, contribute to the implementation of an Abstract
Factory Design Pattern. Let us explore instances where the
Abstract Factory Design Pattern excels:
UI Component Libraries
As demonstrated in the Code example segment for the
Abstract Factory Design Pattern, it proves to be an ideal
pattern for constructing an entire component library with
various themes. The Abstract Factory Design Pattern
extends the capabilities of the Factory Method Design
Pattern by not only creating multiple components sharing a
common implementation but also generating a family of
components with shared features yet distinctive
appearances.
Builder Pattern
The Builder Pattern is a Creational Design Pattern that
enables the step-by-step construction of complex objects.
This pattern empowers the creation of diverse types and
representations of an object utilizing the same construction
code.
Problem scenario
Builder Pattern works best for complex objects that require
step-by-step initialization of many properties and contain
several nested objects. Let us correlate this with a real-life
analogy of building a house, depicted in the picture below
where every house looks different from a Structural point of
view:
Implementation
To solve the problem that we witnessed in the Problem
scenario segment of the Builder Design Pattern, we can
separate the construction code into a separate class
altogether and call it builders.
Subsequently, we break down the entire construction
process into a sequence of distinct steps – such as
buildWall, buildDoor, buildWindows, etc. To assemble
the complex House object, we execute a series of these
steps on the builder object. Importantly, it is not mandatory
to invoke all of these steps. Depending on preferences, one
can opt not to execute a specific step. For example, basic
house builders may exclude the construction step for a
swimming pool.
Let us segregate the entire process into steps for easy
understanding:
Code example
In the preceding section, we meticulously outlined the steps
required to implement the builder Design Pattern in our
code. Now, let us delve into a code example to solidify our
understanding of this concept. For a clearer comprehension
of this concept, we will employ the example of constructing
a computer from scratch.
Applications
Due to its step-by-step building process and its separation of
construction logic from the implementation, the Build
pattern is extremely useful in scenarios where complex
objects need to be created in gradually incremental phases.
Since we utilize an abstract builder, this makes the Builder
Design Pattern useful for creating variants of products
belonging to the same category. Let us look at the scenarios
where Build pattern excels in implementation:
Product variants
As we have seen in the examples for Builder Pattern before,
this pattern aids in dealing with product variants or
configurations, such as different types of computers, cars,
or electronic devices. It helps us in developing a flexible
way to create these variations without cluttering the client
code.
Prototype Pattern
The Prototype Design Pattern, a member of the Creational
Design Patterns, facilitates the replication of existing
objects without necessitating code dependency on their
classes. The primary purpose of the Prototype Pattern is to
generate new objects by duplicating an established object,
referred to as the prototype. This proves highly
advantageous in situations where crafting a new object is
more intricate or resource-intensive compared to
duplicating an existing one.
Problem scenario
In coding scenarios, there are instances where you may
encounter the need to create an identical copy of an object.
Consider the context of a Role Management System within a
company, where various user roles exist, such as
Administrator, Employee, or Customer Support.
For illustrative purposes, let us imagine that the company
initially had only one role – the Employee role. As the
company expanded, introducing the Administrator role
became a logical step. In such a situation, it becomes
imperative to generate an exact replica of the Employee role
for the Administrator role and subsequently incorporate the
additional permissions specific to an admin.
A common approach to do this would be to create a new
object of the same class and then copy all the fields from the
original object to the new object. There are three problems
with this approach:
1. Some of the object’s fields might be private and not
visible from the outside.
2. We need to know the name of the object’s class to
replicate it, which makes us completely dependent on
the class itself.
3. If the code is already implementing a Design Pattern,
there is a chance that you might know the interface of
the object but not the actual concrete class or the base
class. Finding and replicating that logic would not only
be time-consuming but it would further complicate the
code.
Therefore, we need a solution that can delegate the entire
cloning process to a common interface which would solve all
the above problems.
Implementation
In the initial sections of this chapter, we delved into the
JavaScript object prototype, which inherently appends
certain properties to an initialized object. The fundamental
concept underlying an object prototype aligns closely with
what we are about to explore in this section. Let us
deconstruct the implementation of the pattern into clear
and distinct steps:
Code example
In the preceding section, we witnessed the seamless
integration of the Prototype Design Pattern into our code.
To reinforce our understanding, let us explore a real-life
analogy. In this scenario, we will establish a prototype for a
basic Shape object encompassing common properties such
as colour and methods like draw.
Step 1: Create an object prototype
We will initiate the process by crafting the prototype object
for the Shape. This prototype will encompass all the
essential properties and methods, forming the foundation
for its clones:
1. class Shape {
2. constructor(color) {
3. [Link] = color || "default";
4. }
5.
6. draw() {
7. [Link](`Drawing a ${[Link]} shape.`);
8. }
9. }
In the provided code snippet, a class named Shape has
been established. Within the constructor method, the class
accepts a color argument, which is then assigned to the
class property color. If the colour is not specified, the
property will default to the string default. Additionally, a
draw method has been included, responsible for displaying
the shape’s color in the console.
Applications
The object cloning advantage provided by the Prototype
Pattern is widely utilized in various scenarios in JavaScript.
Here are a few applications:
Reducing object initialization overhead
The Prototype Pattern allows you to create new objects by
copying existing ones, reducing the overhead of initializing
objects from scratch. This is particularly beneficial when
object creation involves complex or resource-intensive
operations.
Role-based permissions
When dealing with user roles in an application, each role
may have a common set of permissions. The Prototype
Pattern can be used to create role objects with default
permissions, and new roles can be created by cloning these
prototypes.
Conclusion
In conclusion, this chapter has provided a comprehensive
exploration of Creational Design Patterns in JavaScript,
delving into the fundamental concepts of OOP and drawing
distinctions between classical OOP and the unique
implementation of OOP in JavaScript. Through our journey,
we have gained valuable insights into the significance of
Creational Design Patterns and their role in facilitating
object creation processes.
By thoroughly examining various Creational Design
Patterns, including Singleton, Factory, Abstract Factory,
Builder, and Prototype, we have equipped ourselves with a
diverse set of tools to address specific object creation
challenges. Each pattern offers a distinct solution, enabling
developers to design flexible, maintainable, and scalable
systems.
As we embrace these patterns, it becomes evident that they
not only enhance code organization and structure but also
contribute to the overall efficiency of our applications. The
careful selection and implementation of Creational Design
Patterns empower developers to create objects in a manner
that aligns with specific requirements, promotes code reuse,
and fosters the principles of encapsulation. In the next
chapter, we will be understanding another category of
Design Patterns in JavaScript that addresses communication
between objects.
Points to remember
• JavaScript as a language also follows OOP policies.
Every object on initialization gets several pre-defined
properties and they are accessible from an object
prototype.
• Creational Design Patterns specifically deal with the
correct creation of objects. Often understated, incorrect
object creation has the potential to crash an application.
• Singleton Pattern states that only one instance should
exist for a class and it should be accessed globally.
• Factory Method Pattern offers a super class that can
serve as a reference for subclasses to create objects.
Subclasses can then override the methods provided in
the superclass to create their own versions of products.
• Abstract Factory Design Pattern is an extension of the
Factory Method Pattern where it is useful in creating
related or dependent objects without specifying their
concrete classes. This pattern is particularly useful when
you need to ensure that the created objects are
compatible and should be used together.
• Builder Design Pattern is perfect for scenarios where
you need to create complex objects that require step-by-
step process of construction, and that have several
parameters that could be nested to deeper levels.
• Prototype Pattern helps in building clone of complex
objects by separating the entire construction process
from the implementation.
Exercises
1. Which of the following principles do not belong to
the OOP methodology?
a. A class should not be allowed to share its properties or
let other classes to extends its data or methods.
b. Sensitive information belonging to a class should not
be accessible outside the class.
c. It is prudent to conceal the implementation specifics of
a class from the user.
d. An object can exhibit different behaviours while
accessing the same interface.
2. Which type of Creational Design Pattern deals with
incremental and stepwise building of a complex
object?
a. Prototype
b. Factory Method
c. Builder
d. Abstract Factory
3. Which of the following descriptions fit the
description of an Abstract Factory Pattern?
a. This pattern deals with cloning objects successfully by
separating the cloning process from the
implementation.
b. This pattern believes in creating related or dependent
objects without specifying their concrete classes.
c. This pattern strongly believes that only one instance of
the class should exist.
d. This pattern helps build complex objects gradually in a
step-by-step manner.
Answers
1. a
2. c
3. b
CHAPTER 3
Structural Design Patterns
Introduction
Structural Design Patterns are a crucial part of software
development process that emphasizes on organizing and
composing classes and objects to build larger, flexible
structures. Picture these patterns akin to LEGO blocks –
each block representing an individual class or object. When
arranged adeptly, these blocks enable the creation of not
just visually stunning but also inherently sturdy structures.
Much like assembling LEGO pieces, Structural Design
Patterns offer solutions to common design dilemmas,
fostering code reusability and scalability within intricate
systems. Skilful arrangement and organization of these
classes and objects empower us to craft a resilient system
that champions the separation of concerns, enhancing code
readability in the process.
Structure
This chapter will cover the following topics:
• Introduction to Structural Design Patterns
• Adapter Pattern
• Bridge Pattern
• Composite Pattern
• Decorator Pattern
• Façade Pattern
• Flyweight Pattern
• Proxy Pattern
• Pattern selection tips
Objectives
Upon completing this chapter, you will acquire a mastery of
Structural Design Patterns, unlocking the secrets to
architecting applications with finesse and flexibility. From
demystifying common coding challenges to understanding
the usage of these patterns, you will be able to craft code
that is modular, scalable, and lucid. Structural Design
Patterns will help you to sculpt code that can withstand the
test of time.
Problem statement #1
Let us look at this code example showcasing a nested
object one might commonly encounter while developing an
application:
1. const person = {
2. name: 'John',
3. address: {
4. street: '123 Main St',
5. city: 'Seattle',
6. country: 'USA'
7. }
8. };
9.
10. // Accessing the country property
11. const country = [Link];
Code issue
The code tightly couples the person and the address
object, making it challenging to modify or manage their
relationships. For instance, if you wish to change the
structure of the address object, it will involve modifying
every object and the code in multiple places. This makes
code less flexible and harder to maintain.
Problem statement #2
For our second scenario, let us examine a situation
involving two classes that exhibit a notable degree of
similarity, yet harbour distinct differences:
1. class BasicCar {
2. constructor() {
3. [Link] = function() {
4. [Link]('Basic car started.');
5. };
6. }
7. }
8.
9. class LuxuryCar {
10. constructor() {
11. [Link] = function() {
12. [Link]('Luxury car started.');
13. };
14.
15. [Link] = function() {
16. [Link]('Performing luxury action.');
17. };
18. }
19. }
20.
21. // Usage
22. const car = new LuxuryCar();
23. [Link]();
24. [Link](); // Throws an error if used
with BasicCar
Code issue
Since we looked at the Creational Design Patterns in the
preceding chapter, we might have noticed the glaring
mistake of not having a link between the two classes
BasicCar and LuxuryCar. Lack of a common interface
makes it challenging to use different car types
interchangeably.
Problem statement #3
In our final example, we will be looking at an example
related to inheritance. Let us have a closer look at the
problem that might arise due to a lack of organization:
1. class Animal {
2. constructor(name) {
3. [Link] = name;
4. }
5.
6. eat() {
7. [Link](`${[Link]} is eating.`);
8. }
9. }
10.
11. class Bird extends Animal {
12. fly() {
13. [Link](`${[Link]} is flying.`);
14. }
15. }
16.
17. // Usage
18. const sparrow = new Bird(‘Sparrow’);
19. [Link]();
Code issue
The provided code snippet seems harmless at first glance.
It introduces a class Bird that extends the Animal class.
Through this extension, Bird inherits both the name
property and the eat method from Animal, subsequently
invoking the eat function. However, a potential pitfall lies
in the fact that the eat function exclusively belongs to the
Animal class.
In the dynamic landscape of a growing application,
Structural modifications to the parent class Animal are
conceivable. This could involve drastic changes, including
the removal of the eat function in a large-scale application.
If such alterations occur, all classes inheriting the eat
property would encounter failures, potentially leading to
application crashes that prove challenging to debug.
Furthermore, as the hierarchy expands, it exacerbates code
readability issues. The inherent tight coupling of classes
also hinders their reuse, adding another layer to the
challenge.
Organization of classes
Structural Design Patterns help in organizing classes and
objects in a way that enhances code clarity and separation
of concerns. This aids in managing the complexity of large
codebases by providing a structured and coherent
architecture.
Object composition
These patterns emphasize the composition of objects to
create larger, more complex structures. By composing
objects instead of relying on class inheritance, we can
create a flexible and dynamic relationship between
components.
Code reusability
One of the biggest advantage and goal of the Structural
Design Pattern is code reusability. Structural Design
Patterns like Adapter, Composite, and Decorator, lean on
creating reusable components that can be seamlessly
integrated into different parts of the application.
Problem scenario
Let us understand the problems solved by the Adapter
Pattern with the following two examples. Our first example
has no reference to any coding application.
Scenario #1
In the context of Figure 3.1, which illustrates various
electronic devices used in India, it is apparent that the
chargers for devices such as the MacBook, Android phones,
and smartwatches differ significantly. For instance, the
MacBook charger typically utilizes a USB-C connector,
while Android chargers may have micro USB connectors.
The smartwatch charger, on the other hand, is specifically
designed for USB type-B ports.
Figure 3.1: Different types of ports and sockets used by different countries
When traveling to countries like the UK or various
European nations, a challenge arises due to the distinct
power outlets. The power outlets in the UK and Europe
differ from those in India, and this incompatibility may
render the chargers brought from home unusable. In such
instances, a practical solution is required – a device
commonly known as a travel adapter or plug converter.
This device should be capable of accepting Indian-style
power outlets and converting them to the formats required
in the UK or Europe.
Scenario #2
In the second scenario, let us explore the realm of a
Content Management System (CMS) application
tailored for managing articles and blog posts. For this
illustration, let’s assume our application exclusively
supports the storage and retrieval of data in the relational
database format, exemplified by SQL tables.
The challenge arises when a decision is made to enhance
our application by incorporating a full-text search engine.
After exhaustive research, a third-party search engine
library emerges as the ideal candidate due to its efficiency
and seamless compatibility with our requirements.
However, a significant hurdle surfaces – this library
exclusively operates on document-based storage, such as
MongoDB, and necessitates content in JSON format.
Now confronted with a critical dilemma, the question
looms: should we undergo substantial modifications to our
CMS code to align with this third-party library, or should
we opt for a costly and less efficient alternative that fails to
fully meet our needs?
In both of the scenarios outlined above, a common need
emerges for a device or tool capable of seamlessly
converting or facilitating the adaptation between disparate
devices or codebases. Put simply, what’s required is an
adapter. The Adapter Structural Design Pattern is precisely
crafted to fulfil this purpose.
Implementation
The implementation of the Adapter Design Pattern can be
broken down into five distinct steps, culminating in clear
instructions on its utilization within an actual client code:
Step 5: Usage
In the concluding step, we bring the implementation to
fruition by connecting the existing class with the target
interface using the adapter. This involves instantiating the
existing class, creating an adapter, passing the instance of
the existing class to it, and ultimately utilizing the method
we defined in the previous step to establish the connection
between the two.
Code example
We have broken down the implementation of the Adapter
Pattern into five distinct steps in the previous section. Now,
let us utilize the example of an authentication system to
realise the steps that we have seen in the previous section.
In this example, we have a legacy authentication system
that accepts a username and custom token. However,
modern architecture calls for a username and a password
to authenticate a user. let us see how we can implement
the Adapter Pattern to solve this issue:
Figure 3.2: An adapter acts as a bridge connecting two dissimilar items of the
same type
In Figure 3.2, on the left, we have a charger with a
connector tailored for American users. On the extreme
right, there is an outlet port in a Parisian hotel in Europe.
To address this discrepancy, an adapter, depicted in the
middle, is necessary. This adapter takes the American
connector as input and provides its own connector,
adaptable to European ports, as output. Upon connecting
the device through this adapter, it seamlessly
accommodates both the original and newer formats,
ensuring the correct functioning of the device.
Likewise, we can draw a parallel between the
LegacyAuthenticator and the European outlet port, while
the Indian charger corresponds to the
ModernAuthenticator. To enable the Indian charger to
seamlessly operate on European ports, we require the
adapter LegacyToModernAuthAdapter. This adapter
must be capable of interfacing with the Indian charger,
accepting its connector, and delivering the charge to the
European outlet.
In a similar vein, the LegacyToModernAuthAdapter must
extend the ModernAuthenticator to gain access to its
login method. Simultaneously, it needs to connect to the
European outlet port in its original format, necessitating an
internal connection or execution of the
LegacyAuthenticator’s authenticate method. This
intricate dance ensures the smooth compatibility and
operation of the ModernAuthenticator with the legacy
LegacyAuthenticator.
Step 5: Usage
In the ultimate step, we consolidate all the information
gathered thus far to bring the adapter method to fruition:
1. // Instantiate the existing class (legacy authenticator)
2. const oldAuthenticator = new LegacyAuthenticator();
3.
4. // Create an adapter and pass the existing class
instance to it
5. const adaptedAuthenticator = new
LegacyToModernAuthAdapter(oldAuthenticator);
6.
7. // Use the client code to handle authentication with the
adapted authenticator
8. handleAuthentication(adaptedAuthenticator);
As we can see in the above code example, we have divided
the final implementation into three parts:
1. We first create an instance of the old authentication
system and store it in the oldAuthenticator variable.
2. Next, we create an instance of the adapter. As
explained in step 3, it requires an instance of the
existing class as a parameter. Therefore, in our case we
pass the oldAuthenticator variable to the constructor
of the adapter by passing it in as an argument. We,
then, store the instance of the adapter in the variable
adapterAuthenticator.
3. In the conclusive step, we invoke the method
delineated in step 4 to log into the system as a modern
authenticator. As observed in the preceding code, this
operation executes the original authenticator within its
context, and we consequently receive the resulting
outcome.
With this completion, the adapter method has been
successfully implemented. This method allows a new code
format to seamlessly interact with an existing code
structure without necessitating any alterations to the
original codebase. The adapter serves as a vital
intermediary, facilitating cohesion between the modern and
legacy components, ultimately enabling them to work
together harmoniously.
Applications
From the preceding sections, we have learnt that the
Adapter Pattern is extremely useful for scenarios where
you want to bring about new changes to your code-base
without changing the original code written. This benefit
naturally finds a lot of applications in modern JavaScript
programming. let us look at some of those applications:
Version upgrades
When upgrading a library or framework to a new version
that has breaking changes in its interface, and we wish to
minimize the impact on our existing code, the Adapter
Pattern can help minimize the damage.
Internationalization
For applications that are built on a global scale,
internationalization or the support for multiple
international languages and/or currency is a must-have
feature. Adapter Pattern can help adapt our application to
support multiple languages and support the need to change
the behaviour of certain components based on the selected
language without modifying their source code.
Problem scenario
The Bridge Design Pattern proves instrumental in resolving
issues where the abstraction of a class needs separation
from its implementation, allowing them to function
independently. Let’s delve into two scenarios that mirror
problematic situations, demonstrating how the bridge
Design Pattern can effectively address and solve these
challenges:
Scenario #1
Let’s explore a scenario using the example of geometric
shapes and their colours. Initially, we have a class Shape
with two subclasses – Square and Circle. Now, envision
the need to expand these classes to encompass colours –
Blue and Red. This expansion results in a total of four
classes – BlueSquare, BlueCircle, RedSquare, and
RedCircle. However, the complexity intensifies when the
requirement arises to introduce two additional shapes and
two more colours. This leads to a proliferation of
subclasses, resulting in a more intricate and less robust
code structure.
Scenario #2
For our second scenario, let us consider the example of a
UI toolkit. A UI toolkit is a library that provides common UI
elements like buttons, windows, text fields, and so on, to
allow developers to create UI for applications. Now, the
challenge lies in supporting multiple operating systems like
MacOS, Windows, and Linux. Each operating system has its
own native windowing system and UI conventions.
Application developers want to create cross-platform
applications without worrying about the intricacies of each
operating system’s UI implementation. Once again, the
developers should not require modifying the application
code.
The common thread between the above two scenarios is
that in both the cases, there is a need for the abstraction
(colour and UI toolkit) to be independent of the specific
details of various platforms (shape and operating system).
As new elements or structures are introduced, there should
be a way to seamlessly integrate and support them without
modifying the existing codebase.
Implementation
Let’s deconstruct the implementation of the Bridge Design
Pattern into discrete steps. While these steps may initially
appear complex or overwhelming, in the following section,
we will explore the same steps with a relevant example to
enhance our understanding of the implementation of this
Design Pattern.
Step 1: Define the implementor interface
In this step, the focus is on defining the interface or an
abstract class for the implementor. When we refer to the
implementor, we are addressing the underlying system
with its various implementations. During this phase, the
objective is to outline the functions that will be
implemented by the concrete implementors, which we will
be creating in the subsequent step.
Code example
Without further ado, let us deep dive into understanding
the implementation of the Bridge Design Pattern with the
help of a code example. Let’s consider the example of a
messaging system that needs to send messages via
different channels (e.g. SMS, email, Slack) while keeping
the message content and sending logic separate.
Applications
The Bridge Pattern proves highly advantageous in
scenarios where the abstraction and the implementation of
your code must operate independently without mutually
impacting each other. Here are some instances where this
pattern excels:
UI framework for cross-platform applications
When creating a user interface framework for cross-
platform applications (e.g., web, mobile, desktop), the
Bridge Pattern can be employed to separate the UI
components from the underlying platform-specific
implementations.
Problem scenario
The Composite Design Pattern proves invaluable in
scenarios such as managing a file system in an operating
system, where there are two distinct objects: files and
folders. A folder can contain multiple files as well as other
folders, creating a hierarchical structure.
Now, imagine we are tasked with calculating the total size
of a folder, which includes the sizes of all nested files and
folders within it.
JavaScript developers grappling with this task encounter
several hurdles. Firstly, dealing with different types of
items becomes a concern, as the Folder class must manage
both files and other folders. Designing the composite
structure to accommodate diverse item types while
upholding a consistent interface demands thoughtful
consideration. Additionally, the complexity is heightened by
the fact that the Folder class also handles other folders,
introducing the challenge of circular references.
Implementing checks to prevent infinite loops during
operations becomes a crucial aspect of the solution.
This overview only scratches the surface, and further
exploration into the implementation of this coding problem
reveals additional intricacies.
Implementation
The implementation of the Composite Design Pattern can
be broken down into four distinct steps:
Code example
let us put the steps for implementing the Composite
Pattern, as discussed in the previous section, into action.
We will use the same example of a file system that we have
discussed in the Problem scenario segment for Composite
Pattern.
Applications
The Composite Design Pattern is particular useful in
JavaScript for managing hierarchical structures where
individual objects and compositions of objects need to be
treated uniformly. Here are some scenarios where the
Composite Pattern is highly effective:
GUI components
Graphical user interfaces often involve a hierarchy of
components like panels, buttons, and text fields. The
Composite Pattern enables uniform treatment of individual
GUI elements and complex layouts, simplifying their
creation and manipulation..
Tree structures
This pattern is ideal for scenarios involving tree-like
structures such as organizational hierarchies, family trees,
or nested categories. It provides a consistent way to
traverse and manipulate these structures while reducing
complexity.
Menu systems
Nested menus, such as those in navigation systems, benefit
from this pattern. Each menu item can act as a leaf
(individual menu option) or a composite (submenu with
options), allowing for seamless management of the
hierarchy.
Problem scenario
Upon reviewing the description above, one might initially
consider solving this problem using one of the Design
Patterns encountered so far, or even resorting to simple
inheritance. However, the intricacies of the problem reveal
a layer of complexity that cannot be effectively addressed
by the patterns we have explored thus far. To comprehend
the problem scenario that can be effectively tackled by a
Decorator Design Pattern, let’s explore a real-life analogy.
Consider a car manufacturing company that produces
standard cars equipped with a set of basic features,
including standard seats, a dashboard, and an air-
conditioning system. However, after a customer purchases
a car, they often desire to customize it according to their
preferences. Some may opt for leather seats to enhance
comfort, while others might want to install a sunroof or
upgrade to a superior sound system.
The challenge lies in enabling customers to customize their
cars with optional features such as leather seats, a sunroof,
and an advanced sound system without resorting to
creating an exhaustive set of subclasses for every
conceivable combination of features. For example, some
customers may desire only leather seats, while others
might prefer a combination of leather seats and an
advanced sound system. This leads to numerous
permutations and combinations, and as more services and
additional features are introduced, the system becomes
more error-prone and increasingly complex. We want a
flexible solution that allows customers to pick and choose
additional features dynamically.
Implementation
Let us break down the implementation of the Decorator
Design Pattern into distinct steps:
Step 5: Usage
The ultimate step is to create instances of the concrete
component and decorators. We need to compose them in a
way that builds the desired combination of behaviour.
As done previously, let us concretize these implementation
steps through a code example to enhance our
understanding.
Code example
We have outlined the steps for implementing the Decorator
Design Pattern. Now, let’s put them into practice with a
simple example of making coffee. In this scenario, we have
a base variety of coffee with a certain cost. Users can opt to
add their own decorators to this base coffee, such as milk
and/or sugar, and the price will be adjusted accordingly.
Let’s take a look:
Step 5: Usage
For our final step in using the Decorator Pattern, we will
create instances of the concrete components and
decorators, structuring them in the desired way. Let’s
proceed with this step:
1. // Usage
2. const simpleCoffee = new SimpleCoffee();
3. [Link]("Cost of simple coffee:",
[Link]());
4.
5. const milkCoffee = new MilkDecorator(simpleCoffee);
6. [Link]("Cost of coffee with milk:",
[Link]());
7.
8. const sugarMilkCoffee = new
SugarDecorator(milkCoffee);
9. [Link]("Cost of coffee with sugar and milk:",
[Link]());
In the above code example, we have first created the
instance of the simple coffee. Next, we create the instance
of the milk coffee and add the simple coffee to its decorator
instance. Finally, we make a coffee with both sugar and
milk, and add the milk coffee instance to the
SugarDecorator class to build this version.
On running the above code, you will receive the following
output on your console:
1. Cost of simple coffee: 5
2. Cost of coffee with milk: 7
3. Cost of coffee with sugar and milk: 8
This demonstrates the substantial advantage of using the
Decorator Pattern to address this crucial problem.
Throughout the final usage, there was no direct access or
reference to the original code. It remained well
encapsulated behind the scenes, safeguarding it while still
influencing the overall behaviour. Furthermore, users only
need to define several decorators in the initial steps. They
can then employ these decorators as desired to create their
personalized versions of coffee. This makes the process
highly user-friendly, concealing the implementation details
to a significant extent.
Applications
The Decorator Design Pattern in JavaScript can be useful in
various scenarios where you want to add or modify
functionality of objects dynamically and flexibly. Here are a
few examples:
Data validation
Decorator Patterns can be useful when we have data
validation requirements for our code. For instances where
we want to add validation rules to objects without
modifying their original validation logic, these patterns can
come to our assistance. For example, decorating a form
validation object with additional validation rules like
checking for password complexity or email format.
Authentication
We can use the Decorator Patterns for instances where we
want to add authentication checks to certain functions or
methods without modifying their core functionality. For
instance, decorating a resource-fetching function with
authentication checks before making the actual request.
Dynamic configuration
We can use these patterns to dynamically configure objects
with different settings without altering their original
structure.
Event handling
We can also use Decorator Patterns to dynamically attach
or detach event handlers to objects without modifying their
existing event handling code. Decorating a DOM element
with additional event listeners based on user interactions.
This could allow us to work independently of the element’s
implementation and not affect it.
Problem scenario
To understand the efficacy of the façade Design Pattern, let
us consider the example of a web application that needs to
fetch and display the real-time weather information for a
given location. To achieve this, we would require a
probable integration of multiple weather APIs to gather
comprehensive data like current temperature, humidity,
wind speed, and forecasts.
However, this introduces a series of challenges. For
instance, each weather API might have its own unique set
of endpoints, request formats, and authentication
mechanisms. Additionally, some APIs may necessitate
additional processing or data transformation before it can
be consistently presented in your application.
However, the challenges persist. The codebase might
become cluttered with API-specific logic, leading to
difficulties in maintenance and extension. If the third-party
API undergoes changes in its implementation, such as
adding or removing endpoints or modifying the
authentication mechanism, it would necessitate widespread
modifications throughout the application. Managing API
keys, authentication tokens, and error handling for each
API becomes a complex and error-prone task.
We would require a solution that makes it easier to manage
the complexities arising with such scenarios.
Implementation
Let us break down the implementation of the Façade
Design Pattern into distinct steps. The initial step, even
before diving into the actual implementation logic, is to
understand and identify the complex subsystem or set of
interfaces that need simplification. Once the subsystem
requiring modification is identified, the implementation
process begins:
Applications
The Facade pattern in JavaScript can be beneficial in
various scenarios where you want to simplify and provide a
unified interface to a complex subsystem or set of
functionalities. Here are a few scenarios where the Facade
pattern can be useful:
Resource management
When dealing with resource management, such as handling
connections to databases, caches, or file systems. The
Facade can encapsulate the initialization, configuration,
and clean-up processes, providing a straightforward
interface for resource management.
Problem scenario
Let us examine a scenario that can shed light on the types
of issues that can be addressed by the Flyweight Design
Pattern. Let’s consider the example of game development.
In a video game scenario, such as the Grand Theft Auto
(GTA) series, numerous non-player characters (NPCs)
populate the virtual world. NPCs are characters that exist
in the game environment but are not controlled by the
player. They may include citizens going about their
routines while the main character undertakes missions.
Each NPC in the game possesses common characteristics,
like appearance, behaviour, or animations (considered
intrinsic state), along with unique properties such as
positions and health (considered extrinsic state). Creating a
distinct object for every NPC that encompasses both
intrinsic and extrinsic states could result in significant
memory consumption, especially considering the large
number of NPCs present in a typical game.
We would require a solution that can help us save memory
by grouping and sharing the common properties. This
separation of the common properties and the unique data
of every object can help reduce memory consumption and
improve the efficiency of the application.
Implementation
Let us go through the step-by-step process of implementing
the flyweight Design Pattern in a JavaScript code:
Code example
Let us apply the example of a music-playing application to
implement the steps we discussed in the previous section
and incorporate the flyweight Design Pattern into our
codebase. Imagine we are building a music streaming
service where users can create playlists with a large
number of songs. Each song has common properties like its
title, artist, and duration (intrinsic state), but users can add
songs to different playlists, each with its own order and
play count (extrinsic state).
Applications
The Flyweight Pattern can be effective in various scenarios
in JavaScript, especially when dealing with a large number
of similar objects where memory usage needs to be
optimized. Here are some scenarios where the Flyweight
Pattern can be beneficial:
Problem scenario
To understand the problem solved by the proxy Design
Patterns, let’s take a very relevant example of a photo-
sharing website where users can upload and view images.
As users scroll through their feed, the application needs to
load and display images. However, some images might be
very large in size, leading to a slower loading time and a
poor user experience.
In this scenario, loading all the images at once, especially
the larger ones, can result in a very sluggish performance.
Users generally do not like to wait for the images to load,
and they will end up being frustrated and stop using the
application altogether. Additionally, downloading large
images can consume a significant amount of bandwidth,
impacting both server and client resources.
The challenge is to optimize the image loading process,
ensuring that only the images currently in the user’s
viewport are loaded. Loading images only when necessary
can improve the application’s performance and reduce the
strain on network resources.
Implementation
Let us go through the step-by-step process of implementing
the Proxy Design Pattern in our codebase:
Code example
To understand the implementation of a Proxy Pattern
effectively, let’s explore the concept of lazy-loading in web
applications. We will break down the process into steps for
a clearer understanding and implementation.
Applications
The Proxy Pattern in JavaScript can be useful in various
scenarios to provide control, optimization, and additional
functionalities. Here are several instances where the Proxy
Pattern can be beneficial:
Lazy loading
As we have already seen in the example, proxy Design
Pattern can be used to implement lazy loading for
resources such as images, scripts, or data. The proxy can
delay the actual loading of resources until they are
explicitly requested, improving initial page load times.
Access control
We can employ a proxy to control access to sensitive
operations or resources. For example, we can use a
protection proxy to restrict access to certain methods or
properties based on user roles or permissions..
Caching
We can implement a proxy to cache the results of expensive
operations. The proxy can store the results and return them
if the same operation is requested again, reducing the need
for redundant computations.
Validation
We can use a proxy to validate input parameters or data
before it reaches the actual object. This ensures that only
valid data is processed by the real object, preventing
potential issues.
Adapter Pattern
As we know, Adapter Pattern allows objects with
incompatible interfaces to collaborate. With that in view,
here are certain scenarios where this Design Pattern will
be beneficial to use:
• When integrating a new component with an existing
system.
• When the interface of an existing class needs to be
compatible with a client’s expectations.
• When working with legacy code that has an
incompatible interface.
Bridge Pattern
Bridge Pattern allows us to split a large class of closely
related classes into two independent hierarchies.
Therefore, Bridge Pattern can work wonders for scenarios
where:
• When we want to separate abstraction from
implementation.
• When changes in the implementation should not
affect the abstraction.
• When there is a need to extend both abstraction
and implementation independently
Composite Pattern
Composite Pattern permits us to compose objects into
discernible tree structures. This Design Pattern can help
us:
• When we want to treat both individual objects and
compositions of objects uniformly.
• When clients should be able to ignore the difference
between compositions of objects and individual objects.
• When we need to represent part-whole hierarchies.
Decorator Pattern
Decorator Design Pattern, as its name suggests, helps us to
attach new behaviours to objects by placing these objects
inside special wrappers. This can prove beneficial in the
following scenarios:
• When we want to add new functionalities to an object
dynamically without altering its structure.
• When there is a need for a flexible and reusable way to
extend behaviour.
• When we want to avoid subclassing to extend
behaviour.
Façade Pattern
Facade pattern provides a simplified interface to a library,
a framework, or any other complex set of classes.
Therefore, it can be used extensively for the following
scenarios:
• When we need to provide a simplified interface to a
complex subsystem.
• When client code needs to interact with a subsystem,
but we want to shield it from the complexities of that
subsystem.
• When we want to decouple the client code from the
implementation details of a subsystem.
Flyweight Pattern
Flyweight Pattern helps us fit more objects into the
available amount of RAM by sharing common parts of the
state between multiple objects. Its utility can be beneficial
in the following cases:
• When there are a large number of similar objects that
can be shared to reduce memory usage.
• When the application uses a large number of objects
that have similar properties.
• When the overhead of creating and maintaining a large
number of similar objects is significant.
Proxy Pattern
Proxy Pattern helps us by providing a substitute or a
placeholder for another object. This can be useful in cases
where:
• When we want to control access to an object.
• When we need to add functionality before or after the
actual processing of requests.
• When we want to implement lazy loading of an
expensive object.
Conclusion
In conclusion, this chapter provided an in-depth exploration
of Structural Design Patterns, which play a crucial role in
organizing code for the development of flexible and
scalable systems. Throughout the chapter, we delved into
seven distinct types of Structural Design Patterns. We
started with the Adapter Design Pattern, beneficial for
integrating newer code with legacy systems. The Bridge
Design Pattern was next, creating a bridge to separate the
abstraction and implementation aspects of complex
problems. Following that, the Composite Design Pattern
emerged as a solution for tree-based structures with both
leaf and composite elements. The Decorator Pattern was
discussed, offering a way to add new functionalities to
existing objects. Façade Pattern simplifies the handling of
various integrations by creating a unified interface.
Flyweight Pattern optimizes memory usage by sharing
common components. Lastly, the Proxy Pattern enables the
creation of a surrogate that takes control before accessing
the original object, allowing for the addition of
functionalities or behaviours.
A common thread among all these patterns is that
Structural Design Patterns provide a systematic approach
to addressing code complexity through effective code
structuring and organization. When applied appropriately,
these patterns serve as powerful tools for constructing
systems that are not only scalable but also reliable. In the
ever-evolving landscape of software development, the
understanding and mastery of Structural Design Patterns
are indispensable. As we move forward, armed with the
insights gained from this chapter, it is imperative for
developers to leverage these patterns judiciously, adapting
them to the unique requirements of each project. By doing
so, we contribute to the creation of robust, flexible, and
sustainable software architectures that stand the test of
time.
Points to remember
• Structural Design Patterns can be likened to
assembling individual objects like LEGO pieces. Just as
arranging and structuring these pieces can result in
masterpieces, appropriately organizing and structuring
objects can lead to the development of robust code that
stands the test of time.
• The Adapter Design Pattern involves creating an
interface capable of accommodating newer methods of
implementation. It provides a suitable adapter or
modification that serves as a bridge between the new
and different method and the traditional legacy
approach of a code.
• The Bridge Design Pattern advocates separating the
implementation part of the code from the abstraction
part. This separation allows encapsulating the
implementation code, facilitating the creation of cross-
platform applications.
• The Composite Design Pattern is well-suited for code
with a hierarchical or tree structure. It effectively
addresses complexities arising from components that
can have children, accommodating both leaf nodes and
composite nodes in a structured manner.
• The Decorator Design Pattern enables the addition of
functionalities or behaviours to code at runtime without
modifying the original code base.
• The Façade Pattern acts as a literal façade, hiding the
complex implementation logic of integrating multiple
components. Instead, it provides a simplified interface
for the client code to interact without being aware of
the underlying complexities.
• The Flyweight Design Pattern is useful in reducing
memory usage by efficiently managing shared and non-
shared components, particularly in scenarios involving a
large number of objects.
• The Proxy Design Pattern creates a surrogate that can
access the actual object and add behaviours or
functionalities to it before the actual object can be
accessed.
Exercises
1. Which Structural Design Pattern can effectively
handle the scenario of integrating multiple APIs
into a common codebase?
a. Adapter
b. Proxy
c. Decorator
d. Façade
2. Which Structural Design Pattern is described by
the following statement – “splitting large classes
or closely related classes into two parts –
abstraction and its implementation, allowing them
to be developed independently of each other”?
a. Composite
b. Bridge
c. Adapter
d. Singleton
3. Which of the following applications define the
correct use-case for a Flyweight Design Pattern?
a. Lazy loading
b. Access control
c. Data validation
d. Integration to legacy code.
Answers
1. d
2. b
3. a
CHAPTER 4
Behavioral Design Patterns
Introduction
Behavioral Design Patterns in software development are
patterns exclusively addressing communication between
objects. They focus on defining the responsibilities of
objects and how communication occurs among them. To
illustrate these patterns with a real-life analogy, let us liken
our code to a bustling city intersection, with objects
representing pedestrians or vehicles navigating through the
city. Now, envision the scenario in a real metropolitan city
such as New York, London, or Mumbai without traffic
signals or traffic police officials — utter chaos would ensue.
One could anticipate vehicles colliding and traffic
congestion reaching unprecedented levels. Much like traffic
signals maintaining order and facilitating proper vehicle
navigation, Behavioral Design Patterns serve a similar role
for objects in our code.
Structure
This chapter will cover the following topics:
• Introduction to Behavioral Design Patterns
• Chain of Responsibility Pattern
• Command Pattern
• Iterator Pattern
• Mediator Pattern
• Memento Pattern
• Observer Pattern
• Strategy Pattern
Objectives
Upon completing this chapter, we will gain a deeper
understanding of how responsibilities are distributed among
objects. We will explore how a particular behavior or
interaction can be accomplished by establishing effective
communication channels between objects. Grasping these
two concepts positions you to write more adaptable code.
Like Creational and Structural Design Patterns, the
integration of these patterns into your code will render it
modular, scalable, and resilient against future
modifications.
Problem statement #1
Let us look at this example depicting a shopping cart
pricing:
1. // Problematic Code without Strategy Pattern
2.
3. // Shopping Cart class
4. class ShoppingCart {
5. constructor() {
6. [Link] = [];
7. }
8.
9. // Method to calculate total price
10. calculateTotalPrice() {
11. let totalPrice = 0;
12.
13. [Link](item => {
14. // Problem: Switch statement for different pricing
strategies
15. switch ([Link]) {
16. case 'book':
17. totalPrice += [Link] * 0.9; // 10% discount
on books
18. break;
19. case 'mobile':
20. totalPrice += [Link] * 1.2; //20% markup on
mobiles
21. break;
22. // More cases for other types...
23. }
24. });
25.
26. return totalPrice;
27. }
28.
29. // Method to add items to the cart
30. addItem(item) {
31. [Link](item);
32. }
33. }
34.
35. // Example usage
36. const cart = new ShoppingCart();
37. [Link]({ type: 'book', price: 30 });
38. [Link]({ type: 'mobile', price: 50 });
39.
40. [Link](`Total Price:
$${[Link]()}`);
Code issue
Although the code may seem straightforward with a base
class ShoppingCart managing both item addition and
pricing, numerous issues arise. Firstly, the
calculateTotalPrice method employs a switch statement to
implement various pricing strategies based on the item
type. This complexity hinders extending the pricing strategy
without altering the existing code. Furthermore, it tightly
couples the pricing logic to the class, diminishing its
flexibility.
Problem statement #2
For our second problem statement, we will consider the
example of a logging system:
1. // Problematic Code without Chain of Responsibility
Pattern
2.
3. // Logger class
4. class Logger {
5. log(message, level) {
6. // Problem: Conditional statements for different log
levels
7. if (level === 'info') {
8. [Link](`[INFO] ${message}`);
9. } else if (level === 'warning') {
10. [Link](`[WARNING] ${message}`);
11. } else if (level === 'error') {
12. [Link](`[ERROR] ${message}`);
13. }
14. // More conditions for other log levels...
15. }
16. }
17.
18. // Example usage
19. const logger = new Logger();
20. [Link]('System is running smoothly.', 'info');
21. [Link]('Warning: Low disk space.', 'warning');
Code issue
In this code, a Logger class is present, logging messages to
the console based on level logic. However, it relies on
conditional logic, assigning the class the dual responsibility
of both printing and determining the message to be printed.
This setup complicates the addition or modification of log
levels without altering the existing code, resulting in a lack
of flexibility and extensibility.
In the two examples above, we observed two scenarios, or
rather, two types of problems that a developer might
unwittingly introduce into the codebase. Similarly, we can
encounter several other issues in our code, such as:
• Loose coupling: Tightly coupled objects in the code
make it difficult to modify one without affecting the
other.
• Flexibility and extensibility: Modifying or extending
the functionality in a system can be challenging without
introducing breaking changes.
• Dynamic behaviour: Managing various behaviours
through diverse logic can become intricate when dealing
with numerous participants.
• Responsibility chain: Multiple objects may handle a
request, and the appropriate handler needs to be
determined dynamically.
Problem scenario
To help understand the problems that can be solved using
the CoR pattern, let us consider the example of an expense
approval workflow in a company.
Imagine a small and midsize enterprise (SME) company
experiencing steady growth. In such companies, there exists
a process for approving expenses incurred by employees.
These expenses could range from purchasing software
subscriptions and team outing meals to travel costs accrued
during a client presentation. Varied levels of approval are
necessary, depending on the expense amount and type.
Refer to the following figure showcasing a typical overflow
of expense approval:
Code example
In this section, we will utilize the example of a help desk
ticket resolution system and implement the five-step process
we saw in the preceding section. In this help desk system,
users can submit tickets for various issues. Each ticket
needs to undergo a series of handlers to ascertain the
appropriate resolution. These handlers may encompass level
1 support, level 2 support, and ultimately, escalation to a
manager.
Applications
We have just observed the substantial benefits that the CoR
Design Pattern provides over traditional conditional logic.
Now, let us explore a list of scenarios where this pattern
proves advantageous:
Form validation
In form validation scenarios, where different validation
rules need to be applied to form inputs, CoR can be used.
Each validation rule can be a handler in the chain, checking
a specific aspect of the input data.
Command Pattern
The Command Design Pattern is a Behavioral Design
Pattern that turns a request into a standalone object that
contains all the information about the request. Often used in
decoupling the sender and receiver objects, this
transformation can pass the request around, store it, and
delay its execution for a later time.
Problem scenario
Let us delve into a real-life example of a home automation
system to grasp the type of issues a command Design
Pattern can address. In this scenario, imagine we have
recently installed a home automation system that manages
various devices such as the thermostat, air-conditioner,
lights, and smart speakers with a simple click of a button.
Now, we aim to enhance the system to handle intricate
routines. For instance, uttering the words Good Morning
to the system should trigger a sequence of complex events
like turning on specific lights, powering down the air-
conditioner, and playing our favourite morning playlist.
Initially, our basic remote control features a straightforward
interface and control logic where each button corresponds
to a specific device. However, to integrate our complex
Good Morning logic, we would need to hardcode the
sequence of commands in the remote control. This approach
makes it inflexible and challenging to modify or extend,
especially when adding more complex commands to the
remote control in the future.
By introducing the command Design Pattern to the
aforementioned problem, we can address it by decoupling
the sender (remote control) from the receivers (smart home
devices). In the following sections, we will delve into a
detailed exploration of implementing this pattern in our
code and examine the various applications where it can be
advantageous.
Implementation
As mentioned earlier, we will be decoupling the senders
from the receivers as a part of the command Design Pattern
implementation. We can divide this implementation into the
following steps:
Code example
We will use the example of the smart home system
discussed earlier in the Problem scenario section for the
Command Design Pattern and attempt to implement it in the
code below, following the implementation steps outlined in
the preceding section. To recap the problem statement, we
aim to efficiently control smart home devices. The goal is to
design it in a way that newer and more complex functions
can be added to the remote control used for controlling
these devices without impacting the original code.
Applications
We have noticed that the Command Design Pattern works
wonders in scenarios where we want to decouple the sender
of a request from the receiver, and we need flexibility,
extensibility, and support for features like undo/redo. Here
are some scenarios in JavaScript where the Command
Design Pattern would be beneficial:
GUI applications
In GUI applications, we can use the Command Pattern to
handle user interactions. For example, a button click can be
represented as a command, allowing us to encapsulate and
execute different actions.
Undo/redo functionality
When you need to implement undo and redo functionality,
the Command Pattern shines. Each executed command can
be stored, and undoing or redoing involves reversing or re-
executing those commands.
Workflow management
When dealing with complex workflows or business
processes, the Command Pattern allows you to encapsulate
each step of the workflow as a command, making it easier to
manage and modify the workflow.
In these scenarios, the Command Pattern promotes
flexibility, maintainability, and separation of concerns,
making your codebase more scalable and adaptable to
changing requirements.
Iterator Pattern
The Iterator Design Pattern is a Behavioral Design Pattern
that provides a way of accessing elements of a collection
sequentially without exposing the underlying representation
of the collection. In JavaScript, this pattern is often used to
iterate over elements in an object or array.
Problem scenario
Let us understand the problems that can be solved by an
Iterator Pattern with the example of navigating a tree
structure.
Let us contemplate a scenario involving a tree data
structure representing a file system, where we aim to
execute some operation on every file or folder. It is crucial
to note that each folder may contain nested folders, which
can further include additional folders or files. Navigating
the tree using recursive or nested code would make the
entire codebase intricate and less modular.
1. const fileSystem = {
2. name: 'Root',
3. type: 'directory',
4. children: [
5. {
6. name: 'Folder1',
7. type: 'directory',
8. children: [
9. { name: '[Link]', type: 'file' },
10. { name: '[Link]', type: 'file' },
11. ],
12. },
13. {
14. name: 'Folder2',
15. type: 'directory',
16. children: [
17. { name: '[Link]', type: 'file' },
18. { name: 'Folder3', type: 'directory', children: [{
name: '[Link]', type: 'file' }] },
19. ],
20. },
21. ],
22. };
23.
24. // Without Iterator
25. function processFileSystem(node) {
26. if ([Link] === 'file') {
27. [Link]([Link]);
28. // Perform some operation on the file
29. } else if ([Link] === 'directory') {
30. for (const child of [Link]) {
31. processFileSystem(child);
32. }
33. }
34. }
35.
36. processFileSystem(fileSystem);
As we can see from the code example above, the code
becomes complex and harder to understand, making
maintenance and modification challenging. If the structure
of the tree changes, the traversal logic needs to be updated,
leading to potential errors.
What we require is a solution that is modular that separates
the iteration logic and the ability to traverse complex data
structure with ease. This is a scenario where Iterator Design
Pattern can come to the rescue.
Implementation
The iterator Design Pattern proves useful for segregating
the iteration logic from the logic required to traverse
complex data structures. Let us delineate its
implementation into the four steps outlined below:
Code example
We will employ a real-life analogy, such as a shopping list,
to elucidate the steps outlined in the previous step for
implementing the Iterator Pattern in JavaScript. The
concept is straightforward: we aim to traverse a shopping
list and determine whether there are more items left to buy.
Applications
The Iterator Design Pattern can be beneficial in various
scenarios in JavaScript, especially when dealing with
collections or sequences of elements. Here are some
scenarios where the Iterator Pattern can be particularly
useful:
Mediator Pattern
The Mediator Design Pattern is a Behavioral Design Pattern
that literally lives up to its name. This pattern stresses on
promoting loose coupling by wrapping object interactions in
a mediator. This mediator serves as a middleman, ensuring
participating objects can communicate without directly
referencing each other. It is like a party host who ensures
everyone’s mingling without needing personal
introductions.
Problem scenario
To grasp the problem-solving prowess of the Mediator
Design Pattern, let us delve into the intricate operations of
an airport.
At an airport, several entities must collaborate seamlessly
for safe and efficient operations. These entities include the
outgoing and the incoming airplanes, the ground staff, the
maintenance crew and the air traffic control (ATC) tower.
Each entity has specific responsibilities, like managing
runways and handling emergencies. Failure to perform
these duties could lead to delays, bottlenecks, or even
collisions.
For example, imagine an airport without an ATC tower.
Without one, airplanes would need to coordinate directly for
take-offs and landing. Whether it is a small airport or a
major hub like those in Singapore, New York, or Mumbai
(that see at least 4 landings or take-offs in a minute),
managing communication would be a challenge. Factor in
unpredictable weather and other complications, and the risk
of collisions increases, potentially leading to tragic
outcomes.
In this scenario, the role of an ATC tower is paramount. It
serves as a crucial intermediary, facilitating interactions
between airplanes while providing guidance to ensure safe
operations, avoid collisions, and manage emergencies.
Airplanes communicate with the tower to request clearance
for take-offs and landings, report their positions, and
receive instructions for taxiing on the runway. Ground
vehicles coordinate with the tower to navigate safely around
the airport, and maintenance crews communicate with the
tower to schedule repairs and inspections without
disrupting ongoing flights.
Likewise, the Mediator Pattern streamlines communication
by introducing a middleman to prevent congestion among
interacting objects.
Implementation
The Mediator Design Pattern proves highly advantageous in
scenarios where we seek to simplify communication
between objects through the introduction of a mediator. Let
us explore how we can implement this logic using the
following steps:
Step 1: Define the mediator object
To kick off the implementation of the Mediator Pattern, our
first step is to create the mediator object itself. This
mediator will serve as the intermediary for our
communicating objects, keeping track of registered entities
and managing the distribution of messages among them.
Chat applications
As demonstrated in the previous example with airplanes, the
Mediator Design Pattern thrives in scenarios requiring
interaction and communication between objects. Few
examples illustrate this as effectively as chat applications.
Here, the Mediator Pattern can adeptly manage
communication between users across various chat rooms or
channels, guaranteeing messages reach their intended
recipients.
Game development
In multiplayer online games, the Mediator Pattern can
handle communication between players, NPCs, and game
objects, coordinating actions and events in the game world.
Microservices communication
In a microservices architecture, various services (such as
cart, payment, and catalogue in an e-commerce platform)
reside within different containers, requiring seamless
connection to establish a stable environment. Here, the
Mediator Pattern proves invaluable, facilitating
communication between microservices. Acting as a central
hub, the Mediator efficiently routes messages and
coordinates interactions among services, contributing to the
system’s cohesion and reliability.
Real-time collaborative editing
In collaborative editing applications, such as Google Docs,
the Mediator Pattern can manage concurrent edits from
multiple users, ensuring consistency and synchronization of
document changes.
In these scenarios, the Mediator Pattern helps to decouple
components, manage complex interactions, and promote a
more modular and maintainable architecture.
Memento Pattern
The Memento Design Pattern is a Behavioral pattern that
elegantly handles capturing and restoring an object’s
internal state while keeping its implementation details
hidden. In JavaScript, we wield this pattern with finesse,
employing objects and closures to securely stash and
retrieve an object’s state.
Problem scenario
Let us kick off our exploration of this pattern by delving into
a common problem scenario that resonates with many.
To understand the practical applications of the Memento
Design Pattern, let us take a peek into the world of a text
editor, like Microsoft Word. In such an app, we often hit the
save button to preserve a draft of our current text. This not
only secures our progress but also provides a safety net,
much like creating checkpoints in a game for potential
future returns.
Now, imagine a scenario where we have made numerous
edits to our document, and we realise that we need to
rewind to an earlier version. Without the Memento Pattern,
we would be stuck at manually tracking each change and its
corresponding state—a tedious and error-prone task, to say
the least.
Implementation
We can break down the step-by-step implementation of the
Memento Design Pattern into four stages, with three key
characters playing vital roles in this narrative:
Code example
Let us leverage the example of a music player application to
implement the Memento Design Pattern in JavaScript,
following the steps outlined in the preceding section. We
will utilise the Memento Design Pattern in creating a music
player application that can store playlists of songs.
Applications
The Memento Design Pattern proves advantageous in
scenarios where frequent saving and retrieval of an object’s
state are necessary. Let us explore some examples where
this pattern can shine:
Drawing applications
Applications that involve drawing or painting functionalities
can benefit from the Memento Pattern to save and restore
the state of the canvas, allowing users to undo/redo actions
or revert to previous drawings.
Observer Pattern
The Observer Design Pattern, aptly named, is a Behavioral
Design Pattern utilized to establish a subscription
mechanism. This mechanism allows multiple objects, known
as observers, to stay informed about any events or changes
occurring within an object they are observing. It serves as a
robust solution for building components or models that
require synchronization with the state of another
component or model.
Problem scenario
Let us explore the example of a weather application to shed
light on certain type of challenges developers face, which
can be effectively mitigated by implementing the Observer
Design Pattern.
For the sake of this example, let us envision our weather
application sending notifications to users about changes in
the weather for the cities they have subscribed to. Weather,
being dynamic, can change frequently, and users expect
accurate updates for all their subscribed locations. With
users spread across the globe, each interested in real-time
updates for multiple locations, the system must efficiently
notify subscribed users of weather changes without
excessive resource consumption. Scalability for future
enhancements is essential, given the diverse locations users
may subscribe to. Therefore, the solution should minimize
unnecessary overhead, such as constant polling for updates,
which could degrade user experience and consume
excessive memory, delaying application processes.
The Observer Pattern offers a solution by implementing a
subscription-based mechanism. Let us delve into it in the
following sections:
Implementation
Let us outline the implementation of the Observer Pattern
into the following steps:
Code example
Let us elucidate the aforementioned implementation steps
using an example from a social media application, such as
Instagram or Twitter. In these platforms, users can follow
each other virtually to receive updates on each other’s
activities:
Applications
The Observer Pattern is beneficial in JavaScript in various
scenarios where you have one-to-many relationships
between objects, and you need to notify multiple objects
about changes or events. Here are some scenarios where
the Observer Pattern can be beneficial:
Event handling
JavaScript is event-driven, and many libraries and
frameworks rely on events to trigger actions. The Observer
Pattern can be used to implement event handling systems
where multiple listeners (observers) can react to events
emitted by an event emitter (subject).
Model-View-Controller architecture
In the Model-View-Controller (MVC) architecture, the
Observer Pattern is commonly used to implement the
relationship between the model (subject) and the views
(observers). Changes in the model can trigger updates in
multiple views, ensuring that the views stay synchronized
with the model.
Cross-component communication
In large applications with multiple components, you may
need a way for components to communicate with each other
without creating tight coupling between them. The Observer
Pattern provides a flexible mechanism for cross-component
communication, allowing components to subscribe to and
receive updates from each other.
Overall, the Observer Pattern is beneficial in JavaScript
whenever you need to establish loosely coupled
relationships between objects and enable communication
and synchronization between them.
Strategy Pattern
The Strategy Design Pattern belonging to the Behavioral
Design Pattern allows us to define a family of algorithms.
These algorithms will be encapsulated by the Design
Pattern and make them interchangeable. It enables clients
to choose an algorithm from the available options at runtime
without modifying the client code.
Problem scenario
To consider the types of problems that the Strategy Pattern
can solve, let us examine the example of a shipping
application.
In this application, shipping prices must be calculated for
various types of packages based on factors such as weight,
distance, shipping method (e.g., airways, ships, trucks), and
promotions or discounts.
The potential issues we might encounter while coding such
an application include the need for numerous conditional
statements to handle different combinations of shipping
criteria. For instance, we may resort to nested if-else
statements to determine shipping costs based on factors like
weight, distance, and shipping method. As the application
scales, maintaining and extending this code becomes
increasingly complex and error prone.
Challenges escalate if we must introduce new shipping
methods or modify existing nested conditions. These
changes could necessitate significant modifications across
multiple parts of the codebase, making it susceptible to
errors. Moreover, unanticipated or inadequately tested
modifications may introduce bugs or result in incorrect
calculations, leading to losses for both the service provider
and the customers.
To address such challenges, it is imperative to separate the
shipping calculation algorithm from the rest of the
application. This separation enables the easy addition of
new strategies, modification of existing ones, and switching
between strategies at runtime without impacting other
components of the codebase.
Implementation
Let us outline the implementation of the Strategy Design
Pattern in JavaScript into the following three steps:
Code example
Let us utilize the shipping application example from the
Problem scenario section of the Strategy Design Pattern to
implement the steps outlined in the Implementation section
above.
Applications
From the code implementation above, it is evident that the
Strategy Pattern offers significant benefits in scenarios
where there are multiple options or strategies, and the code
needs to dynamically switch between them at runtime. This
pattern provides flexibility and modularity, allowing for easy
addition of new strategies without impacting existing code.
In JavaScript, the Strategy Pattern finds applications in
various scenarios, including:
Sorting algorithms
Implementing different sorting algorithms (e.g., bubble sort,
quick sort, merge sort) as separate strategies allows for
easy switching between algorithms based on factors like
input size or data characteristics.
Form validation
Implementing different validation strategies for form inputs
(e.g., required fields, email format, password strength)
enables flexible and customizable validation rules based on
specific form requirements.
Authentication strategies
Implementing different authentication strategies (e.g.,
username/password, OAuth, JWT) enables flexible
authentication mechanisms tailored to different security
requirements or integration scenarios.
Search algorithms
Implementing different search algorithms (e.g., linear
search, binary search, hash table) as separate strategies
allows for efficient searching based on factors like data size
or search key characteristics.
Payment processing
Implementing different payment processing strategies (e.g.,
credit card, PayPal, bank transfer) enables flexible handling
of payment transactions based on factors like user
preferences or regional payment methods.
Routing strategies
Implementing different routing strategies in web
applications (e.g., client-side routing, server-side routing,
hybrid routing) allows for optimizing navigation based on
factors like page load performance or SEO requirements.
As seen above, the Strategy Design Pattern promotes
encapsulation and interchangeability of algorithms, allowing
for greater flexibility, maintainability, and scalability in
software design. It is particularly useful in scenarios where
multiple algorithms or behaviours need to be dynamically
selected or switched at runtime based on different factors
or requirements.
Conclusion
Behavioral Design Patterns, such as Chain of Responsibility,
Command, Iterator, Mediator, Memento, Observer, and
Strategy, are essential for addressing challenges in software
development by managing object interactions and
behaviors. They encapsulate behavior within objects,
promoting modular and maintainable designs while enabling
flexibility and code reuse.
These patterns allow behaviors to be parameterized,
configured, and exchanged dynamically at runtime,
facilitating adaptability to changing requirements. By
separating concerns and promoting loose coupling between
objects, Behavioral patterns enhance system maintainability
and scalability.
Unlike Creational and Structural patterns, Behavioral
patterns focus on defining patterns of communication and
collaboration between objects at runtime, rather than on
object creation or structure. By fostering communication
and collaboration among objects, these patterns lead to
better coordination and cooperation, resulting in robust and
scalable systems capable of accommodating evolving
requirements.
In summary, Behavioral Design Patterns provide powerful
tools for managing complexity, promoting flexibility, and
enhancing the maintainability and scalability of software
systems.
Points to remember
• Behavioral Design Patterns aim at facilitating the
interactions and communication between objects in a
software system.
• The Chain of Responsibility Pattern enables the
decoupling of senders and receivers of requests,
allowing multiple objects to handle a request without
explicitly specifying the receiver.
• The Command Pattern encapsulates a request as an
object, allowing for parameterization of clients with
requests, queuing of requests, and logging of command
history.
• The Iterator Pattern provides a way to access
elements of an aggregate object sequentially without
exposing its underlying representation, promoting
flexibility and reusability.
• The Mediator Pattern facilitates communication
between objects by centralizing control and reducing
direct dependencies, resulting in a more maintainable
and loosely coupled system.
• The Memento Pattern captures and externalizes an
object’s internal state, allowing it to be restored to a
previous state, thereby supporting undo and redo
operations.
• The Observer Pattern defines a one-to-many
dependency between objects, ensuring that when one
object changes state, all its dependents are notified and
updated automatically.
• The Strategy Pattern defines a family of algorithms,
encapsulates each one, and makes them
interchangeable, providing a flexible way to select and
switch algorithms at runtime.
Exercises
1. Which behavioural Design Pattern is useful for
scenarios where the participating objects
constantly need to communicate with each other?
a. Memento
b. Iterator
c. Observer
d. Mediator
2. Which behavioural Design Pattern is useful for
scenarios where the history of an object’s state
needs to be maintained so that it can allow the
user to effectively go back to previously taken
decisions?
a. Command
b. Memento
c. Chain of Responsibility
d. Strategy
3. Which behavioural Design Pattern is excellent for
the use-case of a smart home automation system?
a. Chain of Responsibility
b. Iterator
c. Command
d. Strategy
Answers
1. d
2. b
3. c
CHAPTER 5
Asynchronous
Performance Design
Patterns
Introduction
In today’s internet era, it is crucial to have web applications
that are dynamic and responsive. To leverage the full
potential of the internet, we incorporate asynchronous
JavaScript in our code. However, asynchronous operations
such as fetching data from servers, handling user
interactions, or executing time consuming tasks can
introduce complexities that may hinder performance and
scalability if not managed skillfully.
In this chapter, we will delve into the intricate landscape of
asynchronous performance in JavaScript. We will explore
the various challenges faced by developers and the solutions
available to them. Additionally, we will understand how
Design Patterns can aid in crafting efficient and robust
asynchronous code. To begin our journey, we will delve into
or refresh our understanding of asynchronous JavaScript
programming. We will cover its fundamentals and address
the notorious callback hell problem. Moreover, we will
explore event handling in JavaScript and examine the event
pipeline, potential issues that may arise, and the solutions
provided by Design Patterns. Following this, we will
examine several Design Patterns that assist in managing
asynchronous tasks, ensuring smoother execution, and
enhancing the responsiveness of JavaScript applications.
Structure
This chapter will cover the following topics:
• Asynchronous JavaScript
• Event handling in JavaScript
• Throttling and debouncing
• Parallelism and concurrency
• Batch processing
• Promise optimization
Objectives
Upon completing this chapter, we will have a greater
understanding of how Design Patterns can aid in efficiently
handling asynchronous events in JavaScript. In addition to
refreshing our knowledge of Promises and callbacks, we will
explore five new Design Patterns/concepts that will assist
us in writing efficient and robust code for asynchronous
JavaScript applications. These Design Patterns will be
effective for managing both smaller code segments and
enterprise-level code.
Asynchronous JavaScript
In the introductory segment of this chapter, we will
understand the concept of asynchronous coding in
JavaScript. We will rewind the clock to understand the
challenges encountered by the developers’ community when
dealing with asynchronous problems, the rationale behind
introducing asynchronous programming, and the solutions
that were implemented. We will scrutinize the solutions
employed, the drawbacks introduced by their
implementation, and the introduction of new concepts
aimed at addressing those issues.
To gain a comprehensive understanding of how Design
Patterns can assist with asynchronous operations, it is
essential to grasp asynchronous JavaScript in its entirety
and comprehend the problems it resolved. This section will
delve into the history of asynchronous JavaScript and the
various solutions implemented over time to address these
challenges. Feel free to skip this section if you already
possess a solid understanding of asynchronous JavaScript.
Synchronous JavaScript
To start, let us examine an example that can illustrate the
challenge developers faced when they needed to integrate
the raw power of the Internet into building JavaScript
applications:
1. // Synchronous code to fetch data and update UI
2. function fetchDataAndRender() {
3. // Assume fetchData() is a synchronous function that
fetches data from a server
4. const data = fetchData(); // This function might take
some time to complete
5.
6. // Assume renderUI() is a synchronous function that
updates the UI with the fetched data
7. renderUI(data); // This function will not execute until
fetchData() completes
8.
9. [Link]('UI updated with fetched data');
10. }
11.
12. fetchDataAndRender();
13. [Link]('Fetching data and rendering UI...');
While the above code example may not fully encapsulate all
the challenges developers faced when incorporating the
strength of the Internet in the early days, it provides a
glimpse into the issues that affected both user and
developer experiences.
Even before delving into the description of the code
example above, it is important to remember that JavaScript,
by nature, is synchronous and blocking. This means that if
the browser encounters a JavaScript file while parsing the
HTML to create the DOM tree, it will halt further parsing of
the tree until the entire JavaScript file has been read and
executed. With this in mind, when examining the depicted
example, we can observe that the very first line to be
executed is line number 12, where the
fetchDataAndRender method is invoked.
Within the fetchDataAndRender method, the initial line of
code executed is the invocation of the fetchData method,
storing its response in the variable data. Although we have
not delved into the specifics of the fetchData method, it is
reasonable to assume that in the early days of integrating
network fetching in JavaScript, this function’s
implementation would entail connecting to servers,
requesting data, and awaiting the correct data response. In
the forthcoming segments, we will explore the earliest
examples of asynchronous JavaScript with the assistance of
XMLHttpRequest.
Until the entire execution of the fetchData method is
completed, the subsequent line of code responsible for
rendering the UI will not be reached or executed due to the
blocking and synchronous nature of JavaScript. Once again,
the rendering of the page will take several milliseconds,
depending on the size of the data received and the
complexity of the design.
If you have had the experience of opening web pages in the
90s or the early 2000s, waiting several seconds for your
website to load would have been quite common. Users
would often encounter a page similar to the example below:
Figure 5.1: In the early 2000s, it would often take several seconds for web
pages to load, and it was common to observe incomplete web page displays
As depicted in Figure 5.1, it would frequently take several
seconds to fetch and render data. Additionally, improper
styling references could further slowdown the construction
of DOM trees and potentially lead to circular dependencies,
resulting in users seeing an incomplete display of a web
page.
Fast forward to 2024, slower rendering of web applications
could lead to a significant loss of customers. Users are more
likely to switch to a platform that loads quickly and does not
leave them waiting indefinitely to use the application.
Network operations
Web applications often need to interact with servers to fetch
data or perform actions. In synchronous JavaScript, making
a network request would halt the execution of subsequent
code until the request is completed. This led to poor user
experiences, as the browser would appear frozen until the
request finished.
User experience
Users expect web applications to be fast and responsive.
However, synchronous JavaScript could cause delays in
responding to user interactions, leading to frustration and
dissatisfaction.
Browser compatibility
Ensuring consistent behavior across different browsers and
versions was a significant concern. Different browsers had
different implementations of JavaScript and its APIs, which
could lead to unexpected behavior, especially in
asynchronous scenarios.
In response to these challenges, asynchronous
programming constructs such as Promises, async/await, and
event driven architectures were introduced to JavaScript.
These features enable developers to write non-blocking
code that can handle asynchronous operations more
efficiently, resulting in faster, more responsive, and more
maintainable web applications.
Callback functions
In the preceding segment, we observed the numerous
drawbacks associated with the synchronous nature of
JavaScript programming. It became imperative to introduce
an asynchronous approach for handling network requests
and other critical scenarios in JavaScript. This necessity
gave rise to the introduction of callback functions, or simply
callbacks. The concept was straightforward: functions
would accept a callback, or a block of code, as an argument.
This block of code would only be executed when an
asynchronous operation was completed. Let us examine a
code snippet showcasing a callback function:
1. // Asynchronous function with a callback
2. function fetchData(callback) {
3. [Link]("Fetching data...");
4. setTimeout(function() {
5. [Link]("Data fetched!");
6. const data = { id: 1, name: "Example Data" };
7. callback(data);
8. }, 2000); // Simulating a delay of 2 seconds
9. }
10.
11. // Callback function to handle the fetched data
12. function processData(data) {
13. [Link]("Processing data:", data);
14. }
15.
16. // Calling the fetchData function with a callback
17. fetchData(processData);
18.
19. [Link]("Program continues to execute...");
As evident from the above example, we have two functions:
fetchData and processData. It is logical to conclude that
data processing can only occur once the data has been
fetched. Therefore, it is crucial that the processData
function is invoked only after the fetchData function has
successfully completed its task. However, it is equally
essential that invoking this function does not block the
execution of the remaining code. When we run this code,
you will get the following output:
1. Fetching data...
2. Program continues to execute...
3. Data fetched!
4. Processing data: { id: 1, name: 'Example Data' }
As evident from the above output, invoking fetchData
triggers the execution of the first line inside the fetchData
method, printing the statement Fetching data... to the
console. However, immediately afterward, JavaScript
encounters an asynchronous function, setTimeout.
Traditionally, this would entail pausing the code for 2
seconds (the period specified in the setTimeout function)
before proceeding with the rest of the fetchData method.
Consequently, the line that prints Program continues to
execute on the line number 19 would not have been printed
until the entire fetchData function was executed (including
the 2 seconds wait) was complete.
Indeed, with the introduction of asynchronous JavaScript
and callback functions, JavaScript no longer blocks
execution for 2 seconds. Instead, it proceeds to the next line
of code after the fetchData invocation and prints Program
continues to execute.... When the two seconds elapse, it
resumes execution from the point where it was paused due
to its asynchronous nature.
However, there are certain issues with this approach, and
we will delve into them in the next segment: Callback hell.
Callback hell
In the previous segment, we were introduced to one of the
very early solutions to implement the asynchronous
behavior in JavaScript. While callbacks turned out to be a
game-changer, there was one major drawback that soon
came into picture.
Also known as the Pyramid of Doom due to the distinctive
shape taken up by the code, callback hell refers to the
situation in JavaScript where multiple nested callbacks are
used to handle asynchronous operations. In the coding
example from the previous section, we observed the
processData method being dependent on the fetchData
method. Now, consider a scenario where several methods
are sequentially dependent on previous methods for their
invocation.
Let us consider the example of a social media application. In
such an application, it is crucial to first fetch user
information. Only after retrieving the user’s information
can, we proceed to fetch the user’s posts. Subsequently,
only when the user’s posts have been successfully fetched
can we consider fetching comments on a particular post. Let
us visualize this in the form of code:
1. function fetchUserData(userId, callback) {
2. // Simulated API call to fetch user data
3. setTimeout(function() {
4. const userData = { id: userId, name: "John Doe" };
5. [Link]("User data fetched:", userData);
6. callback(userData);
7. }, 1000);
8. }
9.
10. function fetchUserPosts(user, callback) {
11. // Simulated API call to fetch user posts
12. setTimeout(function() {
13. const posts = ["Post 1", "Post 2", "Post 3"];
14. [Link]("User posts fetched:", posts);
15. callback(posts);
16. }, 1500);
17. }
18.
19. function fetchPostComments(post, callback) {
20. // Simulated API call to fetch comments on a post
21. setTimeout(function() {
22. const comments = ["Comment 1", "Comment 2"];
23. [Link]("Comments for post", post, ":",
comments);
24. callback(comments);
25. }, 2000);
26. }
27.
28. // Callback hell - nested callbacks
29. fetchUserData(1, function(user) {
30. fetchUserPosts(user, function(posts) {
31. [Link](function(post) {
32. fetchPostComments(post, function(comments) {
33. // Do something with comments
34. });
35. });
36. });
37. });
As evident from the code above, we have created three
functions to handle the three network requests required.
The fetchUserData method retrieves the user’s
information, the fetchUserPosts method retrieves the
user’s posts, and the fetchPostComments method
retrieves comments on a specific post of the user. We can
observe that these methods have been structured in a
callback fashion, where each method accepts a callback in
its argument, which is then triggered after the function’s
execution has been completed.
While the initial part appears straightforward, the
complexity emerges from line 29 onwards. As evident, due
to the chain of dependencies, each function is called within
the execution of another function. This nesting of callbacks
complicates the code, making it harder to read, understand,
and maintain. The structure resembles a pyramid or
staircase, with each level representing a new asynchronous
operation or error handling logic. While in our example, we
only defined three functions, a more realistic scenario might
involve additional methods for tasks such as login and
authentication before fetching user information.
Additionally, there could be further methods for retrieving
reactions or replies to comments after fetching the
comments. This potential loop of possibilities could
significantly complicate the code further.
Callback hells are notoriously difficult to handle, debug, or
read, as we can see from the example above. There could be
several reasons for a callback hell to arise:
• Sequential asynchronous operation: Like in the
above example, there could arise several situations
where we need to perform one asynchronous task after
another. If we do not pay close attention or if we follow
this above approach, we might end up nesting callbacks
to ensure the sequential execution.
• Error handling: Handling errors in asynchronous
operations adds another layer of nesting, especially if
different error scenarios need to be handled differently.
• Complex control flow: Asynchronous code with
complex control flow, such as loops or conditional
branching, can lead to deeply nested callbacks.
• Anonymous functions: In many cases, callback
functions are defined inline as anonymous functions,
further cluttering the code, and making it harder to
follow.
As evident from the above description, callbacks proved to
be more complex to use for realistic scenarios due to the
challenges associated with nesting related sequential
asynchronous requests.
Promises
The ES6 era in 2015 introduced a new solution to tackle the
issue of asynchronous JavaScript. The challenges observed
with callbacks posed a significant obstacle for JavaScript
users, especially with the increasing prevalence of web
applications powered by JavaScript. The ES6 era brought
forth a robust solution in the form of Promises.
Promises offer a superior method for managing
asynchronous operations. They not only address the issues
encountered in callback hells but also represent the
completion or failure of an asynchronous operation.
Promises provide a cleaner and more structured approach
to handling asynchronous code.
To better understand Promises, let us consider the example
of the popular music band Coldplay. Given their immense
popularity, it is apparent that fans of the band would
inundate them with requests for information about
upcoming concerts. Realistically, it is not feasible for the
band to respond to every fan, potentially numbering in the
millions worldwide. A more streamlined solution would be to
encourage fans to follow their social media accounts or a
blog. By doing so, the band can update all concert-related
information on these platforms, ensuring that every follower
receives up-to-date information about the band and its
concerts. This analogy aligns perfectly with the concept of
Promises.
Figure 5.2: A pictorial description of how Promises work in a real-life scenario
of a music band
In this example, the music band acts as the producer, and
the fans serve as the consumers. A Promise represents the
fans’ request for information about a concert. Consumers
must subscribe to the producers to request this information.
Subsequently, when the request is fulfilled or if it fails, the
subscribed users will be notified accordingly.
Based on what we have read earlier, a Promise works in the
following fashion:
A Promise, essentially, represents a value that may be
available now, or in the future, or never. At any point in
time, it has three states:
• Pending: In the initial state of the Promise, it is
considered pending. At this stage, the Promise is neither
fulfilled nor rejected. Analogously, think of the pending
state as fans’ requests for information about an
upcoming concert. During this period, the music band
has neither confirmed the dates for their next concert
nor denied the possibility of a concert.
• Fulfilled: When the operation is completed
successfully, the Promise is considered fulfilled, and it
holds a resolved value. In our analogy, this would
symbolize the music band confirming the dates of their
upcoming concert, and the subscribed users receiving a
notification about it.
• Rejected: This indicates that the operation has failed,
and the Promise holds a reason for the failure. In our
analogy, it would mean that the music band has
confirmed on their website that they will not be
performing or scheduling any concerts for the
foreseeable future, and the users are duly notified about
this.
Additionally, every Promise had two methods to handle the
above states:
• then(): This method is used to handle the fulfilment of a
Promise. It takes two optional callback functions — one
for the fulfilment (onFulfilled) and one for the rejection
(onRejected).
• catch(): This method is used to handle Promise
rejections (errors). It is similar to the second parameter
of then(), but only catches rejections instead of both
fulfilment and rejection.
To gain a more contextual understanding of Promises, let us
consider the example of a social media application
previously used to illustrate callback hell. In this scenario,
we will implement Promises to handle the task of fetching
comments for a particular post belonging to a specific user.
By the end of this example, we will explore how Promises
alleviate the complexities associated with callback hell:
1. function fetchUserData(userId) {
2. return new Promise(function(resolve, reject) {
3. // Simulated API call to fetch user data
4. setTimeout(function() {
5. const userData = { id: userId, name: "John Doe" };
6. [Link]("User data fetched:", userData);
7. resolve(userData);
8. }, 1000);
9. });
10. }
11.
12. function fetchUserPosts(user) {
13. return new Promise(function(resolve, reject) {
14. // Simulated API call to fetch user posts
15. setTimeout(function() {
16. const posts = ["Post 1", "Post 2", "Post 3"];
17. [Link]("User posts fetched:", posts);
18. resolve(posts);
19. }, 1500);
20. });
21. }
22.
23. function fetchPostComments(post) {
24. return new Promise(function(resolve, reject) {
25. // Simulated API call to fetch comments on a post
26. setTimeout(function() {
27. const comments = ["Comment 1", "Comment 2"];
28. [Link]("Comments for post", post, ":",
comments);
29. resolve(comments);
30. }, 2000);
31. });
32. }
33.
34. // Using Promises to chain asynchronous operations
without nesting
35. fetchUserData(1)
36. .then(function(user) {
37. return fetchUserPosts(user);
38. })
39. .then(function(posts) {
40. return [Link]([Link](fetchPostComments));
41. })
42. .then(function(commentsArray) {
43. // Do something with commentsArray
44. })
45. .catch(function(error) {
46. [Link]("Error:", error);
47. });
In the above example, we have utilized the same functions
previously used to illustrate callback hell. As seen, the
fetchUserData method retrieves the user’s information,
the fetchUserPosts method retrieves the user’s posts, and
the fetchPostComments method retrieves comments on a
specific post of the user. However, a notable difference
between the two examples is the absence of the daunting
pyramid-like structure that signified callback hell. Instead,
we have a much cleaner and a viable option for handling the
chain or sequence of network requests. Each function
returns a Promise, and we use .then() to chain them
together sequentially. This approach avoids the nesting of
callbacks, making the code much more readable and
maintainable. Additionally, error handling is simplified with
the use of .catch() at the end of the chain.
Async/await
Two years after JavaScript was revolutionized with the
introduction of the game-changing ES6, the W3C (the
international standards organization responsible for
standardizing the ECMAScript format) introduced an
advanced approach to handling asynchronous JavaScript
with ES8 in 2017. This new approach, known as
async/await, offers a more concise and synchronous-looking
syntax for writing asynchronous code compared to
traditional Promise-based approaches.
While Promises solved the critical problem of callback hell,
async/await is more like an advancement on the well-
established concept of Promises. While Promises addressed
the issue of callback hell to some extent, code could still
become complex and hard to follow, especially when dealing
with multiple asynchronous operations or error handling
scenarios. Async/await was introduced to provide a more
intuitive and readable way to work with asynchronous code,
making it easier for developers to write and maintain
asynchronous programs.
Async/await is built on top of Promises and provides
syntactic sugar for working with asynchronous operations.
It consists of two keywords:
• async: The async keyword is used to define an
asynchronous function. An async function returns a
Promise implicitly, allowing us to use the await keyword
within it to pause execution until a Promise is resolved
or rejected.
• await: The await keyword is used to pause the
execution of an async function until a Promise is settled
(resolved or rejected). It can only be used within an
async function.
The primary advantage of using async/await over traditional
Promises is its synchronous-like approach, which simplifies
debugging. It eliminates the need for explicit Promise
chaining or nesting of callbacks, resulting in cleaner and
more readable code. Let us briefly examine how async/await
is implemented in code. We will use the same example of
fetching comments on a user’s particular post to illustrate
the difference in coding styles:
1. function fetchUserData(userId) {
2. return new Promise(function(resolve, reject) {
3. // Simulated API call to fetch user data
4. setTimeout(function() {
5. const userData = { id: userId, name: "John Doe" };
6. [Link]("User data fetched:", userData);
7. resolve(userData);
8. }, 1000);
9. });
10. }
11.
12. function fetchUserPosts(user) {
13. return new Promise(function(resolve, reject) {
14. // Simulated API call to fetch user posts
15. setTimeout(function() {
16. const posts = ["Post 1", "Post 2", "Post 3"];
17. [Link]("User posts fetched:", posts);
18. resolve(posts);
19. }, 1500);
20. });
21. }
22.
23. function fetchPostComments(post) {
24. return new Promise(function(resolve, reject) {
25. // Simulated API call to fetch comments on a post
26. setTimeout(function() {
27. const comments = ["Comment 1", "Comment 2"];
28. [Link]("Comments for post", post, ":",
comments);
29. resolve(comments);
30. }, 2000);
31. });
32. }
33.
34. async function fetchData() {
35. try {
36. const user = await fetchUserData(1);
37. const posts = await fetchUserPosts(user);
38. const commentsArray = await
[Link]([Link](fetchPostComments));
39. // Do something with commentsArray
40. } catch (error) {
41. [Link]("Error:", error);
42. }
43. }
44.
45. // Call the fetchData function
46. fetchData();
To comprehend the difference between the approach taken
by Promises and the approach taken by async/await, let us
review the code written for Promises before examining this
code. Indeed, the primary difference between the two
approaches lies in the final execution, where we
sequentially call the network requests. With Promises, we
had to rely on chaining, which was a significant
improvement from traditional callback approaches.
However, it still required writing a considerable amount of
code, which could be somewhat challenging to read. In
contrast, the async/await approach simplifies the entire
process to the extent that it resembles a synchronous
pattern.
As observed in the code above, instead of chaining
sequential network request calls, we utilize three variables.
We are introducing a new function called fetchData to
encapsulate the entire process of our sequential network
requests. Inside this function, we wrap our code inside a
try/catch block. As required, since we utilize await in our
network requests, we prepend the function definition with
the async keyword. This indicates to the browser and
JavaScript that this function implements the async/await
functionality for asynchronous calls. The variable user
awaits the completion of the fetchUserData method,
signified by the await keyword. This ensures that the code
remains asynchronous and waits for execution to be
completed. The variable user is then passed to the
fetchUserPosts invocation on the subsequent line. Once
again, we utilize the await keyword to indicate that this will
be an asynchronous call, and the variable posts will need to
wait for the function execution to be completed before it can
obtain the data. Finally, on the next line, we utilize the
posts variable along with the await keyword to populate the
commentsArray. Any failure occurring at any point in the
code will be caught by the catch block we specifically
added.
We have not only achieved a significant reduction in the
number of lines of code required, but also vastly improved
the readability of the code by adopting a synchronous
coding style.
Web APIs
The web APIs are technically not a language feature and a
few of them have been in existence since the early days of
JavaScript. They are a set of tools and protocols that allow
different software applications to communicate with each
other over the web. They provide a way for web developers
to interact with various web services and functionalities,
such as fetching data from servers, manipulating the DOM,
handling user input, and more. In this segment, we won’t
cover every Web API. Instead, we’ll focus on a selection of
web APIs and describe their roles in JavaScript.
Here are some common examples of Web APIs in JavaScript:
• DOM: This API allows JavaScript to interact with the
HTML and CSS of a web page dynamically. We can use
the methods and properties provided to manipulate the
structure, content, and style of a webpage.
• XMLHttpRequest (XHR): This API allows JavaScript
to make HTTP requests to servers asynchronously. It’s
commonly used to fetch data from a server without
needing to reload the entire webpage. XHR is often used
in Asynchronous JavaScript and XML (AJAX) to
create dynamic and interactive web applications.
• Fetch API: Like XHR, the Fetch API provides a modern,
Promise-based interface for making HTTP requests in
JavaScript. It offers a more streamlined and flexible way
to fetch resources asynchronously from the server.
• Web storage API: This API provides mechanisms for
storing data in the browser, including sessionStorage
and localStorage. Developers can use these storage
options to persist data locally on the user’s device, such
as user preferences, shopping cart items, or
authentication tokens.
• Geolocation API: This API allows web applications to
access the user’s geographical location information. It
enables developers to build location-aware web
experiences, such as mapping services, location-based
reminders, or weather apps.
• Canvas API: The Canvas API provides a way to
dynamically generate and manipulate graphics and
animations on a web page using JavaScript. It is
commonly used for creating interactive visualizations,
games, and multimedia content.
• Web audio API: This API allows developers to
generate, manipulate, and play audio in the browser
using JavaScript. It is useful for building audio-related
applications, such as music players, audio editors, or
sound synthesizers.
These are just a few examples of the web APIs that are
commonly used in daily usage by JavaScript developers.
Each API serves a specific purpose to help create interactive
and intuitive web applications.
Events in JavaScript
In JavaScript, events are essentially actions or occurrences
that occur in the browser, triggered by the user or the
browser itself. They are a by-product of user interactivity.
For example, events include clicking a button, hovering over
an element, pressing a key on the keyboard, resizing a
browser window, and so on.
Event handling involves specifying the code that should be
executed in response to a particular event. For example, if a
user clicks on a button designated for signing in existing
users, the corresponding code should consider the
username and password entered by the user and send it to
the server for user verification. This is commonly achieved
by attaching event listeners to HTML elements. Event
listeners listen for specific events to occur, and when they
do, they execute a callback function.
Let us explore the most basic example of how events are
handled in vanilla JavaScript:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="UTF-8">
5. <meta name="viewport" content="width=device-
width, initial-scale=1.0">
6. <title>Event Handling Example</title>
7. </head>
8. <body>
9. <button id="myButton">Click Me!</button>
10.
11. <script>
12. // Get a reference to the button element
13. var button =
[Link]('myButton');
14.
15. // Add an event listener to the button
16. [Link]('click', function() {
17. // Code to run when the button is clicked
18. alert('Button clicked!');
19. });
20. </script>
21. </body>
22. </html>
In the provided HTML code, there is a single button
displayed on the UI, assigned with the id myButton. In the
JavaScript section, we utilize the DOM method
getElementById to locate the element with the id
myButton in our HTML DOM. Once we have obtained the
corresponding HTML element, we attach an event listener
to it using another DOM method, addEventListener. This
method requires two arguments: the first argument
specifies the event we intend to target, which in our case is
the click event. The second argument is the callback
function to be executed whenever the button with the id
myButton is clicked.
Event propagation
Event propagation refers to how events are managed as
they propagate or traverse through the DOM elements in
the browser. It involves two phases: the capturing phase
and the bubbling phase. Understanding these phases is
crucial for comprehending the intricate nature of event
handling in a real-life web application. This understanding
serves as the groundwork for comprehending Design
Patterns that aid in dealing with asynchronous
programming in JavaScript. Let us try to understand the
difference between event capturing and event bubbling
using an image:
Figure 5.3: Capturing vs. bubbling
Capturing phase
As depicted in the diagram, the capturing phase of event
handling can be seen as a top to bottom approach. In this
phase, the event is captured from the outermost element
down to the target element. The capturing phase allows you
to intercept events at the ancestor elements before they
reach the target element. However, by default, event
handlers are not executed during this phase unless
explicitly set.
Bubbling phase
Event bubbling is a type of event propagation where the
event starts from the target element and then bubbles up
through its ancestors in the DOM hierarchy, all the way up
to the root of the document (usually the <html> element).
During the bubbling phase, each ancestor element in the
DOM hierarchy can handle the event. Event bubbling is the
default behavior in most modern browsers.
Event Loop
Now that we have a solid understanding of events and how
they are managed in JavaScript, let us explore how
asynchronous events are handled. In JavaScript, the Event
Loop is a vital concept that governs the handling of
asynchronous operations. It serves as the mechanism
ensuring non-blocking I/O operations, enabling JavaScript
to efficiently manage multiple tasks.
To understand Event Loop, we need to understand the two
primary participants in this concept — Call Stack and Event
Queue.
JavaScript follows a single-threaded execution model by
default, meaning it can execute only one task at a time. The
Call Stack is where function calls are queued and executed
in a last-in, first-out (LIFO) manner. When a function is
invoked, it is added to the stack, and when it completes, it is
removed from the stack. Like the Call Stack, the Event
Queue is a queue that holds events/callbacks that are ready
to be processed. The Event Loop, meanwhile, continuously
monitors the Call Stack and the Event Queue. It checks if
the Call Stack is empty and if there are any events in the
Event Queue. If the Call Stack is empty and there are events
in the Event Queue, the Event Loop moves the first
event/callback from the Event Queue to the Call Stack for
execution:
1. [Link]("Start");
2.
3. setTimeout(function() {
4. [Link]("Inside setTimeout");
5. }, 0);
6.
7. [Link]("End");
Let us use the provided code example as a reference to
understand how the Event Loop, the Call Stack, and the
Event Queue work together in harmony:
• Initially, the JavaScript engine starts executing the code
synchronously from the top to the bottom. So, based on
the code example above, "Start" will be logged first on
the console. Then, the setTimeout function will be
called, and finally, "End" will be logged. At this point,
the synchronous execution is completed.
• When the setTimeout function is encountered, it
schedules the callback function to be executed after a
specified delay (even though the delay is 0 milliseconds
in this case). However, the callback function is not
executed immediately; instead, it is pushed into the web
API environment to be executed later.
• Once the callback function is ready to be executed, it is
placed in the Event Queue. The Event Queue holds
events/callbacks that are ready to be processed.
• The Event Loop continuously monitors the Call Stack
and the Event Queue. It checks if the Call Stack is empty
and if there are any events in the Event Queue. If the
Call Stack is empty and there are events in the Event
Queue, the Event Loop moves the first event/callback
from the Event Queue to the Call Stack for execution.
• When the callback function from the setTimeout
operation is moved from the Event Queue to the Call
Stack, it gets executed. So, "Inside setTimeout" will be
logged after "End", even though it is scheduled with a
delay of 0 milliseconds.
Therefore, the final output will be:
1. Start
2. End
3. Inside setTimeout
Throttling
Throttling is a Design Pattern in JavaScript that helps
control the rate at which a function is executed, ensuring it
is called at the most once within a specified time period.
This pattern is particularly useful in scenarios where event
handlers or other functions are invoked frequently, such as
scrolling or typing events in a web application. Throttling
helps in optimizing performance and managing resources by
limiting the frequency of function calls. In the context of
asynchronous programming, throttling regulates the flow of
asynchronous tasks, preventing them from overwhelming
the system with rapid execution. By spacing out function
calls, throttling can help prevent performance degradation
and ensure smoother operation of the application.
Problem statement
Consider a scenario where we are implementing a search
feature in a web application. As users type into the search
field, an API request is sent to the server to fetch the results
and update the UI accordingly. However, we cannot
accurately guess the speed at which a user can type. For
instance, if we have a user that types very rapidly, it could
lead to a flood of API requests being sent to the server. This
could potentially overwhelm the server by causing
unnecessary load. This will not only impact on the
performance of the application but also put unnecessary
strain on the server.
Implementation
For these Design Patterns, since the implementation steps
are minimal, we will merge the implementation, and the
code example sections into one. To explain throttling, we
will continue with the same example defined in the problem
statement.
Essentially, we need a function that can regulate the flow of
the number of requests being sent to the server, preventing
it from becoming overwhelmed and crashing. In a more
generic sense, we need to implement a process that can
control the number of asynchronous requests.
Step 1: Define the throttle function
In this step, we will define the throttle function that will
regulate the flow of the asynchronous requests. Let us look
at how we can create the throttle function:
1. function throttle(func, delay) {
2. let lastCall = 0; // Initialize a variable to store the
timestamp of the last function call
3.
4. return function(...args) {
5. const now = new Date().getTime(); // Get the current
timestamp
6. if (now - lastCall >= delay) { // Check if enough time
has elapsed since the last call
7. func(...args); // Call the original function with the
provided arguments
8. lastCall = now; // Update the timestamp of the last
call
9. }
10. };
11. }
In the above code example, we have defined the throttle
function. This function accepts two arguments: a function,
which will be the original function requiring throttling, and
a delay, signifying the time in milliseconds during which the
function should not be called again.
In this code, we have utilized the concept of closure. The
variable lastCall, defined on line number 2, will act as a
private variable for the returned function defined from line
4 onwards. Inside the returned function, lastCall keeps
track of the timestamp of the last invocation. The returned
function also accepts any arguments passed to it and checks
whether enough time has elapsed since the last function call
(now - lastCall >= delay). If it has, the original function
(func) is invoked with the provided arguments, and the
timestamp of the last call (lastCall) is updated.
Step 2: Using the throttle function for throttling
In this step, we will use the throttle function that we have
created in the previous step on a function that requires
throttling. In our case, it will be the function that will make
the network request. Let us have a look at the code to
visualize that:
1. // Function to simulate making a network request
2. function makeNetworkRequest(data) {
3. [Link](`Making network request with data:
${data}`);
4. }
5.
6. // Create a throttled version of the network request
function with a delay of 1000 milliseconds (1 second)
7. const throttledNetworkRequest =
throttle(makeNetworkRequest, 1000);
In the above code, we have created a function called
makeNetworkRequest that accepts an argument data.
Although we are only printing a console statement in this
function for demonstration purposes, in a real-life scenario,
this function could contain the network API request that we
intend to make. This function will be triggered every time
the user enters a keyword to search. Therefore, we need to
ensure that this function is throttled.
Hence, we create a new variable called
throttledNetworkRequest that invokes the throttle
function we created in the previous example. We pass the
makeNetworkRequest function and a time of 1 second
(1000 milliseconds) as arguments.
Step 3: Use the throttled function to invoke async
code
For the final step, we will use the throttled function instead
of the original function to make a network request. This
ensures that our network requests have been throttled or
limited to avoid overwhelming the server.
1. [Link]('searchInput').addEventListe
ner('input', function(event) {
2. const query = [Link];
3. throttledNetworkRequest(query);
4. });
In the above code, we have demonstrated the actual
implementation of a user input that triggers the throttled
function. For demonstration purposes, we have assumed our
input element to have the id searchInput. We add an event
listener to the input event of this element, ensuring that
every time the user adds a value to the input box, the
function in the second argument will be triggered. Inside
that callback function, we call the
throttledNetworkRequest method with the value of the
input element.
Throttling is a technique used to control the frequency of
task execution over a specific time period. Instead of
allowing a task to execute, for example, 20 times in a short
interval, throttling reduces the execution frequency to a
manageable number, such as 5 times. This approach helps
prevent excessive strain on the server by spreading out the
workload, ensuring system performance remains stable and
responsive while avoiding potential overloads or crashes
caused by high-frequency requests.
By following these steps, we can implement throttling in our
JavaScript code to control the rate at which network
requests are made, ensuring smoother performance and
avoiding overwhelming the server with excessive requests.
We can adjust the delay parameter as needed to suit our
specific requirements.
Applications
As mentioned before, throttling is excellent for scenarios
where we need to optimize performance and manage
resources. Here are a few of the scenarios where throttling
can be beneficial:
Scrolling events
Throttle the execution of event handlers attached to scroll
events, such as infinite scrolling or parallax effects, to
prevent excessive function calls as the user scrolls.
Resize events
Throttle the execution of event handlers attached to window
resize events to avoid unnecessary recalculations or UI
updates, especially in complex layouts.
Input events
Throttle the execution of event handlers attached to input
events, such as keypresses or mouse movements, to prevent
rapid firing of functions, especially in autocomplete or
search functionality.
Network requests
Throttle the rate of network requests, such as AJAX
requests or API calls, to avoid flooding the server with too
many requests at once and to adhere to rate limits or API
usage policies.
UI updates
Throttle the execution of UI update functions, such as
rendering or DOM manipulation, to optimize performance
and prevent excessive reflows or repaints, especially in
data-intensive applications.
Polling
Throttle the frequency of polling operations, such as
periodic data updates or status checks, to conserve
bandwidth and server resources, especially in long-lived
connections or real-time applications.
Debouncing
Debouncing is a Design Pattern in JavaScript used to ensure
that a function is only executed after a certain period of
inactivity. Unlike throttling, which limits the frequency of
function calls within a specified time interval, debouncing
delays the execution of a function until there has been a
pause or cessation of activity for a predetermined amount of
time. This pattern is particularly useful in scenarios where
rapid or repetitive events, such as user input or event
listeners, may trigger function calls that need to be handled
in a more controlled manner. In the context of asynchronous
programming, debouncing helps manage the flow of
asynchronous tasks by postponing the execution of a
function until a quiet period occurs, thus reducing
unnecessary overhead and improving performance.
Problem statement
Due to the similarities between throttling and debouncing
and to help distinguish between them, we will use the same
example that we have used for throttling. In this scenario,
we are implementing a search feature in a web application.
As users type into the search field, an API request is sent to
the server to fetch the results and update the UI
accordingly. However, we cannot accurately guess the
speed at which a user can type. For instance, if we have a
user that types very rapidly, it could lead to a flood of API
requests being sent to the server. This could potentially
overwhelm the server by causing unnecessary load. This will
not only impact on the performance of the application but
also put unnecessary strain on the server.
Implementation
To explain debouncing, we will continue with the same
example defined in the problem statement. Like the
throttling Design Pattern, the implementation of the
debounce pattern also involves three simple steps.
Essentially, we need a function that can regulate the flow of
the number of requests being sent to the server, preventing
it from becoming overwhelmed and crashing. In a more
generic sense, we need to implement a process that can
control the number of asynchronous requests.
Step 1: Define the debounce function
Much like the first step for the throttling Design Pattern, we
will create a function for implementing the debounce or the
function for handling the wait for invoking asynchronous
functions:
1. function debounce(func, delay) {
2. let timeoutId;
3.
4. return function(...args) {
5. clearTimeout(timeoutId); // Clear any existing timeout
6. timeoutId = setTimeout(() => {
7. func(...args); // Call the original function after the
delay
8. }, delay);
9. };
10. }
The above code shares several similarities with the one we
implemented for throttling. Like the throttling function, the
debounce function also accepts two arguments: an original
function that requires debouncing and a delay, which
represents the time interval (in milliseconds) to wait before
invoking the function after the last call. Similar to the
throttling function, we also have a closure function defined
here. However, unlike the throttling function, the private
variable signifies a timeout ID for handling the setTimeout
method that we will implement for our use case.
Inside the returned function (closure), timeoutId is used to
store the ID of the setTimeout timer. Whenever the
returned function is called, any existing timeout is cleared
using clearTimeout(timeoutId). A new timeout is set
using setTimeout, delaying the execution of func by delay
milliseconds.
Step 2: Using the debounce function for implementing
debounce
In this step, we will use the debounce function that we have
created in the previous step on a function that requires
debouncing. In our case, it will be the function that will
make the network request. Let us have a look at the code to
visualize that:
1. // Function to simulate making a network request
2. function makeNetworkRequest(data) {
3. [Link](`Making network request with data:
${data}`);
4. }
5.
6. // Create a debounced version of the network request
function with a delay of 1000 milliseconds (1 second)
7. const debouncedNetworkRequest =
debounce(makeNetworkRequest, 1000);
In the above code, we have created a function called
makeNetworkRequest that accepts argument data.
Although we are only printing a console statement in this
function for demonstration purposes, in a real-life scenario,
this function could contain the network API request that we
intend to make. This function will be triggered every time
the user enters a keyword to search. Therefore, we need to
ensure that this function is debounced.
Hence, we create a new variable called
debouncedNetworkRequest that invokes the debounce
function we created in the previous example. We pass the
makeNetworkRequest function and a time of 1 second
(1000 milliseconds) as arguments.
Step 3: Use the debounced function to invoke async
code
For the final step, like the throttling pattern, we will use the
debounced function instead of the original function to make
a network request. This ensures that our network requests
have been debounced to avoid overwhelming the server:
1. // Event listener for search input
2. [Link]('searchInput').addEventListe
ner('input', function(event) {
3. const query = [Link];
4. debouncedNetworkRequest(query);
5. });
In the above code, we have demonstrated the actual
implementation of a user input that triggers the debounced
function. Like the old code, for demonstration purposes, we
have assumed our input element to have the id
searchInput. We add an event listener to the input event of
this element, ensuring that every time the user adds a value
to the input box, the function in the second argument will be
triggered. Inside that callback function, we call the
debouncedNetworkRequest method with the value of the
input element.
To help you understand how this will work, let us explore
another way of invoking this function:
1. // Example usage of the debounced function
2. debouncedNetworkRequest(); // This call will be ignored
because no subsequent call is made
3. debouncedNetworkRequest(); // This call will also be
ignored
4. // Wait for 500 milliseconds
5. setTimeout(() => {
6. debouncedNetworkRequest(); // This call will execute
myFunction after the debounce delay has elapsed
7. }, 500);
In the above example, we invoke the
debouncedNetworkRequest multiple times to observe the
debouncing behavior. The first two calls to
debouncedNetworkRequest are made in quick
succession. However, since the debounce delay has not
elapsed, both calls are ignored, and makeNetworkRequest
is not executed. After waiting for the debounce delay (in this
case, 1000 milliseconds), a third call to
debouncedNetworkRequest is made. This time, the
debounce delay has elapsed, so makeNetworkRequest is
executed.
By following these steps, you can implement debouncing in
your JavaScript code to ensure that a function is only
executed after a period of inactivity, thus optimizing
performance and resource utilization. Adjust the delay
parameter as needed to suit your specific requirements.
Applications
Debouncing, like throttling, is excellent for scenarios where
we need to optimize performance and manage resources.
Here few of the scenarios where debouncing can be
extremely useful:
Search autocomplete
In a search autocomplete feature, debouncing can be used
to delay sending AJAX requests for autocomplete
suggestions until the user has paused typing, reducing
unnecessary network traffic and server load.
Window resize or scroll events
Debouncing can be employed to handle window resize or
scroll events in a web application, ensuring that expensive
layout or rendering operations are only performed after the
user has finished resizing or scrolling, rather than on every
intermediate event.
Input field validation
When performing validation on user input fields, such as
email or password validation, debouncing can delay the
validation function until the user has finished typing,
providing a smoother and less intrusive validation
experience.
Button click or form validations
In scenarios where button clicks or form submissions
trigger complex or time-consuming operations, debouncing
can prevent multiple rapid clicks from firing multiple
requests or submissions, ensuring that only the last action is
executed.
Delayed event handling
Debouncing can be used to delay the execution of event
handlers, such as mouseover or mouse-out events, to
prevent rapid flickering or flashing effects caused by rapid
mouse movements.
Delaying API requests
When integrating with external APIs, debouncing can delay
the execution of API requests until the user has completed
their input or interaction, reduced the number of requests
and improving overall efficiency.
Parallelism
As mentioned at the start of this segment, parallelism is a
Design Pattern that deals with executing multiple tasks
simultaneously. This can be achieved either by utilizing
multiple CPU cores or by performing non-blocking
operations concurrently. In JavaScript, parallelism is
particularly useful for tasks that can be broken down into
independent units of work that can execute concurrently,
thereby improving performance and resource utilization.
There are several approaches that we can use for
implementing the parallelism Design Pattern. Among these
approaches, three patterns closely adhere to the JavaScript
way of development:
• Web Workers: Web Workers allow us to run JavaScript
in a background thread separate from the main
execution thread, enabling parallel execution of tasks
without blocking the UI.
• Parallel array methods: The JavaScript array methods
introduced in ES6, such as map, filter, and reduce, can
be parallelized using libraries like [Link]. These
libraries distribute the workload across multiple CPU
cores, enabling parallel execution of array operations.
• Parallel processing libraries: Several libraries and
frameworks are available in JavaScript for parallelizing
tasks and leveraging multiple CPU cores efficiently.
Of these three approaches, we will solely concentrate on
Web Workers in this book.
Web Workers
In the previous segment, we briefly introduced the concept
of parallelism and Web Workers. To gain a better
understanding of what Web Workers do and how parallelism
can help solve critical issues in asynchronous programming,
let us explore a coding example that highlights a potential
problem users might face when dealing with asynchronous
requests:
1. // Function to perform intensive calculations (e.g.,
factorial calculation)
2. function calculateFactorial(n) {
3. let result = 1;
4. for (let i = 1; i <= n; i++) {
5. result *= i;
6. }
7. return result;
8. }
9.
10. // Function to handle button click event
11. [Link]('calculateButton').addEvent
Listener('click', function() {
12. const input =
[Link]('numberInput').value;
13. const result = calculateFactorial(parseInt(input));
14. [Link]('result').innerText =
'Factorial: ' + result;
15. });
In this simple example depicted above, we have attached a
click event listener to a button with the id
calculateButton. Whenever the user clicks on this button,
the value stored inside an input element with the id
numberInput is fetched and stored in the variable input.
This variable is then passed to the calculateFactorial
function to calculate the factorial of the number provided by
the user. The result is then rendered on another element
with the id result.
On the surface, this function appears straightforward,
where the provided code calculates the factorial for the
number entered by the user. However, if the user inputs a
large number or if the factorial calculation becomes
sufficiently complex, this operation can consume a
considerable amount of time. Given JavaScript’s
synchronous nature, this could potentially block the UI
rendering, rendering it unresponsive for the user to interact
with. This issue becomes even more apparent when dealing
with more complex calculations or when attempting to
perform multiple tasks simultaneously.
Such computational problems that could potentially block
the rendering of UI can be resolved by implementing Web
Workers.
Implementation
Traditional JavaScript execution is single-threaded, meaning
tasks are processed sequentially on a single main thread. In
modern web applications, this can lead to performance
bottlenecks, especially when handling heavy computations
or input/output operations. Web Workers provide a solution
by running scripts in separate background threads, enabling
parallelism. This allows developers to offload
computationally intensive tasks, such as data processing,
image manipulation, or network requests, to background
threads, ensuring the main thread remains free for UI
updates. By leveraging Web Workers, developers can
improve performance and maintain a responsive user
interface, enhancing the overall user experience.
Let us utilize the example of calculating a factorial that we
have seen in the previous section to effectively demonstrate
how Web Workers can solve such a critical problem:
Step 1: Create a Web Worker script file
To start, we will create a separate JavaScript file to house
our Web Worker logic. Let us name this file [Link], and
it will contain the code responsible for performing the
intensive calculations:
1. // [Link]
2.
3. // Function to perform intensive calculations (e.g.,
factorial calculation)
4. function calculateFactorial(n) {
5. let result = 1;
6. for (let i = 1; i <= n; i++) {
7. result *= i;
8. }
9. return result;
10. }
11.
12. // Event listener for receiving messages from the main
thread
13. [Link]('message', function(event) {
14. const input = [Link];
15. const result = calculateFactorial(input);
16. // Send the result back to the main thread
17. [Link](result);
18. });
19.
In the provided code, a new file named [Link] has been
introduced. Within this file, the computationally intensive
function calculateFactorial has been moved. Additionally,
the JavaScript keyword self has been utilized to reference
the global scope of the worker, enabling it to interact with
its own context and handle message passing. An event
listener for the message event has been set up using
addEventListener, which triggers the associated callback
function when messages are received. This function
retrieves data from the event argument that will be passed
to it by the element which triggers the message event. It
then invokes the calculateFactorial method to compute
the factorial. Communication between the main thread and
the Web Worker primarily occurs through message passing,
with the Web Worker listening for messages and responding
using the postMessage method.
Step 2: Instantiate a Web Worker in the main thread
For the next step, we will work on the main JavaScript file,
titled [Link] in this example. In this file, we will create a
new Web Worker instance by specifying the path to the
worker script file ([Link]):
1. // [Link]
2.
3. // Create a new Web Worker instance
4. const worker = new Worker('[Link]');
As shown in the code above, we have created an instance of
the Web Worker by specifying the path to the [Link] file.
In this case, it is assumed that both the [Link] and
[Link] files are stored in the same folder.
Step 3: Send data to the Web Worker
In this step, we will continue working on the main thread,
i.e., [Link] file. We will utilize the native postMessage
method provided by the browser to send data from the main
thread to the Web Worker. This data will be received by the
Web Worker through the event listener set up to listen for
messages:
1. // [Link]
2.
3. // Send data to the Web Worker
4. const inputNumber = 10; // Example input
5. [Link](inputNumber);
For simplicity, we have used a constant variable to
represent the value that might have been received from a
user inputting a numerical value in an input element. We
then pass this number to the Web Worker by invoking the
postMessage method on the worker instance.
Step 4: Receive details from the Web Worker
In the final step, we add an event listener in the main thread
to handle messages received from the Web Worker. This
event listener will receive the results of the computation
from the Web Worker and utilize it in the main thread:
1. // [Link]
2.
3. // Receive results from the Web Worker
4. [Link]('message', function(event) {
5. const result = [Link];
6. [Link]('Result received from Web Worker:',
result);
7. // Update the UI or perform further actions with the
result
8. });
In the second step, we invoked [Link] on the
worker after the results were computed. This method sends
the result using postMessage. Now, we need to listen for
the message event in our main thread. Hence, in the code
above, we added an event listener to listen for the message
event triggered from the worker. The data received from the
postMessage call triggered from the worker will be
available in the event variable, which can then be used to
perform further actions on the UI.
At this stage, we have achieved two-way communication
between the worker thread and the main thread. By
delegating computationally expensive tasks to a Web
Worker, we can successfully achieve parallelism and ensure
a smoother execution of the UI. However, there are two
additional steps you can consider, adding to this approach
for improved efficiency and code organization.
Step 5: Terminate the Web Worker
Once the calculations are complete and the Web Worker is
no longer needed, it is crucial to terminate the worker
explicitly to free up resources and prevent memory leaks.
Although Web Workers are garbage-collected when no
longer referenced, relying on this behavior is not a best
practice, especially in applications with heavy resource
usage:
1. // Terminate the Web Worker (optional)
2. [Link]();
Explicit termination ensures efficient resource management
and avoids potential performance issues in long-running
applications.
Step 6: Handling errors (optional)
Error handling is critical for robust Web Worker usage. Any
unhandled exceptions in the Web Worker code can go
unnoticed if not explicitly captured. To catch and handle
these errors effectively, use an event listener for the error
event:
1. // Error handling for the Web Worker
2. [Link]('error', function(event) {
3. [Link]('Error in Web Worker:',
[Link]);
4. });
This ensures that unexpected issues, such as runtime errors
in the worker’s script, can be diagnosed and addressed,
maintaining the stability and reliability of your application.
With these steps, you have implemented Web Workers in
JavaScript for performing intensive calculations
asynchronously. The Web Worker script ([Link]) runs in
the background thread, allowing the main thread to remain
responsive. Communication between the main thread and
the Web Worker is achieved through message passing.
Applications
From the preceding sections, we have observed the
significant utility of Web Workers for scenarios demanding
intensive asynchronous computations. With this in mind, let
us explore several scenarios where Web Workers can
provide substantial benefits:
Intensive calculations
Any task that involves heavy computational processing, such
as mathematical calculations, image processing, or data
manipulation, can benefit from Web Workers. By offloading
these tasks to background threads, the main thread remains
responsive, providing a smoother user experience.
Large data processing
Processing large datasets, such as parsing and analysing
large JSON files, CSV files, or datasets retrieved from APIs,
can be resource intensive. Web Workers enable parallel
processing of data, improving efficiency and reducing the
time required to complete the operation.
Web-based games
Browser-based games often require complex logic and real-
time interaction. Web Workers allow game developers to
perform tasks like physics calculations, AI computations, or
pathfinding algorithms in separate threads, ensuring that
the game remains responsive and immersive.
Real-time data visualizations
Applications that involve real-time data visualization, such
as charts, graphs, or animations, often require continuous
updates to the UI. Web Workers can handle data processing
tasks in the background while the main thread focuses on
rendering the visualizations, ensuring smooth and
responsive updates.
Concurrency
As the word itself states, concurrency from a JavaScript
Design Pattern perspective means enabling the execution of
multiple tasks simultaneously, making efficient use of the
available resources. In the context of asynchronous
JavaScript, concurrency allows different parts of a program
to execute independently without blocking the execution of
other tasks.
Concurrency vs parallelism
In the previous segment, we briefly introduced the concept
of parallelism and Web Workers. To gain a better
understanding from afar, it may seem that concurrency is
the same as parallelism. However, there is a notable
difference between the two.
As we have seen previously, parallelism involves the
simultaneous execution of multiple tasks or processes,
where each task runs independently and potentially on
different physical or logical processors. In an execution
sense, parallel tasks run concurrently but have the
capability to execute simultaneously without sharing
resources or interfering with each other.
Concurrency, in contrast, refers to the system’s capability to
execute multiple tasks or processes, which may potentially
overlap in time. While the formal definition of concurrency
may appear similar to that of parallelism, the crucial
distinction lies in execution. Whereas parallel tasks, in
implementing parallelism, do not share resources and do
not interfere with each other, concurrent tasks may not run
simultaneously. In fact, they may share resources and take
turns in execution, interleaving their operations.
In summary, while concurrency can be achieved even on a
single processor system through interleaved execution,
parallelism typically requires multiple execution units
working simultaneously.
Approaches
We have already explored the approaches needed to
implement concurrency in detail earlier in this chapter.
However, for the sake of continuity, let us provide a brief
summary of these approaches:
Event Loop
JavaScript is inherently single-threaded, meaning it can
execute only one piece of code at a time. However, with
asynchronous operations, tasks can be scheduled to run in
the background while the main thread continues executing
other tasks. This is managed by the Event Loop, which
continuously checks for tasks in the event queue and
executes them when the main thread is free.
Callbacks
One of the traditional ways to achieve concurrency in
JavaScript is through callbacks. Asynchronous functions
accept callback functions as arguments, which are invoked
once the operation is completed. While waiting for the
asynchronous operation to finish, other tasks can continue
executing.
Promises
Promises provide a more structured way to handle
asynchronous operations and concurrency compared to
callbacks. They represent the eventual completion or failure
of an asynchronous operation and allow chaining of
asynchronous tasks. Promises can be executed concurrently
by initiating multiple asynchronous operations, and their
resolutions are handled independently.
Async/await
Introduced in ES2017, async/await is a syntactic sugar built
on top of Promises, offering a more intuitive way to write
asynchronous code. The async keyword defines a function
that returns a Promise, and the await keyword pauses the
execution of the async function until the Promise is
resolved. With async/await, concurrency is achieved by
awaiting multiple Promises concurrently, allowing for
cleaner and more readable code.
Concurrency in JavaScript is crucial for building responsive
and efficient applications, especially for tasks like making
network requests, handling user input, and performing I/O
operations without blocking the main thread. However, it is
essential to manage concurrency carefully to avoid issues
like race conditions, deadlocks, and excessive resource
consumption.
Batch processing
Batch processing is a crucial Design Pattern in JavaScript,
particularly in scenarios where handling numerous
asynchronous tasks efficiently is paramount. This pattern
involves grouping multiple operations or data elements into
batches, which are then processed either sequentially or
concurrently. By consolidating tasks into batches,
JavaScript applications can optimize resource utilization,
improve performance, and streamline the management of
asynchronous operations. This section explores the
fundamentals of batch processing as a Design Pattern in
JavaScript, delving into its benefits, implementation
strategies, and best practices for leveraging its power in
various application scenarios.
Problem statement
In modern JavaScript applications, developers frequently
encounter scenarios where multiple complex operations
need to be performed asynchronously. One common
scenario involves making successive API requests in a web
application. Without batch processing, each API request
would be sent individually, potentially leading to issues such
as network latency, increased server load, and slower
overall response times. Let us examine a sample of such
problematic code:
1. // Problematic code without batch processing
2. async function fetchDataFromAPI(urls) {
3. let results = [];
4. for (let url of urls) {
5. try {
6. let response = await fetch(url);
7. let data = await [Link]();
8. [Link](data);
9. } catch (error) {
10. [Link](`Error fetching data from ${url}:
${error}`);
11. }
12. }
13. return results;
14. }
15.
16. let urls = ['[Link]
'[Link]
'[Link]
17.
18. fetchDataFromAPI(urls)
19. .then(results => {
20. [Link](results);
21. })
22. .catch(error => {
23. [Link](error);
24. });
In the above code, the fetchDataFromAPI function iterates
over an array of URLs and makes individual HTTP requests
using the fetch API. Each request is processed sequentially,
waiting for the response before initiating the next request.
This approach can lead to increased latency and decreased
efficiency, especially when making multiple requests to the
same server or endpoint.
Implementation
Let us implement the batch processing method in the code
example we saw earlier to understand how this Design
Pattern can simplify the critical problem that might arise in
daily JavaScript programming.
Applications
Batch processing as a Design Pattern can be useful in
various scenarios in JavaScript, especially when dealing
with asynchronous operations, large datasets, or resource
intensive tasks. Here is a list of scenarios where batch
processing can be beneficial:
API requests
As demonstrated in the example from the previous section,
batch processing can be effectively employed to handle API
requests. By grouping multiple requests and processing
them in batches, we can significantly reduce network
latency and enhance overall performance, especially when
making multiple requests to the same server or across
multiple servers.
Database operations
Batch processing can be used for inserting, updating, or
deleting multiple records in a database, optimizing database
transactions and reducing overhead.
Image processing
For tasks like resizing, cropping, or applying filters to
multiple images, batch processing can streamline the image
processing workflow and improve processing time.
Data migration
During data migration processes, batch processing can be
used to transfer data in manageable chunks, minimizing
downtime and ensuring data integrity.
Report generation
For generating reports from large datasets or processing
data for analytics purposes, batch processing can optimize
the generation process and improve responsiveness.
Job queues
Batch processing can be integrated with job queues to
process queued tasks in batches, improving throughput and
reducing processing overhead.
By leveraging batch processing in these scenarios,
JavaScript applications can achieve better performance,
scalability, and resource efficiency, ultimately enhancing
the user experience and optimizing system operations.
Promise optimization
Earlier in the chapter, we have read about Promises while
understanding the history of asynchronous programming in
JavaScript. Later, we also came across it while
understanding concurrency as a Design Pattern. Promises,
in itself, are objects used to represent the eventual
completion or failure of an asynchronous operation. Promise
optimization focuses on strategies to enhance the efficiency
and effectiveness of using Promises in JavaScript
applications.
While Promise optimization is not typically referred to as a
Design Pattern in the same sense as, say, the Singleton or
Observer Patterns, it encompasses a set of best practices
and techniques for effectively using Promises in
asynchronous JavaScript programming.
So, let us look at several approaches for Promise
optimization:
Promise chaining
Promise chaining involves linking multiple asynchronous
operations together sequentially, making the code more
linear and easier to understand. This can be achieved by
returning a Promise from within the .then() method
callback or using async/await syntax:
1. // Chaining Promises using async/await
2. async function sequentialAsyncOperations() {
3. try {
4. const result1 = await asyncFunction1();
5. const result2 = await asyncFunction2(result1);
6. const result3 = await asyncFunction3(result2);
7. return result3;
8. } catch (error) {
9. [Link]('Error:', error);
10. }
11. }
12.
13. sequentialAsyncOperations();
In the provided code example, we showcase Promise
chaining using async/await. Three variables, namely
result1, result2, and result3, are created to demonstrate
sequential dependency. Using async/await ensures that
each asynchronous function executes only after the
completion of the preceding request, thereby establishing a
clear Promise dependency structure.
Error handling
Proper error handling is crucial for robust asynchronous
code. Promise optimization emphasizes the use of .catch()
blocks to handle errors gracefully and prevent uncaught
exceptions from crashing the application. Let us see this in
code example:
1. // Error handling with catch block
2. asyncFunction()
3. .then(result => {
4. // Handle result
5. })
6. .catch(error => {
7. [Link]('Error:', error);
8. });
Parallel Promises
In some scenarios, it is beneficial to execute asynchronous
operations in parallel to improve performance. Promise
optimization involves techniques like [Link]() or
[Link]() to execute multiple Promises
concurrently and handle their results collectively:
1. // Parallel Promises using [Link]()
2. [Link]([asyncFunction1(), asyncFunction2(),
asyncFunction3()])
3. .then(results => {
4. // Handle results array
5. })
6. .catch(error => {
7. [Link]('Error:', error);
8. });
The code snippet above demonstrates the usage of the
[Link]() method. This method is static and accepts an
iterable of Promises as input, returning a single Promise.
When all input Promises are fulfilled (including an empty
iterable), the returned Promise is fulfilled with an array
containing the fulfilment values of each input Promise.
Conversely, if any of the input Promises are rejected, the
returned Promise rejects with the reason of the first
rejection encountered. The [Link]() method
functions similarly to [Link](), but with a crucial
distinction. It resolves when all Promises in the iterable
argument have settled, regardless of whether they were
fulfilled or rejected. Unlike [Link](), which rejects
immediately if any Promise in the iterable rejects,
[Link]() waits for all Promises to settle. It
returns a single Promise that resolves to an array of objects,
with each object representing the outcome of each Promise.
These objects contain a status property indicating whether
the Promise was fulfilled or rejected, along with a value or
reason property containing the fulfilment value or rejection
reason, respectively.
Promise reuse
Promises are reusable entities, and optimizing their usage
involves maximizing their reusability to avoid unnecessary
instantiation. This can be achieved by defining Promise-
producing functions that return the same Promise instance
for repeated usage:
1. // Reusing the same Promise instance
2. const sharedPromise = asyncFunction();
3.
4. [Link](result => {
5. // Handle result
6. });
7.
8. [Link](error => {
9. [Link]('Error:', error);
10. });
The provided code example illustrates the reusability of
Promises. We define a variable named sharedPromise,
which contains an asynchronous function returning a
Promise. It is worth noting that it is not mandatory to
immediately chain a .then() and a .catch() method after
the definition. Instead, we can reuse the Promise at any
point in the code following its declaration.
Memory management
Since Promises are objects, memory management is an
essential consideration in Promise optimization. Avoiding
unnecessary creation of Promises and ensuring proper
disposal of unused Promises can help prevent memory leaks
and optimize memory usage:
1. // Ensure proper disposal of Promises
2. let Promise = asyncFunction();
3.
4. // After using the Promise, set it to null or undefined
5. Promise = null;
In the provided code example, we clear the Promise by
setting it to null after we have successfully utilized it. This
practice ensures that the Promise is no longer referenced
and can be garbage collected, potentially freeing up
memory resources.
Performance considerations
Promise optimization also involves considerations for
performance, such as minimizing unnecessary asynchronous
operations, optimizing asynchronous code for execution
speed, and reducing the overall latency of asynchronous
workflows, as shown in the code example:
1. // Minimize unnecessary asynchronous operations
2. const data = fetchData(); // Synchronous operation
3.
4. // Optimize asynchronous code for execution speed
5. asyncFunction()
6. .then(result => {
7. // Handle result
8. })
9. .catch(error => {
10. [Link]('Error:', error);
11. });
By following these steps and techniques, you can effectively
implement Promise optimization in your JavaScript code,
resulting in cleaner, more efficient, and maintainable
asynchronous workflows.
Conclusion
In this chapter, we have learnt about and explored Design
Patterns that are concerned with asynchronous
programming in JavaScript. We started with its historical
context and the challenges faced by developers working
with synchronous code. We explored various approaches to
tackle asynchronicity, including callbacks, Promises,
async/await, generators, and Web APIs. Understanding how
events are handled in JavaScript, we delved into the Event
Loop mechanism, which ensures smooth asynchronous
execution by continuously checking for tasks in the event
queue.
Moving on to Design Patterns for asynchronous JavaScript,
we discussed throttling and debouncing, both aimed at
controlling the rate of function execution. Throttling
regulates the frequency of function calls, while debouncing
ensures a function is executed only after a period of
inactivity. Exploring parallelism and concurrency, we
differentiated between the two concepts. Parallelism
facilitates the simultaneous execution of multiple tasks,
leveraging Web Workers and parallel processing libraries
for optimal resource utilization. Concurrency, on the other
hand, allows tasks to overlap in time, managing
independent execution without blocking other operations.
We examined batch processing as a Design Pattern for
handling multiple asynchronous operations efficiently,
breaking down complex tasks into manageable batches.
Finally, we explored Promise optimization techniques, such
as [Link]() and [Link](), to streamline
asynchronous code execution and improve performance. By
mastering these Design Patterns, developers can effectively
manage asynchronous programming challenges, optimize
performance, and build robust and responsive JavaScript
applications. In the next chapter, we will be looking how to
integrate JavaScript Design Patterns in modern JavaScript
frameworks like React and Vue.
Points to remember
• Asynchronous JavaScript programming emerged as a
solution to facilitate the smooth execution of modern
JavaScript applications. Initially, callbacks were used,
but developers encountered the issue of nested
callbacks, commonly referred to as callback hell. To
address this challenge, JavaScript introduced Promises,
offering a more streamlined approach to handling
asynchronous requests.
• A further improvement called async/await was
introduced in ES2017, which significantly simplified the
handling of Promises. This addition made asynchronous
code appear more synchronous, enhancing readability
and maintainability.
• JavaScript also introduced generators that provide a
powerful way to work with sequences of data that can be
paused and resumed, and Web APIs that provide a way
for web developers to interact with various web services
and functionalities, such as fetching data from servers,
manipulating the DOM, handling user input, and more.
• Event Loop ensures smooth asynchronous execution by
continuously checking for tasks in the event queue and
then dynamically shifting it to the main thread.
• Throttling controls the frequency of function calls by
restricting the number of function calls made within a
specific time frame. Conversely, debouncing, while
similar to throttling, ensures that a function is executed
only after a period of inactivity, effectively delaying its
invocation until a pause in activity occurs.
• Parallelism facilitates the simultaneous execution of
multiple tasks, leveraging Web Workers and parallel
processing libraries for optimal resource utilization.
• Concurrency, on the other hand, allows tasks to overlap
in time, managing independent execution without
blocking other operations. Promises, Event Loop,
callbacks, and async/await are examples of concurrency.
• Batch Processing is a technique for handling multiple
asynchronous operations efficiently, breaking down
complex tasks into manageable batches.
• Promises, when optimized, can significantly enhance
performance. Best practices include avoiding nested
Promises, effectively chaining Promises, handling errors
gracefully, implementing parallel Promises, reusing
Promises, and considering memory and performance
implications.
Exercises
1. Which of the following statements is true?
a. Handling network requests using the traditional
synchronous JavaScript does not block the UI.
b. Debouncing is the process of restricting the number of
function calls made within a specific time frame.
c. Web Workers work on a separate thread from the main
thread, thereby reducing the chances of UI being
overwhelmed for computationally expensive processes.
d. Promises cannot be reused.
2. Which of the following approaches cannot be used
for optimizing Promises?
a. Reusing the Promises by assigning it to a variable.
b. Nesting Promises when they sequential and dependent
on each other.
c. Using [Link]() and [Link]() when
there are multiple requests.
d. Effectively using the .catch() to handle errors in
Promises.
3. What is the name of the Design Pattern that a
function is executed only after a period of
inactivity?
a. Debouncing
b. Batch processing
c. Throttling
d. Concurrency
Answers
1. c
2. b
3. a
CHAPTER 6
Design Patterns in React
Introduction
Modern web applications primarily rely on cutting-edge
JavaScript frameworks such as React and Vue. These
frameworks are built on component-based architecture,
emphasizing modularity and code reusability. In this
chapter, we will explore how established Design Patterns
can be effectively utilized within React to improve
application structure, maintainability, and scalability. We
will examine both the built-in patterns inherent to React
component system, along with popular Design Patterns
commonly adopted by developers to tackle specific
challenges within React. By grasping and implementing
these design principles, we can develop well-organized,
efficient, and easier-to-maintain JavaScript applications.
Structure
This chapter will cover the following topics:
• Overview of React
• Built-in patterns in React
• Popular Design Patterns specific for React
• Using JavaScript Design Patterns with React
Objectives
Upon finishing this chapter, we will be one step closer to
crafting efficient modern JavaScript applications. By the end
of it, we will delve into the optimal utilization of the built-in
patterns within a modern JavaScript framework like React.
Furthermore, we will learn how to integrate the Design
Patterns we have acquired thus far to complement these
built-in patterns. Consequently, we will be equipped to
develop efficient enterprise-level JavaScript applications
that are robust, streamlined, and simpler to maintain.
Overview of React
As explored in previous chapters, JavaScript has been a
cornerstone of web development since the mid-1990s. While
it has evolved significantly over time, there is always room
for improvement. Around 2010, Google shook up the
development community with the release of AngularJS, an
open-source JavaScript-based web framework. AngularJS
offered numerous advantages over existing libraries,
providing a comprehensive solution for UI development, and
easing the burden on developers. However, its steep
learning curve deterred some from adopting it for their
projects.
Three years later, Facebook (now Meta) introduced React,
followed closely by Vue, a library created by Evan You.
These two libraries quickly revolutionized web development
with their user-friendly interfaces, minimal learning curves,
efficient code development, and speedy UI rendering. As of
April 2024, ReactJS boasts an estimated 20.8 million
projects, highlighting the widespread adoption and
effectiveness of these libraries.
In this segment, we will briefly discuss React, first of the
two preferred libraries of many top product companies for
application development. We will explore what makes it so
popular and the advantages it offers over traditional
JavaScript applications.
Code reusability
Before the advent of front-end libraries, reusing UI
components involved complex inheritance patterns or
writing the same code repeatedly. This made codebases
bulky and updates tedious.
Scalability
As applications grew, managing state and keeping the UI
consistent became a significant challenge. Traditional
JavaScript lacked a structured approach for handling
complex UIs.
These are just a few of the major challenges developers
encountered while coding in traditional JavaScript.
React
React was conceived by Jordan Walke, an engineer at Meta
(formerly Facebook), in 2011. Initially deployed on
Facebook News Feed, React’s demonstration proved highly
effective, prompting its integration into Instagram in 2012.
Recognizing its immense potential, Facebook officially open-
sourced React at JSConf in the USA in 2013. This move
enabled the developer community to embrace React, fueling
its widespread adoption and rapid growth. In this segment,
we will briefly examine the challenges encountered by the
development community prior to the advent of React and
the solutions it brings to the table.
React addressed these issues by introducing a component-
based architecture and a virtual DOM. Firstly, React adopts
a component-based approach, breaking down the UI into
reusable components. Each component manages its own
state and logic, preventing cascading changes that affect
the entire codebase. This approach fosters modularity,
maintainability, and facilitates code reuse.
Furthermore, React incorporates the concept of a virtual
DOM, which serves as a virtual representation of the actual
DOM. When changes occur, React employs a sophisticated
diffing algorithm in conjunction with the virtual DOM. This
algorithm determines the minimal set of changes needed on
the UI and updates only those elements, avoiding the need
to refresh the entire page or restructure the entire DOM.
This approach greatly enhances performance, especially for
complex UIs.
Lastly, React encourages the adoption of JavaScript XML
(JSX), an optional syntax extension that permits writing
HTML-like structures within JavaScript code. This enhances
readability and simplifies UI development, making it more
intuitive.
The simplicity and effectiveness of React along with its
compatriot, Vue, resonated with the web development
community at large. Despite their distinct approaches to
problem solving, both resulted in faster and more efficient
UI rendering, improved code structure, and code that is
easier to read and maintain. This proved to be a significant
advantage for enterprise-level applications, leading to swift
adoption by developers.
As of 2024, a majority of the world’s top web applications
are built on these two front-end libraries, underscoring their
widespread acceptance and impact on the industry.
Class-based components
Based on Figure 6.1, let us begin by examining the class-
based approach to writing components:
1. import React, { Component } from 'react';
2.
3. class Post extends Component {
4. constructor(props) {
5. super(props);
6. [Link] = {
7. isLiked: false, // Initial like state
8. };
9. }
10.
11. handleLike = () => {
12. [Link](prevState => ({
13. isLiked: ![Link]
14. }));
15. }
16.
17. render() {
18. const { profilePic, handle, caption, content } =
[Link];
19. const likeIcon = [Link] ? '[Link]' :
'[Link]';
20.
21. return (
22. <div className="post">
23. <div className="post-header">
24. <img src={profilePic} alt="Profile Pic"
className="profile-pic" />
25. <span className="handle">{handle}</span>
26. </div>
27. <div className="post-content">
28. <p className="caption">{caption}</p>
29. {content && <img src={content} alt="Post
Content" className="post-image" />}
30. <button className="like-button" > {[Link]}>
31. <img src={likeIcon} alt="Like Button" />
32. </button>
33. </div>
34. </div>
35. );
36. }
37. }
38.
39. export default Post;
In the example above, we have employed the class-based
approach to create the Post component. As depicted in the
code, we have defined a class named Post that extends
React’s Component class. Similar to a standard JavaScript
class, we have implemented a constructor and two methods.
Notably, the render method is crucial, as React utilizes it to
render the UI. Within this method, we initially declare
several variables received as props (we will delve into this
further in the next section). Subsequently, we compose
HTML using the JSX syntax, seamlessly intertwining
JavaScript logic with native HTML. This JSX, disguised as
HTML within the render method of the Post class, is
utilized to render the UI showcased in Figure 6.1.
As previously mentioned, the key distinction between a
class-based component and a functional component in React
is the utilization of state. In the example provided, we have
established a state object within the props. Within this state,
we have defined a Boolean variable called isLiked to
determine if the current post has been liked by the user. As
observed later in the code, we utilize this state variable to
determine the icon displayed for the like button.
Additionally, we have implemented a handler function on
the like button named handleLike. This function simply
modifies the value of the state variable isLiked using
React’s setState method. According to React’s state
definition, whenever the value of a state variable changes—
essentially, whenever the internal state of a component
changes—it is prudent to re-render that component to
reflect the latest alterations. Consequently, the Post
component is re-rendered each time a user clicks the like
button, ensuring the incorporation of the updated image
defined within the render method.
This approach leverages classes from JavaScript ES6 to
construct React components, with state serving as a central
focus. It proves particularly beneficial for components
requiring intricate rendering logic.
Functional components
Next, we will delve into the method of writing components
using functions:
1. import React, { useState } from 'react';
2.
3. const Story = ({ profilePic, handle }) => {
4. const [isViewing, setIsViewing] = useState(false); //
Initial viewing state
5.
6. const handleView = () => {
7. setIsViewing(prevIsViewing => !prevIsViewing);
8. };
9.
10. return (
11. <div className="story">
12. <img
13. src={profilePic}
14. alt="Profile Pic"
15. className={isViewing ? 'story-profile-pic-active' :
'story-profile-pic'}
16. > 17. />
18. {isViewing && (
19. <div className="story-content">
20. <span className="story-handle">{handle}
</span>
21. </div>
22. )}
23. </div>
24. );
25. };
26.
27. export default Story;
In the provided code, we have crafted the Story component
using the functional approach to writing components in
React. Unlike classes, this method employs simpler
JavaScript functions. Here, we emulate the behavior of
internal state using the useState hook and employ
destructuring to establish a state variable and a method for
modifying that variable.
In the code, isViewing represents the state variable, while
setIsViewing functions as the method for altering its value.
Furthermore, the useState hook accepts a default value,
which we have set to false. This variable is subsequently
utilized to set the class name for the image and display the
handle name.
State
In the preceding segments, we have explored the concept of
state. As demonstrated in the examples, state represents
the properties specific to a component. It is not imposed by
external segments, parent components, or child
components; rather, it exclusively belongs to the component
itself. When a state is updated, it signifies a change in the
component’s internal logic.
For instance, consider the isLiked property of the state in
the Post component, which determines the image displayed
on the UI. By logical inference, if a user dislikes a post, the
image should reflect this change on the UI. Thus, a state
change prompts a re-render of the component.
Furthermore, this internal state can be passed on to a child
component, and the value received by the child component
is referred to as props.
Props
In the preceding segments, we have also explored the
concept of props, although, we have not delved in greater
detail. The primary difference between state and props is
that state, as we have seen before, is exclusive for the
component itself. Props are the values or properties
received by a component from its parent component and
which can be used in the component.
Let us illustrate props with an example. In the class-based
component Post, we have defined props within the
constructor of the class. These props represent the values
received from the parent component. To provide context,
consider the following code snippet in the parent
component:
1. <Post
2. profilePic="[Link]"
3. handle="bpbPublications"
4. caption="The latest in technology for 2024"
5. content="[Link]"
6. />
In the constructor of the Post component, we utilize the
keyword super to access the props passed from the parent
component, subsequently making them available within the
class via [Link]. Within the render method, we employ
destructuring to extract specific properties from these
props. These properties are then utilized throughout the JSX
within the render method to facilitate UI rendering.
Similarly, for the functional component Story, envision the
parent component importing and invoking the Story
component as follows:
1. <Story
2. profilePic="[Link]"
3. handle="bpbPublications"
4. />
As depicted in the code above, two props, profilePic and
handle, are passed to the functional component Story.
These props are subsequently received as arguments within
the component and are utilized in its rendering.
An important distinction between state and props is that
while props are injected into a component and cannot be
modified within the component itself, state is exclusive to
the component and can be modified internally. However, if
props are defined as functions in the parent component,
their values and invocations can be passed to the
component.
React Hooks
React Hooks is a special feature tailored for functional
components in React. As we have discussed previously, the
key distinction between functional components and class-
based components lies in the absence of state in functional
components. While class-based components possess an
internal state, functional components are inherently simpler
and do not possess a state property.
However, state plays a pivotal role in determining the
behavior and rendering of components in React, as well as
in distributing props to child components. To enable
functional components to exhibit state-like functionality,
React introduced the concept of hooks. In this section, we
will first understand the concept of hooks before
understanding how it performs the duties of a Design
Pattern.
It is important to note that we would not delve into the
intricate details of every hook. Instead, we will explore
examples of a few hooks to demonstrate their usability, set a
tone for explaining how they qualify as Design Patterns, and
refresh our knowledge.
Types of Hooks
According to React’s official documentation, Hooks
empower us to leverage various React features within our
components. React offers several built-in Hooks for this
purpose. Additionally, we have the flexibility to combine
these built-in Hooks to create our own custom Hooks. Let us
have a look at all the built-in hooks offered by React.
State Hooks
As we have seen previously, state helps component
remember information, such as user input in a form
component. While class-based components feature the state
property defined within the constructor, React provides two
hooks to replicate this behavior in stateless functional
components:
• useState: The useState Hook is used to declare a state
variable in a functional component. It provides a simple
way to manage local component state, especially when
the state logic is straightforward or involves primitive
data types. It returns an array with two elements: the
current state value and a function to update that state.
useState is ideal for managing simple or small pieces of
state like form inputs, toggles, or counters.
• useReducer: The useReducer hook is used for more
complex state management scenarios where the state
depends on multiple actions or has intricate update
logic. It works by defining a reducer function that
specifies how the state should change based on the
dispatched action. It returns an array with the current
state and a dispatch function to trigger updates.
useReducer is better suited for scenarios involving
complex state transitions, such as managing a shopping
cart, handling form submissions, or working with deeply
nested state.
Context Hooks
Context lets a component receive information from a distant
parent without having to pass props through several
components and thereby taking unnecessary space.
Let us try to visualize a DOM tree in the following figure to
better grasp this topic:
Figure 6.2: A DOM tree structure with the root node splitting down into child
nodes
As illustrated in Figure 6.2, consider a DOM tree where a
value must be passed from the root node (depicted by text
Root inside the circle) to a child node at the bottom of the
tree (denoted by the text L4). Passing values via props from
the root node to the child node would involve passing a
property unnecessarily through three components that have
no use for it other than to pass it along. This can be
circumvented by leveraging the useContext Hook.
Ref Hooks
Previously, we became acquainted with the state of a
component, a property holding information crucial for
rendering. Refs, however, function differently. They enable
a component to store information not intended for
rendering, such as a DOM node to modify its style or a
timeout ID. Unlike state, updating a ref does not trigger a
re-render of the component. Refs are often referred to as an
escape hatch from the React paradigm. This term signifies a
mechanism that allows developers to bypass the declarative
and state-driven approach of React when necessary. In
essence, refs provide a way to perform imperative tasks—
like directly manipulating the DOM or interacting with
browser APIs—that React typically abstracts away. This
makes them exceptionally useful for integrating React
components with non-React systems or handling specific
scenarios where React’s declarative approach falls short.
We utilise the useRef Hook for creating such refs or
references:
1. import React, { useRef } from 'react';
2.
3. function MyComponent() {
4. // Create a ref object
5. const inputRef = useRef(null);
6.
7. // Function to focus the input field
8. const focusInput = () => {
9. [Link]();
10. };
11.
12. return (
13. <div>
14. {/* Assign the ref to the input element */}
15. <input ref={inputRef} type="text" />
16. <button > Input</button>
17. </div>
18. );
19. }
20.
21. export default MyComponent;
For example, in the code snippet above, we have employed
the useRef hook to define a variable that stores the input
text element. Upon clicking the button, we utilize this ref
variable to bring the element into focus. As evident from the
example, using the useRef hook does not trigger a re-
render of the component. Nonetheless, it proves highly
beneficial for storing a DOM node that can be subsequently
utilized for rendering the component based on a condition.
Effect Hooks
The Effect Hooks are among the most commonly and
extensively used Hooks in React. Every React component
follows a distinct lifecycle, guiding its journey from creation
to destruction. React provides several methods for class-
based components to tap into these lifecycle breakpoints,
enabling developers to take critical or definitive actions.
For example, consider a form component that prompts users
to input their personal information. Before the user clicks
the save or update button to send the information to the
server, it is crucial to perform validation on the entered
information, especially in data-sensitive industries like
banking. By leveraging componentDidUpdate, a lifecycle
method triggered every time a component is updated, we
can run validation after each update—whenever the user
enters information on the UI. This validation can then notify
the user of any errors, such as an incorrect email address or
account number.
Unlike class-based components, functional components in
React do not have access to lifecycle methods. Instead, they
rely on Effect Hooks to fulfill this purpose. Effect Hooks
enable a component to connect to and synchronize with
external systems, effectively emulating the behavior of
lifecycle methods. This encompasses tasks such as
interacting with the network, manipulating the browser
DOM, implementing animations, integrating widgets from
other UI libraries, and interfacing with other non-React
code. We utilize the useEffect Hook to implement effects in
functional components:
1. import React, { useEffect, useState } from 'react';
2.
3. function MyComponent() {
4. const [count, setCount] = useState(0);
5.
6. useEffect(() => {
7. // Update the document title with the current count
8. [Link] = `Count: ${count}`;
9. });
10.
11. return (
12. <div>
13. <p>Current count: {count}</p>
14. <button => setCount(count +
1)}>Increment</button>
15. </div>
16. );
17. }
18.
19. export default MyComponent;
In the provided code snippet, we have utilized the
useEffect Hook to display the number of times a button has
been clicked by the user on the title of the webpage. Each
time the user clicks the button, we employ the useState
Hook to update the count, triggering a re-render of the
component. Consequently, with each re-render, the
useEffect hook is invoked, updating the webpage title with
the latest count stored in the state.
Performance Hooks
Re-rendering a component can be computationally
expensive. While it may not be a major concern for smaller
components with minimal state or props, medium to larger
components can experience performance issues, especially
if even minor changes in state or receiving new props
trigger a re-render of the entire component.
Using the analogy of the DOM tree image from the previous
segment, imagine if a DOM node at the top of the tree
requires re-rendering. This would entail that all its children
nodes and elements (potentially half of the entire tree) will
also be destroyed and re-rendered. Consequently, this could
slow down the application, adversely impacting the user
experience.
To mitigate this problem, functional components utilize
performance hooks to optimize re-rendering performance by
skipping unnecessary work. For example, React can be
instructed to reuse cached calculations or to skip a re-
render if the data has not changed since the previous
render.
To skip calculations and unnecessary re-rendering of the
components, we use one of the following hooks:
• useMemo: This Hook allows us to cache the result of
an expensive calculation. For instance, consider a
component that displays a list of products with their
total prices. Calculating the total price involves summing
up all the products in the list, which can be
computationally expensive, especially for large lists. By
utilizing useMemo, we can memoize the total price
calculation. This means that the calculation is cached,
and it is only recalculated when the list of products
changes. This optimization helps improve performance
by avoiding unnecessary recalculations.
• useCallback: This Hook allows us to cache a function
definition before passing it down to an optimized
component. For instance, let us imagine that we are
constructing a parent component responsible for
rendering a list of items, each equipped with an onClick
event handler. Passing a new function reference to each
item on every render could inadvertently trigger
unnecessary re-renders of the items, even if the function
logic remains unchanged. By leveraging useCallback,
you can memoize the onClick event handler function,
ensuring that it remains the same unless its
dependencies change. This optimization strategy
effectively prevents unnecessary re-renders of the items,
enhancing performance and efficiency.
Higher-order components
Higher-order components (HOCs) represent an advanced
technique in React for reusing component logic. Essentially,
a higher order component is a function that takes a
component and returns a new component. While HOCs are
not officially a part of the React API, they emerge as a
Design Pattern from React’s compositional nature.
While a component transforms props and state into UI, an
HOC transforms a component into another component. In
fact, popular libraries like Redux and relay, utilized by
several top React applications, employ the concept of HOCs
extensively. For instance, Redux’s connect and relay’s
createFragmentContainer methods are based on the HOC
pattern.
To cite an example, imagine we have several components in
our application that must be accessed only by authenticated
members. Instead of adding the same authentication logic to
each and every component, we can instead create an HOC
to handle the authentication process:
1. import React from 'react';
2. import { Redirect } from 'react-router-dom';
3.
4. const withAuth = (WrappedComponent) => {
5. const AuthenticatedComponent = (props) => {
6. const isAuthenticated =
checkIfUserIsAuthenticated(); // Function to check if user
is authenticated
7.
8. if (isAuthenticated) {
9. return <WrappedComponent {...props} />;
10. } else {
11. return <Redirect to="/login" />;
12. }
13. };
14.
15. return AuthenticatedComponent;
16. };
17.
18. export default withAuth;
In the provided example, we have created a HOC named
withAuth. This HOC accepts a component as input. It then
verifies whether the user is authenticated. If the user is
authenticated, it returns the original component as is.
Otherwise, it returns the login component. This enables
conditional rendering based on the authentication status.
It can then be used by a component in this fashion:
1. import React from 'react';
2. import withAuth from './withAuth';
3.
4. const Dashboard = () => {
5. return (
6. <div>
7. <h1>Welcome to the Dashboard</h1>
8. {/* Dashboard content */}
9. </div>
10. );
11. };
12.
13. export default withAuth(Dashboard);
As of May 2024, HOCs are not actively used in modern
React code. However, they retain strategic importance as a
Design Pattern that proved extremely beneficial for several
years. Though not actively utilized, they are still employed
by some React developers.
Container/Component Pattern
The Container/Component Pattern, also known as the
Container/Presentation Pattern or Smart/Dumb
Components Pattern, is a Design Pattern commonly used in
React applications to manage state and UI concerns
effectively. Let s visualize how smaller atomic components
can together build an entire page using the following image:
Figure 6.3: Container/Component (Container/Presentation) Pattern can be
combined to generate a complex UI
As depicted in Figure 6.3, this pattern encompasses two
distinct components that synergize to render UI effectively.
The fundamental concept underlying this Design Pattern is
to enforce the separation of concerns by dividing
components into two distinct categories: one set of
components manages logic, while the other set focuses
solely on presentation or rendering duties.
To mitigate potential confusion arising from the term
component when referring to the Design Pattern, let us
adopt the alternative nomenclature Container/Presentation
to understand this pattern further. With this approach in
mind, let us delve into each of these components
individually to gain a comprehensive understanding of this
pattern.
Container components
The first part of the Container/Presentation Design Pattern
is the Container component. The Container components are
responsible for managing state, fetching data, and
interacting with any external APIs or service. They typically
hold the state of the application and pass down the data and
behavior to their child components (in this case, the
presentation components). In case the application is
powered by Redux which manages the global state, it is the
Container components which would be connected to the
Redux store to dispatch actions and retrieve global state:
1. // Container Component
2. import React, { Component } from 'react';
3. import { connect } from 'react-redux';
4. import { fetchData } from './actions';
5.
6. class UserContainer extends Component {
7. componentDidMount() {
8. [Link]();
9. }
10.
11. render() {
12. const { isLoading, users, error } = [Link];
13.
14. return (
15. <div>
16. {isLoading ? (
17. <p>Loading...</p>
18. ) : error ? (
19. <p>Error: {[Link]}</p>
20. ):(
21. <UserList users={users} />
22. )}
23. </div>
24. );
25. }
26. }
27.
28. const mapStateToProps = state => ({
29. isLoading: [Link],
30. users: [Link],
31. error: [Link]
32. });
33.
34. const mapDispatchToProps = dispatch => ({
35. fetchData: () => dispatch(fetchData())
36. });
37.
38. export default connect(mapStateToProps,
mapDispatchToProps)(UserContainer);
The provided code exemplifies the Container aspect of the
Container/Presentation Design Pattern. Within the
UserContainer component, a ternary conditional operator
is employed to determine whether to render a loading
screen, display an error message, or render another
component, UserList. Notably, this code integrates with the
global state management package Redux to handle global
state. Redux’s HOC, connect, is utilized for the
UserContainer component, alongside mapStateToProps
to access the global state for conditional rendering and
mapDispatchToProps, which is utilized within the lifecycle
method componentDidMount to fetch data required by
the UserList component. In essence, this component serves
as the Container part of the Design Pattern, responsible for
managing logic, data fetching, and interacting with external
libraries like Redux.
Presentation components
The second part of the Container/Presentation Design
Pattern is the Presentation component, which is solely
dedicated to the presentation aspect of the application. Also
referred to as dumb components, Presentation components
typically employ simpler functional components to focus on
UI rendering. They receive data and callbacks as props and
render UI elements based on this data. As these components
solely handle UI rendering, they do not internally manage
state and are typically stateless. Instead, they rely on props
passed from their parent container components to render UI
elements. Presentation components boast high reusability
since they are agnostic to the application’s state and data-
fetching logic:
1. // Presentation Component
2. import React from 'react';
3.
4. const UserList = ({ users }) => (
5. <ul>
6. {[Link](user => (
7. <li key={[Link]}>{[Link]}</li>
8. ))}
9. </ul>
10. );
11.
12. export default UserList;
In the provided code snippet, we have created the UserList
component, previously utilized in the Container components
section. In the preceding example, the UserContainer
component passes the list of users as props to the UserList
component. As evident from the code above, this component
lacks any logic or state. Its sole purpose is to utilize the
users value received in props to render a list of usernames.
In this way, the Container component assumes the heavier
and more intricate task of managing logic and data
maintenance, while the Presentation component simply
receives this data from its parent Container component to
render the UI. Working in tandem, both components
efficiently render complex UI elements.
Advantages
The Container/Component (Container/Presentation) Design
Pattern offers the following advantages in building React
applications:
• Separation of concerns: The pattern separates
concerns between managing state and rendering UI,
making the codebase more modular and easier to
maintain.
• Reusability: Presentation components are highly
reusable as they are not coupled with specific data or
state management logic.
• Testability: Container components, being responsible
for state management and logic, can be easily tested in
isolation without concerning UI rendering.
In conclusion, the Container/Component Pattern is a
powerful Design Pattern for structuring React applications,
offering clear separation of concerns between state
management and UI rendering. By encapsulating stateful
logic within container components and keeping presentation
components focused solely on rendering UI elements,
developers can create more modular, reusable, and
maintainable codebases. This pattern promotes better
organization, testability, and scalability of React
applications, making it a preferred approach for building
complex user interfaces with React.
Implementation
To implement the Render Props Design Pattern, two key
components are involved: a parent component and a child
component that share code or behavior. The implementation
typically follows four straightforward steps:
1. The parent component encapsulates some functionality,
or a state and the child component require access to
that functionality or state.
2. To achieve this, following the React convention of
passing information, the parent component will transmit
this behavior or state to the child component through
props. It is crucial to understand that this behavior or
state resides within the parent component or is a part of
it. Hence, the parent component will convey a function
to the child component via props. Typically, this function
will accept some arguments and return React elements.
3. The child component will then invoke this function
provided by the parent component via props and pass
any required parameters.
4. Finally, the parent component executes the function
and renders the result, possibly passing along additional
props or state.
Let us translate these textual steps into a code
implementation. To facilitate our understanding of the
implementation of this Design Pattern, let us consider the
example of a counter and a button where clicking the button
will increase the count of the counter.
Firstly, we will create a Counter component that manages
the count state. This component will also render a Button
component through a render prop, allowing the button to
increment the count when clicked:
1. // [Link]
2. import React, { useState } from 'react';
3.
4. const Counter = ({ render }) => {
5. const [count, setCount] = useState(0);
6.
7. const incrementCount = () => {
8. setCount(count + 1);
9. };
10.
11. return (
12. <div>
13. {render(count, incrementCount)}
14. </div>
15. );
16. };
17.
18. export default Counter;
19.
In this code example:
1. The Counter component uses the useState hook to
create a state variable called count, initialized to 0. The
setCount function is used to update this state.
2. The incrementCount function updates the count state
by calling setCount with the new value: count + 1.
3. The Counter component does not directly render any
specific UI elements (like a button). Instead, it uses a
Render Props Pattern. It receives a render prop from its
parent component and invokes this prop, passing the
current count value and the incrementCount function
as arguments.
4. The parent component defines what the UI should look
like by providing a function as the render prop. This
function is responsible for rendering the UI (e.g., a
button) and attaching the incrementCount function as
the click handler.
Next, let us create the Button component:
1. // [Link]
2. import React from 'react';
3.
4. const Button = ({ onClick, label }) => {
5. return <button > 6. };
7.
8. export default Button;
9.
In the above code example, we have created the Button
component, which receives the text to display on the button
and the click handler via its props. Serving as a
presentation component, Button simply receives its props
and utilizes them to render the button UI.
Now, we will combine the two and add a wrapper
component above the Counter component to complete the
implementation of the Render Props Design Pattern:
1. import React from 'react';
2. import Counter from './Counter';
3. import Button from './Button';
4.
5. const App = () => {
6. return (
7. <div>
8. <Counter
9. render={(count, incrementCount) => (
10. <Button label=
{`Clicked ${count} times`} />
11. )}
12. />
13. </div>
14. );
15. };
16.
17. export default App;
18.
In the provided code, we have completed the
implementation by importing both the Counter and Button
components and using them together. Inside the App
component, we have used the Counter component and
passed the Button component within the render props of
the Counter component. This way, the Counter component
passes its internal state to the Button component and
receives the callback whenever the button is clicked to
update its state and pass the updated state to be displayed
on the button. This showcases how the Render Props
Pattern facilitates sharing behavior between components.
Advantages
The Render Props Design Pattern offers the following
advantages in building React applications:
• Reusability: Components can be reused across
different contexts since they are not tightly coupled.
• Flexibility: The child component has control over what
gets rendered, allowing for greater customization.
• Encapsulation: Parent components can encapsulate
complex logic or state management, keeping child
components simple and focused on presentation.
• Composability: Components can be composed together
easily, enabling the creation of more complex UIs from
simpler building blocks.
In conclusion, the Render Props Pattern in React offers a
versatile solution for creating reusable components with
shared behavior or state. By passing a function prop from a
parent to a child component, render props enable
encapsulation, flexibility, and composability in building UIs.
This pattern promotes clean, modular code and encourages
the separation of concerns, making it easier to manage
complex applications and promote code reusability.
Implementation
To implement the Context API Pattern, follow these three
steps:
1. Creating a context: We start by creating a context
that can be consumed by any component at any level of
the tree. This is done using the createContext function
provided by React:
1. const MyContext = [Link]();
2. Providing context values: Next, we wrap the part of
the component tree that needs access to the context
within the Provider component:
1. <[Link] value={/* some value */}>
2. {/* Your component tree */}
3. </[Link]>
3. Consuming context values: To access the context
values, use the Consumer component or the
useContext Hook. The Consumer component uses the
Render Prop Pattern, while the useContext Hook allows
you to consume context values in functional components:
1. // using Consumer wrapper
2. <[Link]>
3. {value => /* render something based on the context
value */}
4. </[Link]>
5.
6. // useContext hook
7. import React, { useContext } from 'react';
8.
9. function MyComponent() {
10. const value = useContext(MyContext);
11. // Use the context value here
12. }
Now, let us apply these steps to create a code example:
1. // [Link]
2. import React from 'react';
3.
4. const ThemeContext = [Link]('light');
5.
6. export default ThemeContext;
7.
8. // [Link]
9. import React from 'react';
10. import ThemeContext from './ThemeContext';
11. import Toolbar from './Toolbar';
12.
13. class App extends [Link] {
14. render() {
15. return (
16. <[Link] value="dark">
17. <Toolbar />
18. </[Link]>
19. );
20. }
21. }
22.
23. // [Link]
24. import React from 'react';
25. import ThemeContext from './ThemeContext';
26.
27. function Toolbar() {
28. return (
29. <div>
30. <ThemedButton />
31. </div>
32. );
33. }
34.
35. // [Link]
36. import React from 'react';
37. import ThemeContext from './ThemeContext';
38.
39. function ThemedButton() {
40. const theme = useContext(ThemeContext);
41. return <button style={{ background: theme
}}>Themed Button</button>;
42. }
In the provided code example, we have created four files:
• [Link]: This file creates the context using
the createContext() function from React. The default
value for the context is set to 'light'.
• [Link]: This file serves as the container or wrapper
component for our application. It provides the
[Link] component to make the
context available to all components in the tree. It also
renders the Toolbar component.
• [Link]: This file represents an intermediate node or
level in our DOM tree. It would need to pass props in the
absence of a context. However, with the context API, it
does not need to directly pass props down to its child
components.
• [Link]: This file represents a component
that requires the context provided by the wrapper
component (App). It uses the useContext() hook to
consume the context and render the button with the
appropriate theme.
By using the context API, we can avoid prop drilling and
make the theme context available to any component in the
tree without the need for intermediate components to pass
down props.
Advantages
The Context API Design Pattern offers the following
advantages in building React applications:
• Global state management: Context API allows us to
create a global state that can be accessed by any
component at any level in our DOM tree, without the
need to pass props down manually through multiple
levels of the component tree. This makes managing
application-wide state much easier.
• Avoids props drilling: In large applications with
deeply nested component hierarchies, passing props
through multiple layers can become cumbersome and
error prone. Context eliminates this problem by
providing a centralized way to share data without the
need for prop drilling.
• Simplifies component composition: Context
simplifies the process of composing components by
providing a way to share data between them. This allows
us to create more modular and reusable components
without worrying about how they will access shared
data.
• Encourages separation of concerns: By decoupling
the state management from the component hierarchy,
context encourages a cleaner separation of concerns
within your application. Components can focus on
rendering UI and handling user interactions, while
context providers handle state management.
• Performance optimization: Context API comes with
built-in optimizations to prevent unnecessary re-renders
of components that consume context values. It uses a
subscription-based model to only update components
when the relevant context value changes, improving
performance.
• Scalability: As your application grows, managing state
becomes increasingly complex. Context provides a
scalable solution for managing state that can easily
accommodate the growing needs of your application.
Overall, the Context API Pattern in React offers a powerful
and flexible way to manage application state and share data
between components, making it a valuable tool for building
complex and maintainable applications.
Module Pattern
The Module Design Pattern is a way of organizing code into
separate, self-contained modules that encapsulate related
functionality. React, on its own, emphasizes on creating
components that are essentially modular in nature. So, in
the context of React, we can apply the Module Design
Pattern to create reusable components.
Here is how we can utilize the Module Design Pattern in
React:
1. Identify functionalities: We begin by outlining the
fundamental behaviors or features of our application.
For example, in case of a social media page, we can
consider functionalities such as displaying user posts,
interacting with posts (like, comment, share), displaying
comments, etc. So, the first order of the day is to
determine the functionalities that can be encapsulated
into separate modules. These functionalities could be UI
components, data fetching logic, state management, or
any other reusable piece of code. We begin by outlining
the fundamental behaviors or features of our
application.
2. Create React components: Once we have identified
and outlined every functionality, the next step is to
create a React component to represent these
functionalities. We need to create separate files for each
component to keep them organized.
3. Encapsulate logic: Once we have identified the core
functionalities of our application, it’s crucial to ensure
that each component encapsulates its own logic and
functionality. Clear boundaries should be established to
define the purpose of each component, avoiding
ambiguity, and promoting reusability and
maintainability. By adhering to this principle,
components become more self-contained and easier to
understand, maintain, and reuse across different parts of
the application.
4. Export and import components: We need to export
each component from their respective files using the
export keyword and import them into other files where
they are needed using the import keyword.
5. Pass props: Now, we need to utilize props to pass data
and functions between parent and child components.
This allows us to customize the behavior of each module
as per the requirements.
6. Compose components: For the final part of the
puzzle, we will compose larger UIs by combining these
modular components together. This promotes reusability
and maintainability by keeping the codebase organized
and easy to understand.
Let us look at an example to put these steps into action:
In this example, we will demonstrate how to structure a
page by creating distinct components for each significant
element. Each component will encapsulate a specific
functionality or part of the page, contributing to a modular
and organized architecture:
1. // [Link]
2. import React from 'react';
3.
4. const Header = () => {
5. return (
6. <header>
7. <h1>My Blog</h1>
8. </header>
9. );
10. };
11.
12. export default Header;
13.
In the provided example, we initiate the page structure by
creating a dedicated component responsible for rendering
the header. This component serves the specific purpose of
incorporating a header into the webpage, thereby
promoting clarity and modularity in the codebase:
1. // [Link]
2. import React from 'react';
3.
4. const PostList = ({ posts }) => {
5. return (
6. <div>
7. <h2>Latest Posts</h2>
8. <ul>
9. {[Link](post => (
10. <li key={[Link]}>
11. <h3>{[Link]}</h3>
12. <p>{[Link]}</p>
13. </li>
14. ))}
15. </ul>
16. </div>
17. );
18. };
19.
20. export default PostList;
Following the modular approach, we proceed to craft a
component tailored for displaying a list of posts. This
component is designed to accept a prop named posts,
which is anticipated to be an array. Through iteration over
this array, the component dynamically generates a list, with
each item containing a title and body, facilitating the
showcasing of post titles and content on the user interface.
1. // [Link]
2. import React from 'react';
3.
4. const Footer = () => {
5. return (
6. <footer>
7. <p>© 2024 My Blog</p>
8. </footer>
9. );
10. };
11.
12. export default Footer;
13.
In alignment with the modular design, we proceed to
construct a footer component, as demonstrated in the
provided code example.
Subsequently, we integrate all these distinct components
into a wrapper application. In this application, we curate a
list of posts, which is then supplied to the PostList
component. This furnishes the functionality to generate a
comprehensive list of posts within the application’s
interface:
1. // [Link]
2. import React from 'react';
3. import Header from './Header';
4. import PostList from './PostList';
5. import Footer from './Footer';
6.
7. const App = () => {
8. const posts = [
9. { id: 1, title: 'First Post', body: 'This is the content of
the first post.' },
10. { id: 2, title: 'Second Post', body: 'This is the content
of the second post.' },
11. { id: 3, title: 'Third Post', body: 'This is the content of
the third post.' },
12. ];
13.
14. return (
15. <div>
16. <Header />
17. <PostList posts={posts} />
18. <Footer />
19. </div>
20. );
21. };
22.
23. export default App;
In this example, Header, PostList, and Footer components
are separate modules, each responsible for rendering a
specific part of the blog UI. App component composes these
modules together to create the blog application.
This modular approach makes it easier to manage and
maintain the codebase, as each component focuses on a
specific aspect of the application’s UI. Additionally, it allows
for reusability, as these components can be used in other
parts of the application or even in different projects.
Factory Pattern
The Factory Design Pattern is a Creational Design Pattern
that provides an interface for creating objects without
specifying their concrete classes. In the context of React, we
can implement the Factory Pattern to create components
dynamically based on certain conditions or configurations.
Let us delve directly into an example to illustrate how we
can implement the Factory Design Pattern in a React
application:
1. import React from 'react';
2.
3. // Define different types of components
4. const Button = ({ onClick, children }) => (
5. <button >6. );
7.
8. const Input = ({ onChange, value }) => (
9. <input type="text" value=
{value} />
10. );
11.
12. // Factory function to create components based on type
13. const ComponentFactory = {
14. createComponent: (type, props) => {
15. switch (type) {
16. case 'button':
17. return <Button {...props} />;
18. case 'input':
19. return <Input {...props} />;
20. default:
21. throw new Error('Invalid component type');
22. }
23. },
24. };
25.
26. // Example usage of the factory function
27. const MyComponent = () => {
28. const buttonComponent =
[Link]('button', {
29. onClick: () => alert('Button clicked!'),
30. children: 'Click me',
31. });
32.
33. const inputComponent =
[Link]('input', {
34. onChange: (e) => [Link]([Link]),
35. value: '',
36. });
37.
38. return (
39. <div>
40. {buttonComponent}
41. {inputComponent}
42. </div>
43. );
44. };
45.
46. export default MyComponent;
In this example, we have defined two components that can
be dynamically generated using our component factory. The
Button component receives a click event handler and
content to display as props, while the Input component
receives a change event handler and a value prop.
Next, we create a component factory where users can
specify the type of element they want to produce and
provide the necessary props. Using a switch statement, we
can then return the appropriate component instance to the
caller of the factory function.
As we can see later in the code, in the MyComponent
component, we use the ComponentFactory to create
instances of Button and Input components with specific
props.
In this way, we can abstract the creation of components and
make the code more flexible and maintainable, especially
when dealing with dynamic component creation or
component variations based on certain conditions.
Observer Pattern
The Observer Design Pattern belongs to the Behavioral
category of Design Patterns where an object (known as the
subject) maintains a list of its dependents (observers) and
notifies them of any state changes, usually by calling one of
their methods.
In the context of React, we can implement the Observer
Pattern using a combination of React’s state management
and lifecycle methods.
For this example, we will use functional components and
hooks to implement the observer pattern in React:
1. import React, { useState, useEffect } from 'react';
2.
3. // Subject component
4. const SubjectComponent = () => {
5. const [subjectState, setSubjectState] =
useState('Initial state');
6. const [observers, setObservers] = useState([]);
7.
8. // Function to update subject state
9. const updateState = () => {
10. setSubjectState('New state');
11. };
12.
13. useEffect(() => {
14. // Notify observers when state changes
15. notifyObservers();
16. }, [subjectState]);
17.
18. // Function to register observers
19. const addObserver = (observer) => {
20. setObservers([...observers, observer]);
21. };
22.
23. // Function to notify observers
24. const notifyObservers = () => {
25. [Link]((observer) =>
[Link](subjectState));
26. };
27.
28. return (
29. <div>
30. <h2>Subject Component</h2>
31. <p>State: {subjectState}</p>
32. <button > State</button>
33. </div>
34. );
35. };
36.
37. // Observer component
38. const ObserverComponent = ({ subject }) => {
39. const [observerState, setObserverState] =
useState('');
40.
41. // Function to update observer state
42. const update = (newState) => {
43. setObserverState(newState);
44. };
45.
46. useEffect(() => {
47. // Add itself as an observer to the SubjectComponent
48. [Link]({ update });
49. return () => {
50. // Cleanup: Remove itself as an observer
51. [Link]({ update });
52. };
53. }, [subject]);
54.
55. return (
56. <div>
57. <h2>Observer Component</h2>
58. <p>State: {observerState}</p>
59. </div>
60. );
61. };
62.
63. // App component
64. const App = () => {
65. return (
66. <div>
67. <SubjectComponent />
68. <ObserverComponent subject=
{SubjectComponent} />
69. </div>
70. );
71. };
72.
73. export default App;
In the provided code example, we have created three
components: SubjectComponent, which represents the
subject (object) to be observed; ObserverComponent,
responsible for displaying the state it observes; and App,
which serves as a wrapper for these two components.
In the SubjectComponent, we have initialized the state
using the useState hook to maintain the initial state. We
have defined an updateState method to update the state
value and an empty array called observers to store
observer components. Additionally, we have implemented a
method to add observers to the observers array and a
notifyObservers method to notify all observers whenever
the state changes. We have used the useEffect hook with a
dependency on the subjectState variable to trigger the
notifyObservers function whenever subjectState changes.
In the ObserverComponent, we receive a subject in the
props, which will be added to the state. We then trigger the
addObserver method of the SubjectComponent, thereby
establishing a link between the two components. This
connection allows the ObserverComponent to observe
changes in the state of the SubjectComponent.
This basic example shows how we can achieve an Observer
Pattern in React.
Decorator Pattern
The Decorator Pattern is a Structural Design Pattern that
allows behavior to be added to individual objects
dynamically, without affecting the behavior of other objects
from the same class. HOCs in React perfectly demonstrate
the implementation of the Decorator Pattern. As we have
seen earlier, they are often used to enhance the behavior of
the existing components without modifying them:
1. // [Link]
2. import React from 'react';
3.
4. const withBorder = (WrappedComponent) => {
5. return class WithBorder extends [Link] {
6. render() {
7. return (
8. <div style={{ border: '1px solid red' }}>
9. <WrappedComponent {...[Link]} />
10. </div>
11. );
12. }
13. };
14. };
15.
16. export default withBorder;
In this example, we have created an HOC called
withBorder, which enhances any component by adding a
simple red border. The HOC receives the component as a
prop and wraps it with a div that applies the border styling.
This allows us to apply consistent styling across multiple
components without repeating the border logic in each
individual component:
1. // [Link]
2. import React from 'react';
3. import withBorder from './withBorder';
4.
5. class MyComponent extends [Link] {
6. render() {
7. return <div>Hello, World!</div>;
8. }
9. }
10.
11. export default withBorder(MyComponent);
To utilize this HOC, we merely wrap the component inside
the invocation of the withBorder HOC. This enables us to
reuse the withBorder HOC to enhance other components
as well, without altering their original implementation. This
exemplifies the Decorator Pattern in React using HOC.
Conclusion
In this chapter, we delved into one of the most prominent
modern JavaScript frameworks and libraries: React. Given
that a vast majority of web applications, as of 2024, are built
using React, we explored ways to enhance their efficiency
through the application of Design Patterns.
To begin with, we looked at a brief overview of React before
diving into its built-in patterns such as the component-based
architecture, states, props, hooks, and HOCs. We then
explored several Design Patterns tailored for React,
including the Container/Presentation, context API, and
Render Props Patterns. Concluding the React section, we
implemented JavaScript Design Patterns in React to
illustrate their practical application.
In summary, React is designed to optimize performance,
streamline code, and enhance readability and
maintainability. It inherently embodies Design Patterns that
align with established JavaScript principles. Incorporating
traditional JavaScript Design Patterns into React
applications further enhances their efficiency and
effectiveness. In the next chapter, we will take on the
popular UI framework VueJS and integrate Design Patterns
to build Vue applications.
Points to remember
• React and Vue emerged as solutions to address the
challenges encountered by developers when using
traditional JavaScript. By minimizing the need for direct
DOM manipulation, they significantly enhanced code
reusability. Consequently, they facilitated the
development of larger and more intricate web
applications with greater efficiency and scalability.
• Both React and Vue adhere to a component-based
architecture, wherein each component serves as a
standalone entity comprising its own encapsulated logic,
data, and properties. These components are akin to
LEGO blocks, capable of being interconnected to
construct intricate applications. Their presence
inherently embodies various Design Patterns such as the
Modular Pattern, Singleton Pattern, Strategy Pattern,
and Composite Pattern, among others, thereby
promoting code modularity and reusability.
• While React and Vue both employ internal state
management and prop passing to child components,
React benefits from patterns tailored specifically for its
ecosystem. These include the Container/Presentation
Pattern, which divides components into logic-holding
and UI-rendering roles, the context API for global state
management (later popularized by Redux), and render
props, facilitating component rendering through props.
These patterns enhance React’s capabilities and
promote cleaner, more efficient code organization.
• In conclusion, whether through its native capabilities or
through patterns specific to their ecosystems, React can
leverage traditional JavaScript Design Patterns to
enhance code development. These patterns serve as
tried and true strategies for optimizing application
architecture, promoting reusability, and improving
overall code quality in both frameworks.
Exercises
1. Which traditional JavaScript Design Pattern is best
emulated by the concept of HOCs in React?
a. Singleton Pattern
b. Strategy Pattern
c. Modular Pattern
d. Decorator Pattern
2. Which of the following statement is NOT true?
a. The Container/Presentation Design Pattern divides
components into logic-holding and UI-rendering roles.
b. Computed properties and watchers help maintain
reactivity of the code in React.
c. Render props basically ideates that we can pass React
components or elements via props.
d. The Context API Pattern of React helps in avoiding
drilling of props over the entire DOM tree.
3. Which of the following use-cases best describe the
implementation of Decorator Pattern in React?
a. Implementation of components, states, and props in a
React application.
b. Using React’s state management and lifecycle methods
to keep components abreast of changes in other
components.
c. Using HOCs to enhance components.
d. Creating functions that can spew out multiple versions
of components.
Answers
1. d
2. b
3. c
CHAPTER 7
Design Patterns in Vue
Introduction
Modern web applications primarily rely on cutting-edge
JavaScript frameworks such as React and Vue. These
frameworks are built on component-based architecture,
emphasizing modularity and code reusability. In this
chapter, we will explore how established Design Patterns
can be effectively utilized within these frameworks to
improve application structure, maintainability, and
scalability. We will examine both the built-in patterns
inherent to React and Vue component systems, along with
popular Design Patterns commonly adopted by developers
to tackle specific challenges within these frameworks. By
grasping and implementing these design principles, we can
develop well-organized, efficient, and easier-to-maintain
JavaScript applications.
Structure
This chapter will cover the following topics:
• Overview of Vue
• Design Patterns with Vue
• Popular Design Patterns specific for Vue
• Using JavaScript Design Patterns with Vue
Objectives
Upon finishing this chapter, we will be one step closer to
crafting efficient modern JavaScript applications. By the
end of it, we will delve into the optimal utilization of the
built-in patterns within a modern JavaScript framework like
Vue. Furthermore, we will learn how to integrate the
Design Patterns we have acquired thus far to complement
these built-in patterns. Consequently, we will be equipped
to develop efficient enterprise-level JavaScript applications
that are robust, streamlined, and simpler to maintain.
Overview of Vue
As explored in previous chapters, JavaScript has been a
cornerstone of web development since the mid-1990s. The
modern JavaScript revolution started with introduction of
AngularJS by Google in 2010. While offering numerous
advantages, the steep learning curve associated with
AngularJS often deterred users from actively implementing
it in their projects.
Three years later, Facebook (now Meta) introduced React,
followed closely by Vue, a library created by Evan You.
These two libraries quickly revolutionized web development
with their user friendly interfaces, minimal learning curves,
efficient code development, and speedy UI rendering. As of
April 2024, Vue is shortly behind ReactJS with statistics on
popular websites claiming that close to 2 million live
websites use [Link] to empower their UI, highlighting its
widespread adoption and effectiveness.
In this segment, we will briefly discuss Vue, one of the two
preferred libraries used by several top product companies
for application development. We will explore what makes
Vue so popular and the advantages they offer over
traditional JavaScript applications.
Code reusability
Before the advent of front-end libraries, reusing UI
components involved complex inheritance patterns or
writing the same code repeatedly. This made codebases
bulky and updates tedious.
Scalability
As applications grew, managing state and keeping the UI
consistent became a significant challenge. Traditional
JavaScript lacked a structured approach for handling
complex UIs.
These are just a few of the major challenges developers
encountered while coding in traditional JavaScript.
Vue
Shortly after React’s debut, Evan You introduced Vue in
2014. A former Google engineer, You sought to overcome
the constraints of existing frameworks like AngularJS. He
envisioned a lightweight solution that was simpler to learn
and use than the more complex frameworks of the era, one
that could readily adapt to diverse project requirements
and development methodologies.
Earlier, we explored the challenges encountered by
developers before React’s introduction in 2013. While
React addressed these issues, it still presented a slight
learning curve and followed an opinionated approach to
application structure. Vue, inspired by React, emerged as a
more approachable and adaptable alternative.
Vue adopted a more gradual learning curve by offering a
simpler syntax and API compared to React, making it more
accessible for developers familiar with native HTML, CSS,
and JavaScript. Additionally, Vue provides options for data
binding (one-way or two-way) and component
communication, accommodating various development
styles and project needs. Like React, Vue also utilizes the
virtual DOM approach for efficient updates and seamless
UI rendering.
Moreover, Vue popularized the Single File Component
(SFC) approach, allowing all necessary entities—HTML,
CSS, and JavaScript—to be stored in a single file. This
promotes better code organization and maintainability.
The simplicity and effectiveness of React and Vue
resonated with the web development community at large.
Despite their distinct approaches to problem solving, both
resulted in faster and more efficient UI rendering,
improved code structure, and code that is easier to read
and maintain. This proved to be a significant advantage for
enterprise-level applications, leading to swift adoption by
developers.
As of 2024, a majority of the world’s top web applications
are built on these two front-end libraries, underscoring
their widespread acceptance and impact on the industry.
Mixins approach
The mixin approach is easier to comprehend than the
directive approach. We can create a validation mixin that
provides reusable validation methods and error handling
logic. This mixin can be applied to any component that
needs form validation:
1. // Validation mixin
2. const ValidationMixin = {
3. methods: {
4. validate() {
5. // Validation logic
6. if (!isValid) {
7. [Link](errorMessage);
8. }
9. },
10. showError(message) {
11. // Show error message
12. }
13. }
14. };
In the provided code snippet, a mixin for validation is
created, encapsulating methods intended for validation and
error handling. The mixin contains a methods property
housing the validate method for handling validation and
the showError method for managing errors. To utilize this
mixin, it can be imported and added to a component’s
properties:
1. import ValidationMixin from './ValidationMixin';
2.
3. export default {
4. mixins: [ValidationMixin],
5. // Component definition
6. }
7.
Finally, we can use it in the component like this:
1. <input type="text" @blur="validate">
Advantages
Both computed properties and watchers offer the following
advantages in building Vue applications:
• Performance optimization: By offloading expensive
computations from the template and leveraging the
reactivity system of Vue, computed properties
contribute to overall performance optimization. They
ensure that computations are only performed when
necessary, reducing unnecessary overhead.
• Code reusability: Computed properties allow us to
encapsulate common calculations or transformations in
one place, making it easy to reuse them across multiple
components. This promotes code reusability and helps
follow the Don’t Repeat Yourself (DRY) principle.
• Dependency tracking: [Link] automatically tracks
dependencies between computed properties and
reactive data. If any of the dependencies change, Vue
knows to recompute the computed property. This
eliminates the need for manual dependency
management and ensures that your computed
properties always reflect the latest data state.
• Asynchronous operations: Watchers allow us to
perform asynchronous operations in response to
changes in data. This is particularly useful for scenarios
like making HTTP requests, performing animations, or
updating external libraries asynchronously.
• Fine-grained control: Watchers provide fine-grained
control over how you respond to data changes. You can
execute different actions based on the old and new
values of the watched property, enabling you to
implement complex logic tailored to your specific
requirements.
• Dynamic responses: Watchers allow us to
dynamically respond to changes in data by executing
custom logic. This flexibility enables us to implement
dynamic behaviors, such as conditionally triggering
actions or adjusting UI elements based on the observed
data changes.
In summary, computed properties and watchers in [Link]
offer several advantages, including improved performance,
code readability, reusability, and fine-grained control over
data-driven behaviors. They are essential tools for
managing complex data transformations, asynchronous
operations, and dynamic responses in [Link] applications.
Scoped styles
Scoped styles in [Link] provide a mechanism for defining
component-specific styles that are isolated from other
components in the application. This ensures that styles
defined within a component do not inadvertently affect
other parts of the application. To illustrate this concept,
consider a traditional web application where multiple style
files are used to manage CSS changes. However, as the
application grows larger, managing styles across different
components becomes increasingly complex. Additionally, a
seemingly innocuous change to a CSS property, such as h1,
in one file can unintentionally alter the styling of all h1
elements throughout the application. Scoped styles in
[Link] address these challenges by encapsulating styles
within individual components, enhancing modularity and
reducing the risk of unintended styling side effects.
Here is how scoped styles work in Vue:
In [Link], each component typically has its own template,
JavaScript logic, and styles. Scoped styles allow you to
define CSS styles that are only applied to the HTML
elements within that component’s template. When we
define styles for a component in Vue, by default, those
styles are global, meaning they could affect other
components. However, when you add the scoped attribute
to the <style> tag of a component, Vue automatically
scopes those styles so that they only apply to the elements
in that component’s template. Vue achieves scoped styles
by automatically adding a unique identifier to the CSS
selectors within a scoped <style> block. This unique
identifier is typically a data attribute that is added to the
root element of the component’s template. So, when we
define a CSS rule like h1 { color: red; } within a scoped
<style> block, Vue will automatically transform it into
something like [data-v-xxxxxx] h1 { color: red; }, where
xxxxxx is a unique identifier.
1. <template>
2. <div class="example">
3. <h1>Hello, world!</h1>
4. </div>
5. </template>
6.
7. <script>
8. export default {
9. name: 'ExampleComponent'
10. }
11. </script>
12.
13. <style scoped>
14. /* These styles will only apply to the h1 element within
this component */
15. h1 {
16. color: red;
17. }
18. </style>
In this example, the color: red; style will only be applied to
the <h1> element within this component because of the
scoped attribute on the <style> tag. It would not affect
any other <h1> elements in your application.
Advantages
Scoped styles offer the following advantages in building
Vue applications:
• Modularity: Scoped styles promote modularity by
encapsulating styles within individual components.
Each component can have its own styles without
worrying about conflicts with styles from other
components. This makes it easier to reason about and
maintain your styles, especially in larger applications
with many components.
• Isolation: Scoped styles provide isolation for your
components, ensuring that the styles you define would
not unintentionally affect other parts of your
application. This isolation helps prevent style conflicts
and makes it easier to develop and maintain complex
user interfaces.
• Performance: Scoped styles can improve performance
by reducing the amount of CSS that needs to be
processed by the browser. Since scoped styles only
apply to specific components, the browser doesn’t need
to parse and apply styles that are not relevant to the
current component, resulting in faster rendering times.
Overall, scoped styles in [Link] offer a powerful way to
manage and maintain styles in your application, promoting
modularity, isolation, predictability, reusability, and
performance. By encapsulating styles within individual
components, scoped styles help you build more
maintainable and scalable user interfaces.
Singleton Pattern
We can create singleton objects in [Link] using either
global variables or Vue instances. For example, we might
use a Singleton Pattern to manage global application state
or to create a centralized event bus:
1. // Singleton Pattern with a global variable
2. const EventBus = new Vue();
3.
4. // Usage
5. EventBus.$emit('event', data);
As shown in this code, this EventBus variable can be used
anywhere across the application, and it will only have a
single point of reference always.
Module Pattern
Similar to React, Vue allows for the encapsulation of
related functionality into modules using components. Each
Vue component serves as a module with its own
encapsulated data, methods, and computed properties. As
we have seen previously, Vue components are self-
contained entities with their own set of properties, data,
and styling. Despite their self-contained nature, these
components can seamlessly compose together to construct
more complex and feature-rich user interfaces. This
modular approach enhances code organization, reusability,
and maintainability in Vue applications, mirroring the
benefits offered by React components.
Factory Pattern
Like React, we can use factory functions to create Vue
components dynamically based on certain criteria:
1. function createComponent(type) {
2. if (type === 'button') {
3. return [Link]('my-button', {
4. // button component options
5. });
6. } else if (type === 'input') {
7. return [Link]('my-input', {
8. // input component options
9. });
10. }
11. }
12.
13. // Usage
14. const ButtonComponent =
createComponent('button');
As shown in the above code, we have a factory function that
takes in a type and returns a Vue component, serving the
exact purpose of the Factory Design Pattern.
Observer Pattern
[Link] inherently uses the Observer Pattern with its
reactivity system. Data properties in Vue components are
automatically observed, and any changes trigger re-
renders. Additionally, we have seen the usage of watchers
and computed properties that also follow the principles of
Observer Pattern by observing a change and then
triggering a re-render if required.
By leveraging these Design Patterns, we can effectively
structure and manage our [Link] applications, making them
more modular, maintainable, and scalable.
Conclusion
In this chapter, we delved into understanding one of the
two most prominent modern JavaScript frameworks: Vue.
Given that a vast majority of web applications, as of 2024,
are built using Vue, we explored ways to enhance its
efficiency through the application of Design Patterns.
We examined the built-in patterns, such as components and
SFCs, Vuex for state management, custom directives, and
mixins. Subsequently, we explored Vue-specific Design
Patterns like computed properties and watchers, scoped
styles, and dynamic component loading. Finally, we
discussed the integration of traditional JavaScript Design
Patterns with [Link].
In summary, much like its counterpart React, Vue was
designed to offer numerous advantages over the usage of
traditional JavaScript. Its application and implementation
help optimize performance, and aids in streamlining code
and enhancing its readability. Vue has been built in such a
fashion that it inherently adopts Design Patterns that are
on the same lines with established JavaScript principles. By
introducing JavaScript Design Patterns into Vue, we further
enhance the effectiveness of Vue.
We have now read about basic Design Patterns and their
applications in modern UI libraries and frameworks like
React and Vue. In the next chapter, we will be delving into
the practical applications of advanced Design Patterns.
Points to remember
• React and Vue emerged as solutions to address the
challenges encountered by developers when using
traditional JavaScript. By minimizing the need for direct
DOM manipulation, they significantly enhanced code
reusability. Consequently, they facilitated the
development of larger and more intricate web
applications with greater efficiency and scalability.
• Vue adheres to component-based architecture, wherein
each component serves as a standalone entity
comprising its own encapsulated logic, data, and
properties. These components are akin to LEGO blocks,
capable of being interconnected to construct intricate
applications. Their presence inherently embodies
various Design Patterns such as the Modular Pattern,
Singleton Pattern, Strategy Pattern, and Composite
Pattern, among others, thereby promoting code
modularity and reusability.
• Vue leverages custom directives, mixins, computed
properties, watchers, scoped styles, and dynamic
component loading to enhance coding efficiency. These
built-in or Vue-specific Design Patterns enable
developers to craft maintainable and robust Vue code,
further streamlining development workflows and
improving application performance.
• In conclusion, whether through the native capabilities
or through patterns specific to its ecosystem, Vue can
leverage traditional JavaScript Design Patterns to
enhance code development. These patterns serve as
tried and true strategies for optimizing application
architecture, promoting reusability, and improving
overall code quality in both frameworks.
Exercises
1. Which traditional JavaScript Design Pattern is
best emulated by the concept of custom directives
in Vue?
a. Singleton Pattern
b. Strategy Pattern
c. Modular Pattern
d. Decorator Pattern
2. Which of the following statement is not true?
a. The in-built computed properties offered by Vue
components can track changes on certain variables
and trigger re-rendering if required.
b. Redux helps in maintaining the reactivity of
components in Vue.
c. We can implement the Singleton Pattern in Vue by
creating global variables.
d. Scoped styles in Vue suggest that a given set of styles
can only be applicable to a single component without
it affecting any other components in the application.
3. Which of the following use cases best describe the
implementation of the Façade Design Pattern in
Vue?
a. Implementing Vuex to maintain a global state that can
be used by applications to render the UI.
b. Loading components dynamically to avoid importing
them when they are not required.
c. Using computed properties to keep a track of changes
and invoking functions for re-rendering, if required.
d. Creating atomic components to build up a complex
piece of UI.
Answers
1. d
2. b
3. a
CHAPTER 8
Advanced Design Patterns
in JavaScript
Introduction
In this chapter, we explore advanced Design Patterns in
JavaScript, focusing on the Module Pattern, MV (Model-
View) patterns and their variations, and the art of
combining multiple Design Patterns for robust and scalable
solutions. The Module Pattern harnesses closures to create
encapsulated modules, while MV patterns like Model-
View-Controller (MVC), Model-View-Presenter (MVP),
and Model-View-Mediator-Controller (MVMM) offer
structured approaches to organizing code. By mastering
these patterns and their combinations, developers can
architect flexible, maintainable, and efficient JavaScript
applications, ensuring clean separation of concerns and
promoting code reusability.
Structure
This chapter will cover the following topics:
• Introduction to advanced Design Patterns
• Module Pattern
• MV* patterns
• Combining Design Patterns
Objectives
By completing this chapter, we will officially transition into
the expert stage of writing about JavaScript Design
Patterns. By finishing this chapter, we will delve into more
advanced concepts in JavaScript Design Patterns. These
insights will enable us to develop modern enterprise-level
JavaScript applications using contemporary JavaScript
frameworks. We will gain the ability to systematically
organize code with greater clarity and effectiveness.
Module Pattern
In earlier chapters, we have encountered numerous
mentions of this pattern. The inherent functionality of
modern JavaScript frameworks like React and Vue closely
mirrors this Design Pattern. The Module Design Pattern in
JavaScript serves as a method for encapsulating code into
self-contained units known as modules. This approach
encourages modularity and compartmentalization,
enhancing code manageability, comprehension, and
maintenance. Modules can contain private members and
methods that are shielded from external access, exposing
only essential functionality through a public interface. In
this book, we will explore the Revealing Module Pattern,
one of the most important variants of the Module Pattern
that is not dependent on any third-party library.
Implementation
The Revealing Module Design Pattern typically involves
defining an anonymous function that returns an object
containing the methods and properties that we want to
expose publicly. Within this function, we define any private
variables or functions that are only accessible within the
module. Then, we selectively reveal the public members of
the module by attaching them to the returned object:
1. const AuthModule = (() => {
2. // Private variables
3. let loggedIn = false;
4. let username = '';
5.
6. // Private function
7. function login(user) {
8. loggedIn = true;
9. username = user;
10. [Link](`${username} logged in
successfully.`);
11. }
12.
13. function logout() {
14. loggedIn = false;
15. username = '';
16. [Link]('Logged out successfully.');
17. }
18.
19. // Public members (revealed)
20. return {
21. isLoggedIn: () => loggedIn,
22. getUsername: () => username,
23. login,
24. logout
25. };
26. })();
27.
28. // Usage
29. [Link]([Link]()); // Output:
false
30. [Link]('john_doe');
31. [Link]([Link]()); // Output: true
32. [Link]([Link]()); // Output:
"john_doe"
33. [Link]();
34. [Link]([Link]()); // Output:
false
In the provided code example, we have crafted a module
called AuthModule using an Immediately Invoked
Function Expression (IIFE) to encapsulate its
functionality.
An IIFE is a JavaScript function that is executed
immediately after it is defined. It is enclosed within
parentheses and followed by a pair of parentheses for
invocation, like this: (function() { ... })().
IIFEs are commonly used to create isolated scopes and
avoid polluting the global namespace. This is particularly
useful for defining modules, as it enables private variables
and functions to exist within the IIFE, while exposing only
the public interface.
Within the AuthModule, we have defined two private
variables, loggedIn and username, along with two
functions—login and logout—accessible only within the
module. The login function sets the loggedIn variable to
true and assigns the provided value to the private
username variable. Conversely, the logout function resets
these values.
Towards the end of the module, we reveal certain private
values by returning an object containing references to
these variables. This approach allows us to selectively
expose information to the external environment. As
demonstrated in the console statements below, when the
user is not logged in, the module exposes the isLoggedIn
value as false. Subsequently, after attempting to log in
using the module’s login method, the isLoggedIn value
switches to true, and the getUsername method returns
the username supplied during login. Likewise, upon
logging out, the values are reset, and isLoggedIn reverts
to false.
This example demonstrates how the Revealing Module
Pattern can be used to create a modular and reusable
authentication system in JavaScript, enhancing code
organization, encapsulation, and abstraction.
Advantages
The revealing module offers several advantages in making
code more efficient. Let us look at the various advantages
offered by Revealing Module Design Pattern:
• Encapsulation: In the Revealing Module Pattern,
private variables and functions are hidden from the
global scope, thereby preventing accidental
modification and interference.
• Name spacing: Modules help prevent naming conflicts
by encapsulating functionality within a specific scope.
Since every module is defined as a const, we cannot
have more than one module with the same name.
• Abstraction: By revealing only necessary methods and
properties, the complexity of the underlying
implementation is abstracted, promoting a cleaner
interface.
• Reusability: Modules can be easily reused across
different parts of an application or in other projects.
The inherent functionality of components in modern
JavaScript frameworks like React and Vue mirrors this
advantage the most and this feature of reusability
makes these two frameworks so popular amongst front-
end engineers.
• Testability: Encapsulated modules are easier to test
since their internal state and behavior can be isolated
from other parts of the codebase.
Disadvantages
While it offers numerous advantages, there are several
disadvantages as well to using this pattern:
• Increased memory usage: Each instance of a module
creates its own scope, potentially consuming more
memory.
• Difficulties with asynchronous operations:
Handling asynchronous operations within modules
might require additional complexity.
In summary, the Revealing Module Pattern is a useful
technique for structuring JavaScript code, providing
encapsulation, name spacing, abstraction, reusability, and
testability benefits. However, it is essential to consider its
limitations and suitability for specific use cases.
MV* patterns
The MV* Design Patterns in JavaScript represent a
systematic approach to structuring code in web
applications, aiming to improve maintainability, scalability,
and reusability. These patterns facilitate the separation of
concerns by dividing an application into distinct
components, each responsible for a specific aspect of the
application’s functionality.
At its core, MV* pattern separates an application into three
primary components (as you can find from its
abbreviation):
• Model: The Model represents the data and the
business logic of the application. It encapsulates the
application’s state and behavior, providing methods for
data manipulation and validation. In JavaScript, Models
are often implemented as classes or data structures that
manage the application’s data.
• View: The View is responsible for presenting the user
interface to the user. It displays data from the Model
and provides a means for users to interact with the
application. Views in JavaScript are typically
implemented using HTML templates combined with
CSS styles for presentation.
• Controller/ViewModel/Presenter: The Controller
acts as an intermediary between the Model and the
View. It receives input from the user through the View,
processes that input, and updates the Model
accordingly. It also updates the View to reflect changes
in the state of the Model. The specific responsibilities of
the Controller can vary depending on the chosen MV*
variant.
By separating concerns in this way, MV* patterns promote
code organization, making it easier to understand,
maintain, and extend the application over time.
Additionally, these patterns encourage code reusability, as
components can be more easily isolated and tested
independently.
In this section, we will be looking at three variants of the
MV* pattern where we replace the * with the respective
module. We will be looking at the MVC, MVP, and MVMM
pattern.
MVC pattern
The initial variation of the MV* pattern we will examine is
the MVC Design Pattern. In the preceding section, we
thoroughly explored and grasped the definition and
responsibilities of each component within the MV* pattern.
MVC divides an application’s concerns into three primary
components: Model, View, and Controller.
In the MVC pattern, the Model component oversees data
validation, manipulation, and storage independently of the
user interface (View) and user input handling (Controller).
The View component presents data to the user and
captures user interactions (input). Meanwhile, the
Controller manages the application’s flow and business
logic, determining actions based on user input and
updating the Model accordingly. In MVC, the Controller
can be implemented as event handlers or functions that
respond to user input, such as button clicks or form
submissions.
The general flow of the MVC pattern proceeds as follows:
• The user interacts with the View (e.g. clicks a button).
• The View sends a notification (event) to the Controller,
indicating the user’s action.
• The Controller receives the notification, processes it,
and decides what action to take.
• The Controller may interact with the Model to retrieve
or update data based on the user’s action.
• Once the Model is updated, the Controller notifies the
View to update the presentation accordingly.
• The View retrieves the updated data from the Model
and re-renders itself to reflect the changes, completing
the cycle.
Let us understand this with a simple example:
Model:
1. class TodoModel {
2. constructor() {
3. [Link] = [];
4. }
5.
6. addTodo(todo) {
7. [Link](todo);
8. }
9.
10. removeTodo(index) {
11. [Link](index, 1);
12. }
13.
14. getAllTodos() {
15. return [Link];
16. }
17. }
View:
1. class TodoView {
2. constructor() {
3. [Link] = [Link]('todo-
list');
4. }
5.
6. renderTodos(todos) {
7. [Link] = todos
8. .map((todo, index) => `<li>${todo} <button data-
index=»${index}">Remove</button></li>`)
9. .join('');
10. }
11. }
Controller:
1. class TodoController {
2. constructor() {
3. [Link] = new TodoModel();
4. [Link] = new TodoView();
5. [Link]();
6. }
7.
8. bindEvents() {
9. const addButton = [Link]('add-
button');
10. const inputField =
[Link]('todo-input');
11.
12. // Add Todo Event
13. [Link]('click', () => {
14. const todoText = [Link]();
15. if (todoText) {
16. [Link](todoText);
17. [Link]([Link]());
18. [Link] = '';
19. }
20. });
21.
22. // Remove Todo Event (Event Delegation)
23. [Link]('click', (event)
=> {
24. if ([Link] === 'BUTTON') {
25. const index =
parseInt([Link]('data-index'), 10);
26. [Link](index);
27. [Link]([Link]());
28. }
29. });
30. }
31. }
32.
33. // Initialize the application
34. const controller = new TodoController();
Sample HTML structure:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="UTF-8">
5. <meta name="viewport" content="width=device-
width, initial-scale=1.0">
6. <title>Todo MVC Example</title>
7. </head>
8. <body>
9. <div>
10. <input type="text" id="todo-input"
placeholder="Enter a new todo" />
11. <button id="add-button">Add</button>
12. </div>
13. <ul id="todo-list"></ul>
14. <script src="[Link]"></script>
15. </body>
16. </html>
In the provided code, we have employed a straightforward
example of a to-do application, allowing users to add and
remove items from their to-do lists. As illustrated, we have
developed three distinct components:
• Model (ToDoModel): This component stores all the
to-dos and provides functions to manipulate this data. It
is responsible for managing the to-do data
independently.
• View (ToDoView): The View renders the list of to-dos
stored in the ToDoModel. It is responsible for
displaying the to-dos to the user.
• Controller (ToDoController): This component
interacts with both the View and the Model. It exposes
an event listener capable of detecting button clicks and
adding new items to the to-do list.
The renderTodos method is part of the View in the MVC
pattern. It is responsible for updating the DOM to reflect
the current state of the to-do list.
• It takes an array of todos as input.
• It uses the map method to generate an HTML string
for each to-do, including a Remove button with a data-
index attribute to identify the to-do’s index.
• The generated HTML is joined into a single string and
set as the innerHTML of the todoList element.
This ensures that every time the to-do list changes (e.g.,
adding or removing a to-do), the View is updated to reflect
the latest state of the application.
These components collectively facilitate the functionality of
the to-do application, maintaining a clear separation of
concerns and enhancing maintainability and scalability.
Advantages
The MVC pattern offers the following advantages in writing
JavaScript applications:
• Separation of concerns: MVC separates different
aspects of an application, making it easier to maintain
and update code because changes in one component do
not directly affect others.
• Reusability: Each component in MVC can be reused in
different parts of the application or in different
applications altogether, promoting code reuse and
reducing redundancy.
• Testability: Because of its modular structure, each
component in MVC can be tested independently,
allowing for more thorough and efficient testing of the
application.
• Scalability: MVC helps in scaling the application by
organizing code into manageable components, making
it easier to add new features or modify existing ones
without causing significant disruptions.
Disadvantages
There are also a few disadvantages of using the MVC
pattern:
• Complexity: Implementing MVC in smaller
applications might introduce unnecessary complexity. It
is essential to assess the size and requirements of the
project before opting for MVC.
• Potential overhead: In smaller applications, the
overhead of implementing MVC might outweigh its
benefits. MVC is best suited for larger, more complex
applications where the separation of concerns is crucial.
• Learning curve: New developers may initially
struggle to understand the connection and interaction
among the three parts of the MVC pattern.
MVP pattern
The second variation of the MV* pattern that we will
explore is the MVP pattern. As we can see, the primary
difference between the MVC and the MVP patterns is in the
final component. Instead of the Controller in MVC, the MVP
pattern uses a Presenter to complement the Model and the
View, making it the Model-View-Presenter pattern.
MVP is a Design Pattern commonly used in JavaScript
applications, particularly in frameworks like AngularJS.
Much like its counterpart, the MVC pattern, the Model and
View components retain similar responsibilities. The Model
represents the data and business logic of the application. It
manages the application’s data, processes it, and enforces
business rules. The View represents the user interface
(UI) of the application, responsible for displaying data to
the user and capturing input.
The final piece of the puzzle is the Presenter component.
Similar to the Controller in the MVC pattern, the Presenter
acts as an intermediary between the Model and the View.
However, in MVP, the Presenter is specifically responsible
for handling user interactions with the View. It fetches data
from the Model and updates the View accordingly. Like the
Controller, the Presenter communicates directly with both
the View and the Model, ensuring that the View and Model
remain unaware of each other.
Advantages
The MVP pattern offers the following advantages in writing
JavaScript applications:
• Separation of concerns: MVP separates the
application into three distinct components — Model,
View, and Presenter. This separation makes the
codebase easier to understand, maintain, and extend
because each component has a specific responsibility.
• Flexibility and extensibility: Since the View is only
responsible for displaying data and capturing user
input, it can be easily replaced or updated without
affecting the underlying business logic (Presenter) or
data management (Model). This makes the application
more flexible and extensible.
• Testability: MVP facilitates unit testing because of its
clear separation of concerns. The business logic resides
in the Presenter, which can be tested independently of
the View and Model. This allows for easier testing with
mock objects or stubs.
• UI/UX improvements: MVP promotes cleaner and
more maintainable UI code by separating the
presentation logic (handled by the Presenter) from the
UI layout and styling. This separation allows UI/UX
designers and developers to work independently and
iterate on the user interface without affecting the
application’s logic.
• Reduced dependency: In MVP, the View does not
contain any business logic. This reduces dependencies
between the UI and the underlying application logic,
making it easier to change or update either component
without affecting the other.
Disadvantages
There are also a few disadvantages of using the MVC
pattern:
• Complexity: Implementing MVP requires additional
layers of abstraction (especially the Presenter)
compared to other patterns like MVC. This can lead to
increased complexity, especially in smaller projects
where the added structure may not be necessary.
• Potential for inconsistency: Since MVP does not
enforce strict rules on how components interact, there
is potential for inconsistencies in how the pattern is
implemented across different parts of the application or
by different team members. This can lead to
maintenance challenges and codebase inconsistencies
over time.
• Over-engineering: In some cases, especially for
smaller or less complex applications, implementing
MVP may be considered over-engineering. The added
layers of abstraction and separation may not provide
significant benefits compared to simpler patterns like
MVC.
MVMM pattern
The final variation of the MV* pattern is the Model-View-
Mediator-Model (MVMM) Design Pattern. It adds an
additional layer, the Mediator, which helps to decouple the
components further. In JavaScript, it is particularly useful
for building complex user interfaces where you need a
clear separation of concerns and easy maintainability.
Let us break down each component in the MVMM pattern:
• Model: Like the previous MV* patterns, the Model
represents the data and the business logic of the
application. It is responsible for managing the
application’s data, responding to requests for
information, and also updating itself when data changes
occur.
• View: In MVMM, the View is responsible for presenting
the data to the user and handling user interactions. It
listens to changes in the Model and updates the UI
accordingly.
• Mediator: In MVMM, we introduce a new component
called the Mediator. It acts as an intermediary between
the Model and the View. It facilitates communication
between them, ensuring that they remain loosely
coupled. The Mediator listens to user interactions from
the View, updates the Model accordingly, and notifies
the View of any changes in the Model that require UI
updates. It helps to minimize direct dependencies
between the Model and the View, making the
application easier to maintain and extend.
• Mediated Model: In some implementations of MVMM,
there is an additional layer called the Mediated Model.
This is essentially a wrapper around the original Model,
providing an interface that the Mediator can interact
with. It allows the Model to remain decoupled from the
Mediator and simplifies the communication between
them.
Let us quickly dive into an example to understand how the
MVMM Design Pattern is implemented:
1. // Mediated Model
2. class MediatedUserModel {
3. constructor(model) {
4. [Link] = model;
5. }
6.
7. setAge(age) {
8. [Link](age);
9. }
10. }
11.
12. // Model
13. class UserModel {
14. constructor(name) {
15. [Link] = name;
16. [Link] = 0;
17. }
18.
19. setAge(age) {
20. [Link] = age;
21. // Notify the mediator that data has changed
22. [Link]();
23. }
24. }
25.
26. // View
27. class UserView {
28. constructor() {
29. [Link] =
[Link]('nameInput');
30. [Link] =
[Link]('ageDisplay');
31. [Link] =
[Link]('submitButton');
32. }
33.
34. bindSubmit(handler) {
35. [Link]('click', () => {
36. const name = [Link];
37. handler(name);
38. });
39. }
40.
41. displayAge(age) {
42. [Link] = age;
43. }
44. }
45.
46. // Mediator
47. class UserMediator {
48. constructor(model, view) {
49. [Link] = new MediatedUserModel(model);
50. [Link] = view;
51.
52. // Bind view events to mediator methods
53. [Link]([Link](this));
54.
55. // Initial update of view
56. [Link]();
57. }
58.
59. updateName(name) {
60. [Link] = name;
61. // Update the model with new age, triggering a
change
62. [Link]([Link]([Link]() *
100));
63. }
64.
65. onDataChange() {
66. [Link]();
67. }
68.
69. updateView() {
70. [Link]([Link]);
71. }
72. }
73.
74. // Usage
75. const userModel = new UserModel('John');
76. const userView = new UserView();
77. const mediator = new UserMediator(userModel,
userView);
Let us start with the core part — the Model. In this case,
we have created a class called UserModel. The
UserModel class represents the data of a user. It has
properties such as name and age, initialized with default
values. It provides a method setAge to update the age of
the user. When the setAge method is called, it updates the
age property and notifies the Mediator (UserMediator) by
calling [Link]().
Next, we look at the Mediated Model —
MediatedUserModel. It acts as a wrapper around the
original UserModel. It is constructed with an instance of
the UserModel, which it holds as its internal Model
property. The purpose of this wrapper is to provide an
interface for the Mediator (UserMediator) to interact with
the underlying UserModel. It delegates method calls to the
underlying UserModel, effectively mediating the
interaction between the Mediator and the Model.
Next, we look at the View — UserView. The UserView
class represents the UI elements related to the user. It has
references to HTML elements such as nameInput,
ageDisplay, and submitButton, presumably obtained
using getElementById. It provides a method bindSubmit
to bind a submit event handler to the submit button. It
provides a method displayAge to update the age display in
the UI.
Finally, we look at the Mediator (UserMediator). It acts as
an intermediary between the Model and the View. It is
constructed with instances of the Mediated Model
(MediatedUserModel) and the View (UserView). It binds
the submit button of the View to the updateName method
of the Mediator. It listens for changes in the Mediated
Model triggered by user interactions and updates the View
accordingly. It provides methods like updateName,
onDataChange, and updateView to handle interactions
between the Model and the View.
The Mediator (UserMediator) facilitates communication
between the Model and the View, using the Mediated
Model (MediatedUserModel) as an intermediary layer to
decouple the Model from the Mediator.
Advantages
The MVMM pattern offers the following advantages in
writing JavaScript applications:
• Separation of concerns: MVMM promotes a clear
separation of concerns by assigning specific
responsibilities to each component. The Model handles
data and business logic, the View manages UI
presentation, and the Mediator facilitates
communication between them. This separation makes
the codebase more organized and easier to understand.
• Flexibility and extensibility: Because of the loose
coupling between components, it is easier to swap or
update individual components without affecting the
overall functionality of the application.
• Decoupling of components: MVMM helps in
achieving a high level of decoupling between the Model,
View, and Mediator. This makes it easier to maintain
and extend the application as changes in one
component don’t directly impact the others.
• Mediated communication: The Mediator acts as a
central hub for communication between the Model and
View. This centralized communication channel
simplifies complex interactions and reduces the
likelihood of tight coupling between components.
Disadvantages
There are also a few disadvantages of using the MVMM
pattern:
• Complexity: The MVMM pattern introduces an
additional layer of abstraction (the Mediator) which can
add complexity to the architecture. Managing
communication between components through the
Mediator can sometimes make the code harder to
understand, especially for developers unfamiliar with
the pattern.
• Potential for over-engineering: Depending on the
complexity of the application, implementing MVMM
may be overkill. For simpler applications, the overhead
of adding a Mediator layer may not be justified, and a
simpler pattern like MVC or MVP may be more
appropriate.
• Increased codebase size: Introducing an additional
layer like the Mediator can lead to an increase in the
size of the codebase. This may result in more code to
maintain and potentially more opportunities for bugs to
occur.
• Learning curve: MVMM may have a steeper learning
curve compared to simpler patterns like MVC or MVP,
especially for developers who are not familiar with the
concept of a Mediator. Training and documentation may
be necessary to onboard new team members effectively.
Conclusion
In this chapter, we delved into the advanced utilization of
Design Patterns. After examining individual Design
Patterns tailored for specific use cases and their
application in modern JavaScript libraries, we explored
three significant concepts. First, we checked the Module
Pattern and its variant – Revealing Module Design Pattern.
Post that, we delved into the MV* Design Patterns, where
M represents the Model, V represents the View, and the *
symbolizes the various components that can be blended
with these two to craft diverse Design Patterns catering to
advanced needs. We scrutinized three distinct variants of
the MV* patterns — MVC, MVP, and MVMM. Each of these
variants closely mimics the behavior and essence of robust
modern JavaScript libraries. This introduces intricacy to
the code while delivering significantly on reliability,
robustness, and efficiency. Following our exploration of the
MV* patterns, we further examined the technique of
merging different Design Patterns. Real-world applications
frequently confront multiple design concerns
simultaneously, such as separation of concerns, code
reusability, and scalability. By amalgamating different
Design Patterns that address these individual needs, we
tackle multiple problems and optimize performance and
usability. Though there exist numerous permutations and
combinations to merge different Design Patterns, we
focused on the top three combinations capable of resolving
issues commonly encountered by developers.
In the next chapter, we will see how Design Patterns fulfil
the maximum potential for web architects whose primary
job is to ensure a web application has a sound and reliable
structure.
Points to remember
• Design Patterns are crafted to address specific use
cases and tackle particular sets of problems. However,
real-world applications frequently present complex
challenges that are a blend of these individual
problems. To effectively address such complexities, it
becomes imperative to delve into advanced Design
Patterns.
• The Revealing Module Design Pattern is particularly
useful for creating modules with private and public
methods and variables.
• The MV* pattern, short for Model-View-*, maintains the
Model and View components as constants, defining the
data/business logic and the user interface, respectively.
The * denotes the diverse components that can
intertwine with these two to tackle advanced issues.
This MV* pattern draws loose inspiration from the
operational structure of modern JavaScript libraries.
• The MVC pattern adds the Controller to the Model and
View components. The Controller manages the
application’s flow and business logic, determining
actions based on user input and updating the Model
accordingly. The View actively observes the Model and
updates itself.
• In the MVP pattern, the Controller is replaced by the
Presenter component. The Presenter is responsible for
handling user interactions with the View. It fetches data
from the Model and updates the View accordingly.
Unlike MVC, the View is passive in MVP and relies on
the Presenter to update it.
• The MVMM pattern introduces a Mediator and a
Mediated Model. The Mediator listens to user
interactions from the View, updates the Model
accordingly, and notifies the View of any changes in the
Model that require UI updates. The optional Mediated
Model allows the Model to remain decoupled from the
Mediator and simplifies the communication between
them.
• While individual Design Patterns can solve particular
issues, combining multiple Design Patterns can
leverage their strengths to tackle complex design
challenges more effectively.
• Combining Design Patterns can address multiple
concerns by effectively covering both set of problems
that individual Design Patterns can solve. Additionally,
they improve the flexibility and maintainability while
optimizing performance and usability.
Exercises
1. Which of the following Design Patterns utilize a
mediator to listen to user interactions from the
View and then update the Model?
a. MVC pattern
b. MVMM pattern
c. MVP pattern
d. Decorator + Strategy Pattern
2. Which of the following statements is an advantage
provided by combining the Observer and Mediator
Design Patterns?
a. Decoupling components by allowing them to
communicate indirectly through the mediator object,
reducing direct dependencies.
b. Dynamically changing strategies on the fly.
c. Centralized communication channel that simplifies
complex interactions and reduces the likelihood of
tight coupling between components.
3. Which of the following use-cases best describe the
implementation of the MVP Design Pattern?
a. Encapsulating different algorithms and behaviors and
providing flexibility in choosing and modifying them
at runtime.
b. A single point of control for creating various objects,
ensuring both efficiency and manageability.
c. Creating modules with private and public methods
and variables.
d. Allowing UI/UX designers and developers to work
independently and iterate on the user interface
without affecting the application’s logic.
Answers
1. b
2. a
3. d
CHAPTER 9
Design Patterns for Web
Architects
Introduction
The ever-growing complexity of web applications demands
a structured approach to design and development. This
chapter delves into the power of JavaScript Design Patterns
for web architects. We will explore how these established
solutions empower us to make informed decisions that
significantly impact the scalability, availability, and
performance, often denoted by the abbreviation SAP, the
cornerstones of robust web systems. This chapter unveils
the core concepts of web architecture, laying the
foundation for understanding how Design Patterns
influence critical design choices. We will then delve into
the profound impact of Design Patterns on each aspect of
SAP. By mastering these Design Patterns, we will gain the
ability to craft web architectures that are not only
functional but also maintainable, scalable, and performant
in the face of ever-evolving user demands.
Structure
This chapter will cover the following topics:
• Web architecture and web architects
• Role of Design Patterns in designing systems
• Applying Design Patterns to improve scalability
• Applying Design Patterns to improve availability
• Applying Design Patterns to improve performance
Objectives
This chapter will equip us with a deep dive into the world of
web architecture, offering valuable insights from a web
architect’s perspective. We will explore how the Design
Patterns we have encountered can be leveraged to achieve
the three pillars of successful web architecture: SAP.
Here is a breakdown of the impact Design Patterns have on
each aspect of SAP:
• Scalability: We will discover how Design Patterns
empower us to create systems that gracefully handle
growing traffic and data demands.
• Availability: We will explore how Design Patterns
promote fault tolerance and redundancy, ensuring our
applications remain accessible to users.
• Performance: We will gain strategies for optimizing
performance by leveraging established Design Patterns,
leading to a more responsive and efficient user
experience.
By the end of this chapter, we will be well-equipped to craft
web architectures that are not only functional but also
maintainable, scalable, and performant in the face of
evolving user demands.
Web architecture and web architects
Web architecture is the foundation of any successful web
application. It defines the overall structure, behavior, and
interactions within a web system. To illustrate, consider the
analogy of a complex building. An architect meticulously
plans every stage of the layout, determines the logistics
and materials, and defines the functionalities. Similarly, a
web architect must consider every detail of the web
application, including components, technologies, and other
features that contribute to its overall functionality. To
demystify web architecture, we will dissect its core
components and explore the crucial role web architects
play in bringing it all together.
Module Pattern
The Module Pattern encapsulates related functions,
variables, and data into a single unit, providing a public API
while hiding internal implementation. This helps organize
code in a scalable web application, especially for reducing
the complexity in large codebases.
One of the effective uses of the Module Pattern is lazy
loading, where heavy initialization logic is executed only
when required, improving performance by reducing load
time. In the example below, we will demonstrate how lazy
loading can be integrated within the UserService module:
1. const UserService = (function() {
2. // Simulating a heavy initialization logic that is only
executed once
3. let users = [];
4.
5. // Lazy loading - Initialization of the users list happens
only once, on first use
6. function initialize() {
7. if ([Link] === 0) {
8. [Link]("Initializing users...");
9. // Simulated delay for user loading, to showcase
lazy loading
10. users = [{ id: 1, name: 'Alice' }, { id: 2, name:
'Bob' }];
11. }
12. }
13.
14. function addUser(user) {
15. initialize(); // Ensure initialization happens before
use
16. [Link](user);
17. }
18.
19. function getUser(id) {
20. initialize(); // Ensure initialization happens before
use
21. return [Link](user => [Link] === id);
22. }
23.
24. return {
25. addUser: addUser,
26. getUser: getUser
27. };
28. })();
In this code example above tailored for web architects, we
have created a service called UserService. It is responsible
for maintaining a list of users. Inside this service, we have
created methods that can be accessible to developers
outside for adding and fetching users while hiding the
internal implementation. This helps in organizing code into
self-contained modules, making it easier to manage
dependencies, and reducing global scope pollution. This
pattern is particularly useful in large codebases to improve
readability and maintainability.
Singleton Pattern
From a web architect’s perspective, it is crucial to ensure
that there is only one instance of a global configuration or
database services that impacts every part of the
application. By implementing and advocating the Singleton
Pattern, the web architect can ensure that this critical
feature is limited to a single instance, accessible from any
part of the application.
1. const ConfigManager = (function() {
2. let instance;
3.
4. function createInstance() {
5. const config = {
6. apiUrl: '[Link]
7. timeout: 5000
8. };
9. return config;
10. }
11.
12. return {
13. getInstance: function() {
14. if (!instance) {
15. instance = createInstance();
16. }
17. return instance;
18. }
19. };
20. })();
21.
22. // Usage
23. const config1 = [Link]();
24. const config2 = [Link]();
25. [Link](config1 === config2); // true
In the above example, we have utilized the Singleton
Pattern to have a single instance of the configuration that
is applicable for the entire system. This ensures that only
one instance of these critical resources exists, preventing
inconsistencies. This is ideal for managing global
application state or shared resources like configuration
settings, database connections, or logging services.
Observer Pattern
From a web architect’s perspective, the Observer Pattern is
very crucial for event-driven architecture. Observer
Patterns define a one-to-many dependency between objects
so that when one object changes state, all its dependents
are notified and updated automatically.
1. class EventEmitter {
2. constructor() {
3. [Link] = {};
4. }
5.
6. on(event, listener) {
7. if (![Link][event]) {
8. [Link][event] = [];
9. }
10. [Link][event].push(listener);
11. }
12.
13. emit(event, data) {
14. if ([Link][event]) {
15. [Link][event].forEach(listener =>
listener(data));
16. }
17. }
18. }
19.
20. const eventEmitter = new EventEmitter();
21.
22. function logger(data) {
23. [Link]('Logging data:', data);
24. }
25.
26. [Link]('dataReceived', logger);
27. [Link]('dataReceived', { id: 1, message:
'Hello World!' }); // Logging data: { id: 1, message:
'Hello World!' }
For the above code example from a web architect’s
perspective, we have implemented the functionality of an
emitter. This is useful in scenarios where components need
to react to state changes, such as in event-driven
architectures or when implementing reactive UI updates.
This pattern helps in decoupling components and
improving the modularity of the application.
Factory Pattern
The Factory Pattern provides an interface for creating
objects in a superclass while allowing subclasses to modify
the types of objects that will be created. This functionality
is essential for building web applications.
1. class User {
2. constructor(name) {
3. [Link] = name;
4. }
5. }
6.
7. class Admin {
8. constructor(name) {
9. [Link] = name;
10. }
11. }
12.
13. class UserFactory {
14. createUser(type, name) {
15. switch(type) {
16. case 'user':
17. return new User(name);
18. case 'admin':
19. return new Admin(name);
20. default:
21. throw new Error('Unknown user type');
22. }
23. }
24. }
25.
26. const factory = new UserFactory();
27. const regularUser = [Link]('user',
'Alice');
28. const adminUser = [Link]('admin', 'Bob');
29.
30. [Link](regularUser instanceof User); // true
31. [Link](adminUser instanceof Admin); // true
From a web architect’s perspective, it is crucial to define
how users, their roles (e.g., Administrator, Manager,
Employee), and their permissions are generated for the
web application. In the example above, we utilized the
Factory Pattern to provide a fixed and robust method for
handling user creation. This pattern decouples object
creation from implementation, managing object creation
and introducing an abstraction layer to reduce dependency
on specific classes. It is particularly useful for creating
families of related or dependent objects without specifying
their concrete classes.
Decorator Pattern
A web architect’s job extends beyond creating the initial
structure and skeleton of the application; it also involves
designing a system that is future-proof and can
accommodate modifications without breaking down. The
Decorator Pattern, which adds behavior to objects
dynamically, can help the web architect design a future-
proof system.
1. function Order() {
2. [Link] = 100;
3. }
4.
5. function addShipping(order) {
6. [Link] += 10;
7. }
8.
9. function addTax(order) {
10. [Link] *= 1.15;
11. }
12.
13. const myOrder = new Order();
14. addShipping(myOrder);
15. addTax(myOrder);
16.
17. [Link]([Link]); // 126.5
In the above code example, we have denoted a way for the
web architect to ensure that the object that has been
visualized at the start can be modified in the future and the
Decorator Pattern is something that can be utilized to
achieve the same. It allows for adding responsibilities to
objects without altering their structure. It is useful for
scenarios where we need flexible and reusable code, such
as adding features to UI components or managing
middleware in server-side applications.
Strategy Pattern
A web architect must consider that certain applications
need to alter their behavior dynamically. For such cases, it
is crucial to have a family of encapsulated, interchangeable
algorithms. The Strategy Pattern facilitates this
requirement.
1. class Payment {
2. constructor(strategy) {
3. [Link] = strategy;
4. }
5.
6. setStrategy(strategy) {
7. [Link] = strategy;
8. }
9.
10. execute(amount) {
11. return [Link](amount);
12. }
13. }
14.
15. const creditCardPayment = {
16. pay: (amount) => `Paid ${amount} using credit
card`
17. };
18.
19. const paypalPayment = {
20. pay: (amount) => `Paid ${amount} using PayPal`
21. };
22.
23. const payment = new Payment(creditCardPayment);
24. [Link]([Link](100)); // Paid 100 using
credit card
25.
26. [Link](paypalPayment);
27. [Link]([Link](200)); // Paid 200 using
PayPal
In the example above, we describe one of the most common
features required in today’s web applications that double as
SaaS products—payments. Since payment features may
need future modifications to incorporate new technologies,
the Strategy Pattern is ideal for designing a system that
allows for dynamic interchange of algorithms. It facilitates
switching between different algorithms or strategies at
runtime, which is particularly useful for handling various
payment methods uniformly.
Proxy Pattern
Proxy Pattern provides a surrogate or placeholder for
another object to control access to it. This is useful for
managing access to resources.
1. class APIService {
2. fetchData() {
3. return 'Data from API';
4. }
5. }
6.
7. class APIServiceProxy {
8. constructor() {
9. [Link] = new APIService();
10. [Link] = null;
11. }
12.
13. fetchData() {
14. if (![Link]) {
15. [Link] = [Link]();
16. }
17. return [Link];
18. }
19. }
20.
21. const proxy = new APIServiceProxy();
22. [Link]([Link]()); // Data from API
23. [Link]([Link]()); // Data from API
(cached)
In the above example, we have shown the system of a proxy
that can be useful for caching in a data-hungry application.
It is useful for lazy initialization, access control, logging, or
caching. This pattern can help optimize performance by
delaying the loading of heavy objects until necessary or by
adding security layers.
By incorporating these Design Patterns, a web architect
can create a well-structured, efficient, and scalable web
application that can evolve gracefully over time in the
following way:
• Improved code organization and maintainability:
Design Patterns provide a structured approach to
organizing code, making it easier to manage,
understand, and maintain as the codebase grows.
• Reusability: By promoting reusable components and
clear interfaces, Design Patterns reduce redundancy
and the need to rewrite code, enabling faster
development and scalability.
• Decoupling: Many Design Patterns, such as the
Observer and Strategy Patterns, promote decoupling of
components. This makes it easier to change or extend
parts of the system independently without affecting
other parts.
• Performance optimization: Patterns like the proxy
and decorator can help optimize performance by
managing resource usage and adding functionality
dynamically.
• Enhanced collaboration: Clear patterns and
structures make it easier for multiple teams to work
together on different parts of the application,
facilitating parallel development and integration.
Observer Pattern
The Observer Pattern implements an event-driven
architecture to enhance responsiveness and decouple
components. Here is an example demonstrating its usage,
which can help a web architect design a system capable of
handling operations responsively:
1. class Observer {
2. constructor() {
3. [Link] = [];
4. }
5.
6. subscribe(fn) {
7. [Link](fn);
8. }
9.
10. unsubscribe(fn) {
11. [Link] = [Link](subscriber =>
subscriber !== fn);
12. }
13.
14. notify(data) {
15. [Link](subscriber =>
subscriber(data));
16. }
17. }
18.
19. // Usage
20. const userObserver = new Observer();
21.
22. function updateUserInterface(data) {
23. [Link](`User Interface Updated: ${data}`);
24. }
25.
26. function logUserChange(data) {
27. [Link](`User Change Logged: ${data}`);
28. }
29.
30. [Link](updateUserInterface);
31. [Link](logUserChange);
32.
33. // Simulate user data change
34. [Link]('User data changed');
In the above code, we have implemented a notification
system where multiple components need to be updated
when user data changes, enhancing the real-time
capabilities of the application. This enhances
responsiveness by allowing parts of the application to react
to events as they occur, rather than relying on periodic
checks or manual invocations. This leads to more efficient
and timely handling of tasks. Observer Patterns facilitate
decoupling of components, making it easier to manage
dependencies and improve the modularity of the system.
This leads to more robust and maintainable code, which is
crucial for high availability.
Decorator Pattern
The Decorator Pattern allows behavior to be added to
individual objects, without affecting the behavior of other
objects from the same class. It helps in adding
functionalities like logging and caching dynamically
without modifying the core logic. This helps in optimizing
performance and monitoring system health without
affecting core functionalities.
1. function withCaching(fn) {
2. const cache = new Map();
3. return async function(...args) {
4. const key = [Link](args);
5. if ([Link](key)) {
6. [Link]('Returning cached data');
7. return [Link](key);
8. }
9. const result = await fn(...args);
10. [Link](key, result);
11. return result;
12. };
13. }
14.
15. async function fetchData(apiEndpoint) {
16. const response = await fetch(apiEndpoint);
17. return [Link]();
18. }
19.
20. const fetchDataWithCaching =
withCaching(fetchData);
21.
22. // Usage
23. fetchDataWithCaching('[Link]
).then([Link]);
24. fetchDataWithCaching('[Link]
).then([Link]); // Cached
For instance, in the above example, we have created a
caching function by implementing the Decorator Pattern.
By adding caching to a function that fetches data from an
API, we can improve the performance and availability of a
code. The Decorator Pattern provides a flexible way to
extend functionalities, allowing the system to adapt to
changing requirements without major overhauls, thus
ensuring continuous availability.
Proxy Pattern
The Proxy Pattern provides a surrogate or placeholder for
another object to control access to it. This can be useful for
implementing lazy initialization, access control, logging, or
caching. It controls access to critical resources, ensuring
that only authorized requests are processed, which helps in
protecting the system from abuse or overload.
1. class APIService {
2. async fetchData(apiEndpoint) {
3. const response = await fetch(apiEndpoint);
4. if (![Link]) throw new Error('Network
response was not ok');
5. return [Link]();
6. }
7. }
8.
9. class APIServiceProxy {
10. constructor() {
11. [Link] = new APIService();
12. [Link] = 3;
13. }
14.
15. async fetchData(apiEndpoint) {
16. let attempts = 0;
17. while (attempts < [Link]) {
18. try {
19. return await
[Link](apiEndpoint);
20. } catch (error) {
21. attempts += 1;
22. [Link](`Attempt ${attempts} failed:
${[Link]}`);
23. if (attempts >= [Link]) {
24. return { error: 'Service unavailable, please try
again later.' };
25. }
26. }
27. }
28. }
29. }
30.
31. // Usage
32. const apiServiceProxy = new APIServiceProxy();
33. [Link]('[Link]
ata').then([Link]);
For instance, in the above code, we have used a proxy to
add retry logic and fallback response to an API request,
improving fault tolerance and thereby, the availability of
the system. Proxy Design Pattern implements lazy
initialization and caching, reducing the load on backend
systems and improving response times. This is particularly
useful for high-demand services where performance
directly impacts availability.
Factory Pattern
The Factory Pattern provides a way to create objects
without specifying the exact class of the object that will be
created. It offers a level of abstraction that separates object
creation from business logic, making the system easier to
maintain and extend. This pattern is critical for ensuring
long-term flexibility and scalability, especially when new
types of objects need to be integrated into the system.
In this example, the NotificationFactory is responsible for
creating different types of notifications (e.g., email, SMS)
based on the input type. This approach encapsulates the
object creation process, making it easier to introduce
additional notification types without altering existing code:
1. class EmailNotification {
2. send(message) {
3. [Link](`Sending email: ${message}`);
4. }
5. }
6.
7. class SMSNotification {
8. send(message) {
9. [Link](`Sending SMS: ${message}`);
10. }
11. }
12.
13. // Factory class responsible for creating notification
objects
14. class NotificationFactory {
15. // Factory method to create notifications based on
the type provided
16. createNotification(type) {
17. switch (type) {
18. case 'email':
19. return new EmailNotification();
20. case 'sms':
21. return new SMSNotification();
22. // Extensibility: Adding a new notification type
is as simple as adding a new case
23. case 'push':
24. return new PushNotification(); // Example of
adding a new type (Push Notification)
25. default:
26. throw new Error('Invalid notification type');
27. }
28. }
29. }
30.
31. // New type of notification that can be added easily
32. class PushNotification {
33. send(message) {
34. [Link](`Sending push notification:
${message}`);
35. }
36. }
37.
38. // Usage
39. const factory = new NotificationFactory();
40. const emailNotification =
[Link]('email');
41. const smsNotification =
[Link]('sms');
42. const pushNotification =
[Link]('push'); // New type added
43.
44. [Link]('Hello via Email!');
45. [Link]('Hello via SMS!');
46. [Link]('Hello via Push Notification!');
In this case, we have used the Factory Design Pattern to
manage object creation for different types of notifications
(email, SMS) to ensure that the correct type is used and it
is easily extendable. It manages the creation of objects in a
centralized manner, ensuring that the appropriate
resources are allocated efficiently.
By implementing these Design Patterns, a web architect
can create a system that is more resilient, scalable, and
responsive. These patterns help in:
• Efficient resource utilization: Ensuring that
resources are used optimally and shared effectively
across the application.
• Modularity and maintainability: Keeping the system
modular and maintainable, which reduces downtime
and facilitates easier updates.
• Fault tolerance: Incorporating mechanisms to handle
failures gracefully, ensuring that the system remains
available even when some components fail.
• Performance optimization: Enhancing performance
through techniques like caching, lazy loading, and load
shedding, which directly contribute to higher
availability.
Incorporating these patterns into the design and
architecture of a web application leads to a more robust
and highly available system, capable of handling high
traffic and resilient to failures.
Module Pattern
The Module Pattern is used to create encapsulated modules
of code. This pattern helps in organizing and grouping
related code, which improves readability and
maintainability. By encapsulating related functionality
within a module, we reduce the risk of naming collisions
and global variable pollution, leading to more efficient code
execution and easier debugging.
1. const HeavyModule = (function() {
2. let module;
3. return function() {
4. if (!module) {
5. // Perform heavy initialization only when needed
6. module = { /* heavy initialization */ };
7. }
8. return module;
9. };
10. })();
11.
Lazy loading is a common performance optimization
technique in web development, especially for large
applications where loading all modules upfront can lead to
longer initial load times. By using the Module Design
Pattern, modules can be loaded only when needed,
reducing the initial load time and improving the perceived
performance of the application. Modules can be loaded on
demand, reducing initial load times and improving page
performance.
Singleton Pattern
The Singleton Pattern ensures that a class has only one
instance and provides a global point of access to it. This is
useful for managing shared resources or configurations.
This can be particularly useful for managing global state or
configuration settings without redundant instances
consuming memory.
1. const Config = (function() {
2. let instance;
3. return {
4. getInstance: function() {
5. if (!instance) {
6. instance = { /* fetch or compute config */ };
7. }
8. return instance;
9. }
10. };
11. })();
Singleton objects can be used to store shared configuration
settings, avoiding the need to re-fetch or re-calculate them
multiple times. Expensive initialization operations are
performed only once, which can improve startup
performance.
Observer Pattern
The Observer Pattern allows an object (the subject) to
notify other objects (observers) when its state changes.
This is particularly useful for implementing event-driven
systems.
1. class Store {
2. constructor() {
3. [Link] = {};
4. [Link] = [];
5. }
6.
7. subscribe(observer) {
8. [Link](observer);
9. }
10.
11. setState(newState) {
12. [Link] = { ...[Link], ...newState };
13. [Link]();
14. }
15.
16. notify() {
17. [Link](observer =>
[Link]([Link]));
18. }
19. }
20.
We can use observers to update UI components only when
necessary, avoiding unnecessary DOM manipulations and
improving UI responsiveness. Usage of the Observer
Design Pattern allows for efficient event handling and state
management, reducing the need for continuous polling or
redundant checks. This can lead to improved
responsiveness and lower CPU usage. It promotes a
decoupled architecture where components can subscribe to
and react to events without needing direct references to
each other, enhancing modularity and ease of maintenance.
Factory Pattern
The Factory Pattern defines an interface for creating
objects but allows subclasses to alter the type of objects
that will be created. This pattern helps in creating objects
without exposing the creation logic. It streamlines the
creation of complex objects, potentially reusing existing
instances or configurations, which can reduce the overhead
of object instantiation.
1. class ObjectPool {
2. constructor(createFn) {
3. [Link] = createFn;
4. [Link] = [];
5. }
6.
7. get() {
8. return [Link] ? [Link]() :
[Link]();
9. }
10.
11. release(obj) {
12. [Link](obj);
13. }
14. }
15.
With the help of the Factory design patter, we can
implement object pooling to reuse objects instead of
creating and destroying them frequently, which can reduce
garbage collection overhead and improve performance. The
Factory Design Pattern allows for more flexible and
optimized object creation strategies, such as caching
frequently used objects, which can improve performance in
resource-intensive applications.
Decorator Pattern
The Decorator Pattern allows behavior to be added to
individual objects, either statically or dynamically, without
affecting the behavior of other objects from the same class.
1. function cacheDecorator(fn) {
2. const cache = new Map();
3. return function(...args) {
4. const key = [Link](args);
5. if ([Link](key)) {
6. return [Link](key);
7. }
8. const result = fn(...args);
9. [Link](key, result);
10. return result;
11. };
12. }
We can use decorators to add caching behavior to
functions, reducing the need for repetitive and expensive
operations. This Design Pattern allows for the dynamic
addition of responsibilities to objects without altering their
code, enabling performance enhancements (e.g., adding
caching or logging) without impacting the core logic. It
reduces the need for code duplication by enabling reusable
decorators, leading to leaner and more maintainable
codebases.
Proxy Pattern
The Proxy Pattern provides a surrogate or placeholder for
another object to control access to it. This is useful for lazy
initialization, logging, access control, and other operations.
Proxies can control access to objects, providing a way to
implement caching, logging, and other performance-related
features without modifying the original object.
1. class ImageProxy {
2. constructor(filename) {
3. [Link] = filename;
4. [Link] = null;
5. }
6.
7. display() {
8. if (![Link]) {
9. [Link] = new RealImage([Link]);
10. }
11. [Link]();
12. }
13. }
We can use proxies to delay the creation of heavyweight
objects until they are absolutely necessary, improving
initial load times. This can significantly improve application
startup times and reduce initial memory usage.
By leveraging these Design Patterns, web architects can
create applications that are not only more efficient but also
easier to maintain and scale. These patterns help optimize
resource usage, reduce unnecessary operations, and
ensure that the application remains responsive under
varying loads.
Conclusion
In this chapter, we delve into the pivotal role of a web
architect in the software development process, exploring
how Design Patterns in JavaScript serve as invaluable tools
to achieve their objectives. A web architect shoulders the
responsibility of crafting the blueprint for a web
application, laying its foundational pillars crucial for its
longevity and functionality. From handpicking technologies
to structuring the architecture with an eye on
maintainability and future adaptability, the web architect’s
decisions profoundly shape the project’s trajectory.
At the heart of this endeavor lies Design Patterns, offering
systematic approaches to common development challenges.
They empower web architects to construct systems that
embody the trifecta of scalability, availability, and optimal
performance—cornerstones of web architecture. Patterns
such as Factory, Module, Proxy, and Decorator, among
others, serve as building blocks for crafting resilient, high-
performance applications. By embracing these patterns,
web architects ensure that their creations evolve gracefully
over time, seamlessly transforming into highly available
systems capable of withstanding heavy traffic and resilient
to failures. Moreover, these patterns optimize resource
utilization, streamline operations, and uphold
responsiveness under varying loads, thus safeguarding the
user experience.
In the next chapter, we will look at the topic of testing and
Design Patterns. We will deep dive into the utilization of
Design Patterns to enhance the effectiveness,
maintainability, and organization of unit tests.
Points to remember
• Web architecture, true to its name, embodies both
artistry and scientific precision in crafting efficient web
applications. It operates on the fundamental principle of
SAP, alongside other guiding principles. By prioritizing
these pillars, web architecture lays the groundwork for
applications that are not only scalable and future-proof
but also resilient under heavy traffic, highly available,
and efficient in resource utilization.
• The role of a web architect demands meticulous
planning, entailing a thorough analysis of project
requirements, user expectations, and performance
objectives. It involves translating these insights into a
cohesive architectural blueprint, detailing the
components, technologies, and communication
protocols essential for the application’s functionality.
Anticipating future needs, web architects craft
architectures that prioritize maintainability and align
seamlessly with the SAP principle—ensuring scalability,
availability, and performance remain at the forefront of
their design considerations.
• The Module Design Pattern serves as a valuable
framework for organizing and consolidating related
code, optimizing application scalability and
performance through streamlined execution and
simplified debugging processes.
• The Singleton Design Pattern guarantees a single
global instance of a shared resource, accessible
throughout the web application. This aids web
architects in enhancing scalability, availability, and
performance by promoting resource isolation and reuse.
• The Decorator Pattern allows dynamic addition of
behavior to objects without altering their structure. It
improves availability through features like logging and
caching, ensuring efficient resource usage and system
monitoring. Performance is optimized by avoiding
extensive subclassing, allowing runtime behavior
addition which keeps memory and processing efficient,
leading to a more responsive and maintainable web
application.
• The Factory Pattern improves scalability by
centralizing object creation, making it easy to add new
types. Availability is enhanced through object pooling,
reducing downtime and latency. This pattern ensures
consistent and controlled instance creation, promoting
clean, maintainable, and reliable code.
• The Proxy Pattern improves availability with access
control, caching, and lazy initialization, reducing load
on critical resources. This pattern adds abstraction for
new functionalities like security checks, leading to a
flexible, robust, and responsive web application
architecture.
• The Observer Pattern enhances scalability by allowing
multiple objects to listen and react to changes in a
subject, facilitating easy addition of new observers
without altering the subject. It improves availability by
ensuring that updates are propagated immediately,
keeping all parts of the system in sync.
Exercises
1. Which of the following features describe the
application of a Decorator Pattern from a web
architecture’s perspective?
a. Pooling objects
b. Propagating updates systematically, as required
c. Organizing related code into compartments
d. Caching
2. Dynamic interchange of algorithms during
runtime helps the system in maintaining high
availability while also promoting scalability. Which
Design Pattern from the list below best describes
this behavior?
a. Decorator
b. Revealing Module
c. Proxy
d. Strategy
3. Which of the following tasks are not a part of a
web architect’s profile?
a. Creating simplified and intuitive designs that improve
the experience of the user
b. Selecting technologies that are future-proof and can
serve the exact needs of the application
c. Analyzing project requirements and user expectations
d. Build the foundation of a web application by
implementing a system that is scalable, highly
available and performing at peak levels
Answers
1. d
2. d
3. a
Introduction
Testing is often understated and undervalued, yet it is a
critical component of software development.
Comprehensive testing can identify existing bugs that could
disrupt the customer experience, thereby helping retain
customers by maintaining quality and improving reliability
and maintainability. Design Patterns are established
solutions to common software design and development
problems. When applied to testing, these patterns can
streamline the test development process, promote code
reuse, and enhance test readability and organization.
Leveraging Design Patterns enables developers to create
tests that are effective in identifying bugs and resilient to
changes in the codebase.
In this chapter, we explore key Design Patterns in
JavaScript, demonstrate how they can be applied to various
testing scenarios, and discuss their roles in creating
structured and maintainable test suites.
Structure
This chapter will cover the following topics:
• Overview
• Writing unit tests for Design Patterns
• Planning unit tests with Design Patterns
Objectives
By the end of this chapter, we will be able to apply
JavaScript Design Patterns to the concept of unit testing in
JavaScript. By incorporating Design Pattern principles into
our unit tests, we can create test cases that effectively
identify bugs in the codebase. Moreover, utilizing the
reliability of Design Patterns will help us write test cases
that are resilient to changes in the codebase, making them
more future-proof. This approach will elevate the quality of
our test cases, ultimately resulting in a better codebase and
an improved user experience. Since this chapter focuses on
how Design Patterns can be effectively tested in a
JavaScript-powered web application and how they can
shape the application, we expect readers to have a basic
understanding of Jest, one of the most popular testing
frameworks. We will not cover the basics of these
frameworks.
Overview
To understand this chapter better, it is imperative for us to
be clear about what testing in JavaScript signifies, the
types of testing and how they are implemented, and the
potential role of Design Patterns in improving the testing
process. We will begin by revisiting the basic concepts and
ideology of testing in JavaScript to set the foundation for
this chapter’s core exploration: integrating Design Patterns
with testing.
Testing
While testing is a broad concept, we will focus specifically
on testing in JavaScript for this chapter. Testing in
JavaScript involves writing and executing code to verify
that JavaScript applications function correctly and meet
specified requirements. This process includes creating tests
that simulate various scenarios and inputs to ensure the
application behaves as expected under different conditions.
Design Patterns play a critical role in making this process
more efficient. By providing predictable structures and
well-defined interactions, Design Patterns make it easier to
isolate and test individual units of code. This predictability
simplifies test creation, reduces the chances of errors, and
improves the maintainability of both the tests and the
application itself.
Types of testing
There are several techniques and methods of testing, but
for a quality analyst (QA) and a web developer testing
JavaScript code, the three standard categories are as
follows:
• Unit testing: Unit testing focuses on testing individual
components or functions in isolation to ensure they
work as intended. Popular frameworks for unit testing
in JavaScript include Jest, Mocha, and Jasmine.
• Integration testing: It tests the interactions between
different modules or components to verify that they
work together correctly. Tools like Enzyme and Testing
Library are commonly used for integration testing in
JavaScript.
• End-to-end testing: It simulates real user interactions
with the application, testing the entire workflow from
start to finish. Frameworks such as Cypress and
Selenium are widely used for E2E testing in JavaScript.
In this chapter, we will focus exclusively on unit testing, as
it is entirely developer-centric and within the scope of our
book. We will be utilizing the Jest framework for testing
JavaScript code in this chapter.
Singleton Pattern
The Singleton Pattern restricts the instantiation of a class
to a single object. This is useful in scenarios requiring a
single point of control, such as a configuration manager or
a logging service. For example, consider a global
configuration manager for an application. It is responsible
for setting up constants and service configurations needed
throughout the application, such as the base URL for API
requests, user theme settings, and user information. Let us
look at an example demonstrating this:
1. class ConfigManager {
2. constructor() {
3. if (![Link]) {
4. [Link] = {};
5. [Link] = this;
6. }
7. return [Link];
8. }
9.
10. set(key, value) {
11. [Link][key] = value;
12. }
13.
14. get(key) {
15. return [Link][key];
16. }
17. }
18.
19. const instance = new ConfigManager();
20. [Link](instance);
21. export default instance;
22.
Using a mocking framework like Jest, we can test the
Singleton Pattern by ensuring that the instance is unique:
1. import ConfigManager from './ConfigManager';
2.
3. test('ConfigManager instance should be unique', () =>
{
4. const instance1 = ConfigManager;
5. const instance2 = ConfigManager;
6. expect(instance1).toBe(instance2);
7. });
8.
9. test('ConfigManager should store and retrieve values',
() => {
10. const config = ConfigManager;
11. [Link]('appName', 'MyApp');
12. expect([Link]('appName')).toBe('MyApp');
13. });
In the above example, we have imported our class into a
separate test file, typically saved with the .[Link] extension
based on the application’s configuration. This test file
includes two scenarios.
In the first scenario, we create an instance of our
Configuration Manager and store it in instance1. We then
assign the same instance to instance2. According to our
test case, the expression
expect(instance1).toBe(instance2) should return true,
verifying that the two instances are the same.
In the second test case, we verify that the ConfigManager
correctly stores and retrieves values. Using the set method
defined in the class, we set a property appName with the
value MyApp. The test case should then successfully fetch
this value with [Link]('appName').
Factory pattern
The Factory pattern provides a way to create objects
without specifying the exact class of object that will be
created. This pattern is particularly useful for creating
different types of objects based on specific conditions. Let
us consider an application that needs to create different
types of notifications (email, SMS, push).
1. class EmailNotification {
2. send(message) {
3. return `Email sent: ${message}`;
4. }
5. }
6.
7. class SMSNotification {
8. send(message) {
9. return `SMS sent: ${message}`;
10. }
11. }
12.
13. class PushNotification {
14. send(message) {
15. return `Push notification sent: ${message}`;
16. }
17. }
18.
19. class NotificationFactory {
20. static createNotification(type) {
21. switch(type) {
22. case 'email':
23. return new EmailNotification();
24. case 'sms':
25. return new SMSNotification();
26. case 'push':
27. return new PushNotification();
28. default:
29. throw new Error('Invalid notification type');
30. }
31. }
32. }
33.
34. export default NotificationFactory;
In the above example, we have implemented a classic use
of the Factory Design Pattern in JavaScript. We created a
NotificationFactory that determines what kind of
notification should be generated based on the type received
during instantiation. This is how we write a unit test for a
Factory Design Pattern.
1. import NotificationFactory from './NotificationFactory';
2. import EmailNotification from './EmailNotification';
3. import SMSNotification from './SMSNotification';
4. import PushNotification from './PushNotification';
5.
6. test('Factory should create an email notification
object', () => {
7. const notification =
[Link]('email');
8.
expect(notification).toBeInstanceOf(EmailNotification);
9. expect([Link]('Hello')).toBe('Email sent:
Hello');
10. });
11.
12. test('Factory should create an SMS notification
object', () => {
13. const notification =
[Link]('sms');
14.
expect(notification).toBeInstanceOf(SMSNotification);
15. expect([Link]('Hello')).toBe('SMS sent:
Hello');
16. });
17.
18. test('Factory should create a push notification object',
() => {
19. const notification =
[Link]('push');
20.
expect(notification).toBeInstanceOf(PushNotification);
21. expect([Link]('Hello')).toBe('Push
notification sent: Hello');
22. });
In the above code, we created three test cases, each
targeting a different instance that can be created by the
factory. A similar pattern is followed for all three Design
Patterns: first, we create an instance by passing a type to
the NotificationFactory. We then verify that the created
instance is of the type passed as the argument. Finally, we
check if the correct type of notification is generated.
Observer pattern
The Observer pattern allows an object, called the subject,
to maintain a list of its dependents, called observers, and
notify them of state changes. Let’s look at an example
denoting this Observer Design Pattern. We will consider a
newsletter subscription system where users can subscribe
to receive updates.
1. class Newsletter {
2. constructor() {
3. [Link] = [];
4. }
5.
6. subscribe(subscriber) {
7. [Link](subscriber);
8. }
9.
10. unsubscribe(subscriber) {
11. [Link] = [Link](sub =>
sub !== subscriber);
12. }
13.
14. notify(message) {
15. [Link](subscriber =>
[Link](message));
16. }
17. }
18.
19. class Subscriber {
20. constructor(name) {
21. [Link] = name;
22. }
23.
24. update(message) {
25. return `${[Link]} received: ${message}`;
26. }
27. }
28.
29. export { Newsletter, Subscriber };
In this code example, we created two components: a
Newsletter that maintains a list of subscribers and informs
them of all changes, and a Subscriber class that
subscribes to the Newsletter and receives updates from it.
Let us see how we can write a test case for this.
1. import { Newsletter, Subscriber } from './Newsletter';
2.
3. test('Subscribers should be notified of new messages',
() => {
4. const newsletter = new Newsletter();
5. const subscriber1 = new Subscriber('Alice');
6. const subscriber2 = new Subscriber('Bob');
7.
8. [Link](subscriber1, 'update');
9. [Link](subscriber2, 'update');
10.
11. [Link](subscriber1);
12. [Link](subscriber2);
13.
14. [Link]('New Edition Available');
15.
16.
expect([Link]).toHaveBeenCalledWith('Ne
w Edition Available');
17.
expect([Link]).toHaveBeenCalledWith('Ne
w Edition Available');
18. });
In the above code, we used Jest’s spyOn method to monitor
the update method of the two subscriber instances,
subscriber1 and subscriber2. Next, we use the
subscribe method of the Newsletter to add these
subscriber instances to its list of subscribers. We then call
the notify method of the Newsletter to trigger an update
to all its subscribers. Finally, we verify that each
subscriber’s update method was called with the message
passed in the notify method of the Newsletter.
Strategy Pattern
The Strategy Pattern allows a family of algorithms to be
defined, encapsulated, and made interchangeable. This
pattern is useful for defining a suite of algorithms and
selecting one to use at runtime. Let us consider the
example of an application that can handle multiple payment
methods to implement a Strategy Pattern:
1. class CreditCardPayment {
2. process(amount) {
3. return `Processing credit card payment of
${amount}`;
4. }
5. }
6.
7. class PayPalPayment {
8. process(amount) {
9. return `Processing PayPal payment of ${amount}`;
10. }
11. }
12.
13. class BitcoinPayment {
14. process(amount) {
15. return `Processing Bitcoin payment of
${amount}`;
16. }
17. }
18.
19. class PaymentContext {
20. constructor(strategy) {
21. [Link] = strategy;
22. }
23.
24. executeStrategy(amount) {
25. return [Link](amount);
26. }
27. }
28.
29. export { PaymentContext, CreditCardPayment,
PayPalPayment, BitcoinPayment };
In the above example, we have defined a product that
supports three payment categories: Credit Card, PayPal,
and Bitcoin. In the main class, we implemented a method
that allows users to execute and modify a payment strategy
at runtime. Now, let us write unit test cases for this Design
Pattern:
1. import { PaymentContext, CreditCardPayment,
PayPalPayment, BitcoinPayment } from
'./PaymentStrategy';
2.
3. test('Credit card payment strategy should process
payment', () => {
4. const strategy = new CreditCardPayment();
5. const context = new PaymentContext(strategy);
6. const result = [Link](100);
7. expect(result).toBe('Processing credit card payment
of 100');
8. });
9.
10. test('PayPal payment strategy should process
payment', () => {
11. const strategy = new PayPalPayment();
12. const context = new PaymentContext(strategy);
13. const result = [Link](100);
14. expect(result).toBe('Processing PayPal payment of
100');
15. });
16.
17. test('Bitcoin payment strategy should process
payment', () => {
18. const strategy = new BitcoinPayment();
19. const context = new PaymentContext(strategy);
20. const result = [Link](100);
21. expect(result).toBe('Processing Bitcoin payment of
100');
22. });
23.
To write unit test cases for Strategy Design Pattern, we test
all three payment strategies one by one. We initialize the
payment context with the instance of the payment we
require, and then check if the executeStrategy method
returns the correct payment method.
Decorator Pattern
The Decorator Pattern allows behavior to be added to an
individual object, dynamically, without affecting the
behavior of other objects from the same class.
This flexibility makes it ideal for scenarios where
enhancements or modifications are needed for specific
objects without altering the base implementation.
To demonstrate this, we consider a coffee-ordering system
where users can customize their coffee by adding
ingredients like milk or sugar. Each addition dynamically
updates the final price.
1. class Coffee {
2. cost() {
3. return 5;
4. }
5. }
6.
7. class MilkDecorator {
8. constructor(coffee) {
9. // Validate that the coffee object is properly
initialized
10. if (!coffee || typeof [Link] !== 'function') {
11. throw new Error('Invalid coffee object
provided to MilkDecorator.');
12. }
13. [Link] = coffee;
14. }
15.
16. cost() {
17. return [Link]() + 1; // Adds the cost of
milk
18. }
19. }
20.
21. class SugarDecorator {
22. constructor(coffee) {
23. // Validate that the coffee object is properly
initialized
24. if (!coffee || typeof [Link] !== 'function') {
25. throw new Error('Invalid coffee object
provided to SugarDecorator.');
26. }
27. [Link] = coffee;
28. }
29.
30. cost() {
31. return [Link]() + 0.5; // Adds the cost of
sugar
32. }
33. }
34.
35. export { Coffee, MilkDecorator, SugarDecorator };
In the above code, we implemented the Decorator Pattern
by creating a base class Coffee and allowing users to add a
MilkDecorator for milk and a SugarDecorator for sugar.
Each decorator modifies the final price accordingly. Now,
let’s write unit test cases for a Decorator Design Pattern:
1. import { Coffee, MilkDecorator, SugarDecorator } from
'./CoffeeDecorator';
2.
3. test('Cost of coffee should be 5', () => {
4. const coffee = new Coffee();
5. expect([Link]()).toBe(5);
6. });
7.
8. test('Cost of coffee with milk should be 6', () => {
9. const coffee = new MilkDecorator(new Coffee());
10. expect([Link]()).toBe(6);
11. });
12.
13. test('Cost of coffee with milk and sugar should be 6.5',
() => {
14. const coffee = new SugarDecorator(new
MilkDecorator(new Coffee()));
15. expect([Link]()).toBe(6.5);
16. });
To test Decorator Design Pattern, we check for the most
important differentiation for this pattern – the final impact
on the object. In this case, the differentiating factor will be
the cost of the coffee on addition of the decorators. As we
can see in the code above, we test a few permutations of
the decorators by verifying the price of only coffee, coffee
with milk, and coffee with milk and sugar.
Adapter Pattern
The Adapter Pattern allows incompatible interfaces to work
together. This pattern involves a single class, called an
adapter, which is responsible for joining functionalities of
independent or incompatible interfaces. Let us consider a
scenario where we have an old logging system and a new
logging system with different interfaces.
1. class OldLogger {
2. log(message) {
3. return `Old Logger: ${message}`;
4. }
5. }
6.
7. class NewLogger {
8. writeLog(message) {
9. return `New Logger: ${message}`;
10. }
11. }
12.
13. class LoggerAdapter {
14. constructor(logger) {
15. [Link] = logger;
16. }
17.
18. log(message) {
19. return [Link](message);
20. }
21. }
22.
23. export { OldLogger, NewLogger, LoggerAdapter };
24.
In this example, we can see the stark differences between
the two logging systems – OldLogger and NewLogger. By
using the LoggerAdapter, we can seamlessly integrate the
old with the new. Let’s write unit test cases for it:
1. import { OldLogger, NewLogger, LoggerAdapter } from
'./LoggerAdapter';
2.
3. test('OldLogger should log messages correctly', () => {
4. const oldLogger = new OldLogger();
5. expect([Link]('Test message')).toBe('Old
Logger: Test message');
6. });
7.
8. test('LoggerAdapter should adapt NewLogger to
OldLogger interface', () => {
9. const newLogger = new NewLogger();
10. const adapter = new LoggerAdapter(newLogger);
11. expect([Link]('Test message')).toBe('New
Logger: Test message');
12. });
For the Adapter Design Pattern, it’s critical to ensure that
both the older system and the newer system function
seamlessly without issues. Therefore, we test both the older
and the newer system. To verify the newer system, we use
the LoggerAdapter to test if it can log using the old
logger’s implementation method, confirming that the
Design Pattern is working correctly.
Builder Pattern
The Builder Pattern is used to construct a complex object
step by step. It allows you to create different types and
representations of an object using the same construction
process. Let us consider a scenario where we want to build
a complex meal with multiple components.
1. class Meal {
2. constructor() {
3. [Link] = [];
4. }
5.
6. addPart(part) {
7. [Link](part);
8. }
9.
10. showMeal() {
11. return `Meal contains: ${[Link](', ')}`;
12. }
13. }
14.
15. class MealBuilder {
16. constructor() {
17. [Link] = new Meal();
18. }
19.
20. addBurger() {
21. [Link]('Burger');
22. return this;
23. }
24.
25. addDrink() {
26. [Link]('Drink');
27. return this;
28. }
29.
30. addFries() {
31. [Link]('Fries');
32. return this;
33. }
34.
35. build() {
36. return [Link];
37. }
38. }
39.
40. export { Meal, MealBuilder };
In the above example, we created a base class Meal and a
MealBuilder class that allows the base meal to be built
piece-by-piece. By segregating each step, we make the
code configurable and easier to debug. Now, let’s write unit
test cases for this:
1. import { MealBuilder } from './MealBuilder';
2.
3. test('MealBuilder should create a meal with a burger,
drink, and fries', () => {
4. const builder = new MealBuilder();
5. const meal =
[Link]().addDrink().addFries().build();
6. expect([Link]()).toBe('Meal contains:
Burger, Drink, Fries');
7. });
8.
9. test('MealBuilder should create a meal with only a
burger and drink', () => {
10. const builder = new MealBuilder();
11. const meal =
[Link]().addDrink().build();
12. expect([Link]()).toBe('Meal contains:
Burger, Drink');
13. });
Testing the Builder Pattern involves verifying that the
components are added correctly, and the final product is as
expected. In this example, we check multiple combinations
of the meal by adding different ingredients. We verify the
final meal created by utilizing the showMeal method of the
base class.
Prototype Pattern
The Prototype Pattern is used to create new objects by
copying an existing object, known as the prototype. This
pattern is useful when the cost of creating a new object is
high, and it is more efficient to copy an existing object. Let
us consider a scenario where we need to create multiple
copies of a user profile template.
1. class UserProfile {
2. constructor(name, age, email) {
3. [Link] = name;
4. [Link] = age;
5. [Link] = email;
6. }
7.
8. clone() {
9. return new UserProfile([Link], [Link],
[Link]);
10. }
11. }
12.
13. export default UserProfile;
In the above code example, we defined a UserProfile class
with intrinsic properties such as name, age, and email.
Additionally, we implemented a clone method that creates
a new instance of this class, effectively duplicating the
existing object. Now, let’s see how to write a test case for
this:
1. import UserProfile from './UserProfile';
2.
3. test('UserProfile should be cloned correctly', () => {
4. const originalProfile = new UserProfile('John Doe',
30, '[Link]@[Link]');
5. const clonedProfile = [Link]();
6.
7. expect(clonedProfile).[Link](originalProfile); //
Ensure it's a new object
8. expect([Link]).toBe('John Doe');
9. expect([Link]).toBe(30);
10.
expect([Link]).toBe('[Link]@[Link]
m');
11. });
12.
Testing the Prototype Pattern involves ensuring that the
cloned object has the same properties as the original
object. In the above unit test, we first create an original
profile and then its clone. We then verify that the cloned
instance is a new instance, distinct from the original.
Finally, we confirm that the cloned instance has the same
properties as the original profile.
Mediator Pattern
The Mediator Pattern defines an object that encapsulates
how a set of objects interact. This pattern promotes loose
coupling by keeping objects from referring to each other
explicitly and allowing their interaction to be managed by
the mediator. Let us consider the example of a chat room
application where users send messages to each other
through a chat room mediator.
1. class ChatRoom {
2. showMessage(user, message) {
3. const time = new Date().toLocaleTimeString();
4. return `${time} [${user}]: ${message}`;
5. }
6. }
7.
8. class User {
9. constructor(name, chatRoom) {
10. [Link] = name;
11. [Link] = chatRoom;
12. }
13.
14. send(message) {
15. return [Link]([Link],
message);
16. }
17. }
18.
19. export { ChatRoom, User };
In the above code example, we defined a chat room using
the ChatRoom class, which contains a method to display a
message with the correct timestamp. Additionally, we
created a User class that defines the user’s name and
associates the user with the chat room. A send method is
used for sending messages. Now, let’s write unit test cases
for this:
1. import { ChatRoom, User } from './ChatRoom';
2.
3. test('ChatRoom should mediate messages between
users', () => {
4. const chatRoom = new ChatRoom();
5. const user1 = new User('Alice', chatRoom);
6. const user2 = new User('Bob', chatRoom);
7.
8. const message1 = [Link]('Hello, Bob!');
9. const message2 = [Link]('Hi, Alice!');
10.
11. expect(message1).toMatch(/^\d{1,2}:\d{2}:\d{2}
[APM]{2} \[Alice\]: Hello, Bob!$/);
12. expect(message2).toMatch(/^\d{1,2}:\d{2}:\d{2}
[APM]{2} \[Bob\]: Hi, Alice!$/);
13. });
Testing the Mediator Pattern involves ensuring that
messages are correctly mediated by the chat room. In the
above unit test case, we verify this by creating two users
and having each user send a different message. We then
check to ensure that the correct messages have been sent
and mediated properly.
Summary
Based on what we have covered in this section so far, let us
summarize and compile a list of what needs to be tested for
each Design Pattern:
Singleton Pattern
• Uniqueness: Ensure only one instance is created.
• Instance identity: Confirm that repeated calls return
the same instance.
• State consistency: Check that the instance maintains
state across multiple accesses.
Factory pattern
• Object creation: Verify that the factory method
creates objects of the correct type.
• Exception handling: Ensure that the factory handles
invalid input appropriately.
• Scalability: Test that the factory can be extended to
create new types of objects without modifying existing
code.
Observer pattern
• Subscription: Test that observers can subscribe to the
subject.
• Unsubscription: Ensure observers can unsubscribe
from the subject.
• Notification: Verify that all subscribed observers are
notified of state changes.
• Order of notification: Check the order in which
observers are notified, if order matters.
Strategy Pattern
• Algorithm selection: Verify that the context correctly
selects and uses the provided strategy.
• Strategy interchangeability: Ensure different
strategies can be swapped and produce correct results.
• Extensibility: Test that new strategies can be added
without modifying existing strategies.
Decorator Pattern
• Dynamic behavior: Verify that decorators can
dynamically add behavior to the original object.
• Combinability: Ensure multiple decorators can be
combined and applied in sequence.
• State independence: Check that the decorated object
maintains state independently of other decorators.
Adapter Pattern
• Interface compatibility: Verify that the adapter
makes the adapted object compatible with the target
interface.
• Functionality: Ensure the adapter correctly maps
methods from the adaptee to the target interface.
• Transparency: Test that the client can use the adapted
object without being aware of the adapter’s presence.
Builder Pattern
• Stepwise construction: Verify that the builder
constructs objects in a step-by-step manner.
• Final product: Ensure the final object constructed by
the builder is correct.
• Fluent interface: Check that the builder’s interface
allows chaining of methods (if applicable).
Prototype Pattern
• Cloning: Test that objects can be cloned correctly
from the prototype.
• Identity: Ensure the cloned object is a distinct
instance but with identical properties to the prototype.
• Customization: Verify that the cloned object can be
customized after cloning.
Mediator Pattern
• Interaction management: Verify that the mediator
correctly manages interactions between colleagues.
• Decoupling: Ensure that colleagues are decoupled
and only communicate via the mediator.
• State management: Check that the mediator handles
state changes and notifies relevant colleagues.
Mock dependencies
We need to use mocking to isolate the unit under test.
Mocking allows us to simulate dependencies, which helps
ensure that our tests are focused on the component itself
rather than on its collaborators.
Automate tests
We can use a testing framework like Jest, Mocha, or
Jasmine to automate the execution of our test cases.
Automation helps ensure consistency and makes it easier to
run tests frequently.
Ensure coverage
We can use coverage tools like Istanbul to measure how
much of our code is exercised by our tests. While aiming for
high coverage is important, it’s equally crucial to write
meaningful tests that serve as a blueprint for how our
application is built, rather than just covering lines of code.
Conclusion
In this chapter, we have explored a critical part of the
software development cycle—testing—and how Design
Patterns in JavaScript can influence the way unit tests are
written. We began by understanding what testing is, the
different types of testing, and its importance in the
software development life cycle. Then, we examined ten of
the most commonly used JavaScript Design Patterns and
explored unit test cases for each. Finally, we discussed how
to plan unit test cases for these JavaScript Design Patterns.
This chapter has provided insight into how to write unit
test cases for JavaScript Design Patterns and how these
patterns can shape the process of writing effective tests.
In the next chapter, we will discuss the process to write
and create Design Patterns.
Points to remember
• Testing is a crucial part of the software development
cycle. In JavaScript, testing involves writing and
executing code to verify that applications function
correctly and meet specified requirements. This process
includes creating tests that simulate various scenarios
and inputs to ensure the application behaves as
expected under different conditions.
• Testing has a significant role in writing small to
enterprise level code. Testing helps identify and fix
bugs during the development process. Additionally,
testing ensures that even when the code needs to be
refactored, any regressions will be caught by the tests
thereby not impacting the user’s experience of the
product.
• A well-written test is easier to maintain and extend,
and thereby boosts confidence amongst developers.
Testing also helps in frequent and reliable code
deployments.
• The Singleton Pattern restricts the instantiation of a
class to a single object. To test Singleton Patterns, we
can check to ensure only one instance is created and
confirm that repeated calls return the same instance.
• The Factory pattern provides a way to create objects
without specifying the exact class of object that will be
created. To test code written using Factory Design
Pattern, we can verify that the factory method creates
objects of the correct type, and that the method can be
extended to create new types of objects without
modifying existing code.
• The Observer pattern allows an object, to maintain a
list of its observers, and notify them of state changes.
To test it, we can check if observers can subscribe and
unsubscribe the object and further verify that all
subscribed observers are notified of state changes.
• To write effective test cases for Design Patterns in
JavaScript, we first need to have a thorough
understanding of the Design Pattern and understand its
nuances to write a test case for it.
• For writing an effective test case for these Design
Patterns, we first need to identify the main components,
its responsibilities, and then define the test cases.
• While defining a test case for a Design Pattern, we
should test for its expected behaviour, edge-case
scenarios, and error handling.
• For API-driven code, it is necessary to mock certain
functions and applications and automate the code. Code
coverage often helps in understanding the health of our
application and how secure will it be when users start
using it in their daily grind.
Exercises
1. Which of the following things need to be tested
when writing a unit test case for a Decorator
Design Pattern?
a. Ensuring that the cloned object is a distinct instance
but with identical properties to the prototype.
b. Ensure that the final object constructed by the builder
is correct.
c. Verify that behaviour can be added dynamically to the
original object.
d. Verify that the context correctly selects and uses the
provided strategy.
2. Which of the following scenarios are NOT to be
considered when writing unit test cases for
Observer Design Pattern?
a. Verify that requests are passed along the chain until
handled.
b. Verify that all subscribed observers are notified of
state changes.
c. Confirm if the observer can unsubscribe from a
subject.
d. Checking the order in which observers are notified.
3. Which of the following tasks should not be
considered when planning a unit test cases for
JavaScript Design Patterns?
a. Ensuring coverage of the total written test cases
using a tool like Istanbul.
b. Ignore the error handling part of the code as it is
more important to test its working functionality.
c. Identifying key components and their responsibilities
so that we have a clear understanding of what we
need to test in a component.
d. Have a thorough understanding of the Design Pattern
for which the unit test cases need to be written.
Answers
1. c
2. a
3. b
CHAPTER 11
Writing and Creating
Design Patterns
Introduction
In software development, Design Patterns are vital for
transforming theoretical concepts into practical, reusable
solutions. This chapter focuses on crafting Design Patterns
for JavaScript, emphasizing their role in solving recurring
problems and enhancing project robustness. We will
explore the steps in authoring Design Patterns, from
analyzing problem scenarios to defining the problem and
context. Understanding the structure and elements of a
Design Pattern is essential for clarity and utility. Clear,
concise pattern descriptions and strategies for ensuring
reusability and flexibility will be discussed. Additionally,
the importance of review, refinement, and thorough
documentation will be highlighted.
Finally, we will explore the significance of publishing and
sharing Design Patterns, covering various platforms and
fostering collaborative development. This guide aims to
equip us with the knowledge and skills to create impactful
Design Patterns that enhance JavaScript development.
Structure
This chapter will cover the following topics:
• Design Pattern authoring process
• Creating effective Design Patterns
• Publishing and sharing Design Patterns
Objectives
The objectives of this chapter are to provide a
comprehensive understanding of the process involved in
writing and creating effective Design Patterns for
JavaScript. By the end of this chapter, readers will be able
to analyse problem scenarios and define the context
accurately. They will learn the essential components and
structure of Design Patterns, enabling them to write clear
and concise descriptions. The chapter also aims to teach
strategies for ensuring the reusability and flexibility of
Design Patterns, making them adaptable to various coding
environments. Additionally, readers will understand the
importance of thorough review, refinement, and
documentation. Finally, the chapter will guide readers on
how to publish and share their Design Patterns, fostering
collaborative development within the community.
Facilitate extension
Design the pattern in a way that makes it easy to extend.
Consider the Open/Closed Principle, which states that
software entities should be open for extension but closed
for modification. This means we should be able to add new
functionality without altering existing code.
Example: In the Factory Method Pattern, the creator class
should be able to produce new types of products without
modifying the existing factory method. Instead, we can
extend the creator class and override the factory method.
Review, refinement, and documentation
The process of creating effective Design Patterns does not
end with the initial draft. Review, refinement, and thorough
documentation are crucial steps to ensure the pattern’s
quality and usability. Here are some of the ways we can do
this:
Peer review
Conducting peer reviews with other developers is one of
the first methods for getting feedback on the pattern. Peer
reviews help us identify ambiguities, inconsistencies, and
potential improvements. To make the peer review process
actionable, we should encourage reviewers to evaluate the
pattern based on specific criteria:
• Readability: Is the pattern easy to understand? Are
the names of functions, variables, and classes intuitive?
• Scalability: Can the pattern handle increased load or
complexity? Does it support future changes without
requiring significant modifications?
• Edge case handling: Does the pattern account for
edge cases and error scenarios? Are there any
situations where it might fail or produce unexpected
results? Reviewers should feel empowered to ask
questions and challenge assumptions to refine the
design further.
Iterative refinement
Based on feedback from peer reviews and practical
applications, refine the pattern iteratively. Each iteration
should aim to improve clarity, address edge cases, and
enhance the pattern’s applicability. This iterative process
helps ensure the pattern evolves to meet real-world needs.
Comprehension documentation
Thorough documentation is essential for the widespread
adoption of Design Patterns. We need to ensure that our
documentation covers all aspects of the pattern, including:
• Intent
• Motivation
• Structure
• Participants
• Collaborations
• Consequences
• Implementation
• Sample code
• Known uses
• Related patterns
By following these practices, we can develop Design
Patterns that are not only effective solutions to specific
problems but also valuable resources for the broader
developer community.
Open-source contributions
Open-source contributions to our Design Pattern repository
should be encouraged. Open-source projects benefit from
the collective knowledge and experience of the community,
leading to continuous improvement.
How to use:
• Define contribution guidelines and a code of conduct
for contributors.
• Use version control systems like Git to manage
contributions and track changes.
• Review and merge contributions through pull requests
or merge requests.
Conclusion
Creating and sharing effective Design Patterns is a vital
skill for any software developer. By understanding the
Design Pattern authoring process, analyzing problem
scenarios, and defining clear, reusable solutions, we can
contribute valuable patterns to the developer community.
Writing clear and concise pattern descriptions, ensuring
flexibility, and engaging in collaborative development
enhance the impact and usability of our patterns. Utilizing
various publishing platforms and repositories ensures that
our patterns reach a broad audience, fostering best
practices and innovation in software development. This
chapter has provided the tools and insights needed to craft
and share high-quality Design Patterns.
In the next chapter, we will be looking at the common
pitfalls faced by developers when working with JavaScript
Design Patterns and counter those with proven measures
and processes.
Points to remember
• To begin with, we need to ensure that our Design
Patterns are described using simple, precise language,
focusing on the core idea and enhancing understanding
with visual aids and real-world examples.
• Design Patterns should be general enough to apply to
various problems, promote loose coupling, and facilitate
easy extension, adhering to principles like the
Open/Closed Principle.
• We need to include all key elements in our pattern
documentation, such as intent, motivation, structure,
participants, collaborations, consequences,
implementation, sample code, known uses, and related
patterns.
• We should engage in peer reviews, use online
collaboration tools, participate in pattern writing
workshops, and encourage open-source contributions to
refine and validate our patterns.
• We need to utilize online pattern repositories,
platforms like GitHub, personal blogs, technical
journals, conferences, and books to publish and share
our Design Patterns with the developer community.
Exercise
1. Which of the following is most important when
writing a clear and concise Design Pattern
description?
a. Using complex technical jargon.
b. Including detailed implementation code only.
c. Focusing on the core idea and using simple language.
d. Describing every possible use case.
2. What principle should be followed to ensure that a
Design Pattern is easily extendable without
modifying existing code?
a. Single Responsibility Principle
b. Open/Closed Principle
c. Dependency Inversion Principle
d. Interface Segregation Principle
3. Which platform is NOT typically used for
publishing and sharing Design Patterns?
a. GitHub
b. Technical Journals
c. Personal Blogs
d. Social Media Platforms
Answers
1. c
2. b
3. d
Introduction
In JavaScript development, even experienced developers
can encounter pitfalls that comPromise code efficiency,
maintainability, and security. This chapter highlights
common challenges such as implicit type conversion, scope
and variable hoisting, and misconceptions around the this
keyword. It also addresses asynchronous issues like
callback hell, performance concerns like memory leaks and
inefficient DOM manipulation, and the importance of robust
error handling and security practices. By understanding
and avoiding these pitfalls, developers can enhance their
approach to JavaScript Design Patterns, leading to more
scalable, maintainable, and high-performance code. This
chapter offers practical insights and strategies for
overcoming these obstacles.
Structure
This chapter will cover the following topics:
• Implicit type conversion and coercion
• Scope and variable hosting
• The this keyword misconception
• Callback hell and asynchronous operations
• Memory leaks and performance bottlenecks
• Inefficient DOM manipulation
• Error handling and debugging
• Security vulnerabilities
• Anti-pattern and code smells
Objectives
This chapter aims to help developers recognize and
navigate common JavaScript pitfalls by understanding
implicit behaviors like type conversion, coercion, scope,
and hoisting. It focuses on managing asynchronous
operations to prevent callback hell and improve code
maintainability. By addressing performance issues such as
memory leaks and inefficient DOM manipulation,
developers can optimize their applications. The chapter
also emphasizes enhancing error handling, debugging
skills, and addressing security vulnerabilities. Additionally,
it guides developers in identifying and avoiding anti-
patterns and code smells, all while promoting best
practices for cleaner, more scalable, and maintainable
code.
Security vulnerabilities
Security vulnerabilities are a critical concern in JavaScript
applications, especially when Design Patterns are involved.
While Design Patterns help create reusable and structured
code, they can also introduce security flaws if not
implemented carefully. Issues like cross-site scripting
(XSS), cross-site request forgery (CSRF), and insecure
handling of sensitive data can comPromise application
security. When applying Design Patterns like the Singleton,
Observer, or Factory, developers must ensure that their
code adheres to secure coding practices to prevent
potential breaches.
Spaghetti code
Spaghetti code refers to code that lacks clear structure,
often resulting from poor organization, excessive coupling,
and an absence of Design Patterns. Such code is hard to
read, test, and extend. It tends to arise in projects that
grow organically without a clear architectural plan, making
debugging and future development difficult. Here is a
perfect example of a spaghetti code:
1. function placeOrder(order) {
2. updateUser([Link]);
3. if ([Link] === 'card') {
4. processCardPayment(order);
5. } else {
6. processPaypalPayment(order);
7. }
8.
9. if ([Link] > 0) {
10. for (let i = 0; i < [Link]; i++) {
11. updateInventory([Link][i]);
12. }
13. }
14.
15. logOrder(order);
16. }
This function tries to do too many things at once. There is
no clear separation of concerns, and the code becomes
difficult to manage as new features or payment types are
added. We can refactor the spaghetti code by using Design
Patterns like the Strategy Pattern for payment handling
and Command Pattern for processing the order:
1. class PaymentStrategy {
2. processPayment(order) {
3. throw new Error("Method not implemented");
4. }
5. }
6.
7. class CardPaymentStrategy extends PaymentStrategy {
8. processPayment(order) {
9. // Card payment logic
10. }
11. }
12.
13. class PaypalPaymentStrategy extends
PaymentStrategy {
14. processPayment(order) {
15. // PayPal payment logic
16. }
17. }
18.
19. class OrderProcessor {
20. constructor(paymentStrategy) {
21. [Link] = paymentStrategy;
22. }
23.
24. processOrder(order) {
25. [Link](order);
26. [Link](item =>
updateInventory(item));
27. logOrder(order);
28. }
29. }
30.
31. const paymentStrategy = new
CardPaymentStrategy(); // or new
PaypalPaymentStrategy()
32. const orderProcessor = new
OrderProcessor(paymentStrategy);
33. [Link](order);
The Strategy Pattern enables you to switch between
different payment methods without cluttering the main
logic, while the Command Pattern helps organize the order
processing steps. This approach leads to cleaner, more
maintainable code.
Premature optimization
Premature optimization involves focusing on optimizing
code before it is necessary, often leading to complex and
hard-to-maintain solutions. While performance is
important, optimizing too early can introduce unnecessary
complexity and obscure the primary focus of the code. It is
usually best to optimize after identifying performance
bottlenecks using profiling tools. Check the following
example to understand it better:
1. function calculateTotal(items) {
2. let total = 0;
3. // Optimized prematurely for large arrays
4. for (let i = 0, len = [Link]; i < len; i++) {
5. total += items[i].price;
6. }
7. return total;
8. }
The above loop is optimized for performance, but unless we
are dealing with a large number of items, this early
optimization introduces complexity without providing
significant benefits. To solve this, we should write clean,
simple, and readable code first, and then optimize when we
identify real performance bottlenecks using profiling tools.
Here is a revised solution:
1. function calculateTotal(items) {
2. return [Link]((total, item) => total +
[Link], 0);
3. }
This approach is cleaner and more readable. Only focus on
optimization when you can measure a performance issue,
following the adage Premature optimization is the root of
all evil.
Avoiding anti-patterns and code smells is crucial for
creating maintainable, scalable, and clean JavaScript
applications. By identifying common pitfalls such as the
God Object, spaghetti code, and premature optimization,
developers can ensure that their Design Patterns are
implemented correctly. Refactoring code to adhere to
design principles and patterns helps eliminate bad
practices, resulting in a more robust application.
Conclusion
In this chapter, we have explored several common pitfalls
developers face when working with JavaScript and Design
Patterns, such as implicit type coercion, scope issues,
misuse of the this keyword, callback hell, memory leaks,
inefficient DOM manipulation, and security vulnerabilities.
We also highlighted anti-patterns like the God Object and
spaghetti code, which can erode the benefits of well-
structured Design Patterns. By identifying and avoiding
these challenges, developers can ensure their code remains
maintainable, scalable, and efficient. Ultimately, the key to
mastering JavaScript Design Patterns lies in understanding
these pitfalls and applying best practices to create robust,
resilient applications.
In the next chapter, which will be the final chapter of this
book, we will be looking at popular case studies of
successful organizations that have implemented Design
Patterns in their products.
Points to remember
• Understand implicit type coercion: JavaScript’s
automatic type conversion can lead to unexpected
results. Always use strict equality (===) to avoid
pitfalls.
• Be mindful of scope and hoisting:
Misunderstanding how scope and variable hoisting
work can lead to bugs. Use let and const instead of var
to avoid scope-related issues.
• Avoid this keyword misconceptions: The context of
this can change depending on how a function is called.
Use arrow functions or explicit binding methods like
bind(), call(), or apply() when necessary.
• Handle callback hell: Deeply nested callbacks can
make code unreadable and difficult to manage. Use
Promises, async/await, or Design Patterns like the
Observer to handle asynchronous operations more
effectively.
• Prevent memory leaks: Unmanaged event listeners,
closures, or unreferenced variables can cause memory
leaks. Clean up event listeners and watch out for
dangling references.
• Optimize DOM manipulation: Excessive or
inefficient manipulation of the DOM can severely
impact performance. Batch updates and use techniques
like virtual DOM or frameworks like React to optimize
rendering.
• Practice effective error handling: Always implement
proper error handling using try/catch blocks and build
informative error messages to aid debugging.
• Mitigate security vulnerabilities: Validate user
inputs, avoid directly injecting data into the DOM, and
always escape or sanitize input to protect against XSS
and injection attacks.
• Recognize anti-patterns and code smells: Avoid
common anti-patterns like the God Object or spaghetti
code by following design principles such as the SRP and
adhering to Design Patterns.
• Avoid premature optimization: Focus on writing
clean, readable code first. Only optimize after
identifying bottlenecks through profiling tools, to
prevent introducing unnecessary complexity.
By keeping these points in mind, developers can avoid
common pitfalls, ensuring their JavaScript applications
remain efficient, maintainable, and scalable.
Exercise
1. Which of the following is a common issue caused
by implicit type coercion in JavaScript?
a. Memory leaks due to improper event listener cleanup.
b. Unintended type conversions lead to incorrect
comparisons.
c. DOM manipulation that causes performance
bottlenecks.
d. Infinite loops in asynchronous operations
2. Which Design Pattern or technique helps to avoid
callback hell in JavaScript?
a. Singleton Pattern
b. Factory Pattern
c. Promises and async/await
d. Prototype Pattern
3. What is a key reason for memory leaks in
JavaScript applications?
a. Improper use of the this keyword in arrow functions.
b. Overuse of strict equality (===) instead of loose
equality (==).
c. Unreleased references, such as event listeners or
unreferenced objects.
d. Incorrect use of const and let instead of var.
Answers
1. b
2. c
3. c
CHAPTER 13
Case Studies
Introduction
In this final chapter, we explore how JavaScript Design
Patterns are applied in real-world scenarios by leading tech
companies. Design Patterns provide proven solutions to
recurring problems in software development, and their
practical application can significantly enhance scalability,
maintainability, and performance. By examining case
studies from companies like Google, Netflix, Twitter,
Amazon, and Airbnb, we will see how these organizations
have successfully employed specific Design Patterns to
address unique challenges. Each case study highlights a
particular pattern—such as the Singleton, Factory,
Observer, Composite, or Strategy Pattern—and showcases
the tangible benefits these companies achieved by
integrating them into their systems. Through this
exploration, we aim to provide not just theoretical
knowledge, but real-world insights into how Design
Patterns can drive innovation and success. By the end of
this chapter, you will have a clearer understanding of how
these patterns can be implemented in your own projects.
Structure
This chapter will cover the following topics:
• Case study examples
o Singleton Pattern at Google
o Factory Pattern at Netflix
o Observer Pattern at Twitter
o Composite Pattern at Amazon
o Strategy Pattern at Airbnb
• Analysis and insights
o Success stories and impacts
o Lessons learned
Objectives
The primary objective of this chapter is to demonstrate the
practical application of JavaScript Design Patterns through
real-world case studies. By analyzing how major companies
like Google, Netflix, Twitter, Amazon, and Airbnb have
implemented specific Design Patterns, readers will gain
insights into solving complex software challenges using
proven methodologies. This chapter aims to highlight the
tangible benefits of applying Design Patterns, such as
improved scalability, flexibility, and maintainability, and to
provide actionable knowledge that can be applied to
everyday development practices. Ultimately, it seeks to
bridge the gap between theory and practical execution.
Success metrics
Now, let us define and summarise the individual metrics of
success for each of the case studies:
• Google: The Singleton Pattern reduced resource usage
by 30%, leading to improved application performance
and reduced costs in server maintenance.
• Netflix: The Factory Pattern improved the efficiency of
service instance creation by 25%, allowing for faster
adaptation to user demands and increased engagement.
• Twitter: The Observer Pattern decreased update
latency by 40%, enhancing user experience and
increasing user interactions on the platform.
• Amazon: The Composite Pattern led to a 20%
reduction in code duplication and allowed for 35%
faster inventory updates, facilitating rapid expansion
during peak sales periods.
• Airbnb: Dynamic pricing strategies using the Strategy
Pattern boosted revenue for hosts by up to 25%,
improving overall booking rates by 15% during high-
demand seasons.
Business impacts
Finally, let us understand the business impacts of these
success metrics:
• Google: The improvements in resource management
translated into cost savings, allowing Google to reinvest
in new features and services, strengthening its
competitive position in cloud services.
• Netflix: Enhanced service responsiveness led to
increased user satisfaction and engagement, directly
impacting subscriber growth and retention rates.
• Twitter: Faster updates and improved user interaction
contributed to the role of Twitter as a leading platform
for real-time information, significantly increasing user
engagement metrics.
• Amazon: Streamlined inventory management and
product categorization facilitated a smoother shopping
experience, directly correlating with increased sales
during high-traffic events.
• Airbnb: Enhanced pricing flexibility allowed hosts to
maximize their earnings, which in turn increased host
loyalty and attracted more property listings to the
platform.
These success stories reinforce the value of Design
Patterns as vital tools in modern software development. By
strategically implementing these patterns, organizations
can not only overcome technical challenges but also
achieve remarkable business outcomes. As the landscape of
software engineering continues to evolve, the principles
derived from these case studies will serve as guiding lights
for developers seeking to create efficient, scalable, and
successful software solutions.
Lessons learned
The implementation of Design Patterns in the case studies
of Google, Netflix, Twitter, Amazon, and Airbnb offers
valuable lessons that can guide developers and
organizations in their software development practices.
Here are some key takeaways:
• Emphasize flexibility and adaptability
Design Patterns provide a flexible framework that can
be adapted to meet changing business needs and user
demands. Organizations should prioritize patterns that
allow for quick adjustments in response to evolving
requirements. For example, the usage of the Strategy
Pattern by Airbnb demonstrates how adaptable pricing
models can significantly impact revenue.
• Prioritize code maintainability
One of the most significant benefits of Design Patterns
is the improvement in code maintainability. By
organizing code around established patterns, developers
can create systems that are easier to understand, test,
and modify. For instance, the Composite Pattern used by
Amazon reduced code duplication, allowing teams to
maintain and extend the system with greater efficiency.
• Foster collaboration between teams
Successful implementation of Design Patterns often
requires collaboration among cross-functional teams.
Encouraging communication between developers,
product managers, and stakeholders can lead to more
effective solutions. The Factory Pattern used by Netflix
highlights how aligning development efforts across
teams can improve user experience by ensuring that
services are tailored to diverse needs.
• Focus on measurable outcomes
It is essential to establish clear metrics to measure the
impact of Design Patterns on business goals. By
quantifying success, organizations can make informed
decisions about which patterns to implement. The usage
of the Singleton Pattern by Google, for example,
demonstrated tangible improvements in resource
management, which can be closely monitored for
ongoing effectiveness.
• Continuous learning and iteration
The technology landscape is ever-evolving, and so are
the challenges faced by software teams. Organizations
should foster a culture of continuous learning and
iteration, encouraging teams to revisit and refine their
use of Design Patterns. The experiences of Twitter with
the Observer Pattern exemplify the importance of
adapting design approaches to optimize system
performance over time.
• Understand the trade-offs
While Design Patterns provide numerous benefits, they
also come with potential trade-offs, such as increased
complexity in certain cases. Developers must carefully
weigh the advantages against the complexities
introduced by each pattern. Understanding the specific
context in which a pattern is applied is crucial to
maximizing its effectiveness while minimizing
drawbacks.
• Document and share knowledge
Proper documentation of Design Patterns and their
implementations fosters knowledge sharing within the
organization. By creating a repository of Design
Patterns, teams can build on past successes and avoid
repeating mistakes. This practice is vital for onboarding
new team members and ensuring consistency in
software development practices.
The lessons learned from these case studies underscore the
importance of Design Patterns in creating robust, scalable,
and maintainable software solutions. By applying these
lessons, developers and organizations can not only improve
their technical capabilities but also enhance their overall
business outcomes. As the software development landscape
continues to evolve, leveraging Design Patterns will remain
a critical strategy for success.
Conclusion
This chapter has explored the practical implementation of
Design Patterns through case studies from leading
organizations such as Google, Netflix, Twitter, Amazon, and
Airbnb. Each case highlights how Design Patterns,
including Singleton, Factory, Observer, Composite, and
Strategy, have provided innovative solutions to complex
challenges, leading to significant improvements in
performance, scalability, and user satisfaction. The success
stories shared here not only demonstrate the technical
benefits of Design Patterns but also reveal their broader
business impacts, from enhanced resource management to
increased revenue and customer loyalty. Moreover, the
lessons learned emphasize the importance of flexibility,
maintainability, collaboration, and continuous learning in
the software development process. By recognizing the
trade-offs associated with Design Patterns and committing
to thorough documentation and knowledge sharing,
organizations can harness the full potential of these
patterns. As the software development landscape continues
to evolve, the insights gained from these case studies will
serve as valuable guides for developers seeking to
implement effective solutions in their own projects.
Ultimately, Design Patterns remain essential tools that
empower teams to create efficient, scalable, and
sustainable software systems that meet the demands of
today’s fast-paced digital world.
Points to remember
• Understanding Design Patterns: Design Patterns
are structured solutions to common software design
challenges, enhancing code maintainability and
readability.
• Real-world applications: Companies like Google,
Netflix, Twitter, Amazon, and Airbnb showcase how
Design Patterns address specific challenges and lead to
notable outcomes.
• Selection of patterns: Choosing the appropriate
Design Pattern is crucial; each has unique strengths
and weaknesses that should align with the problem
being solved.
• Flexibility and scalability: Design Patterns facilitate
adaptability, allowing software systems to evolve in
response to changing requirements and user needs.
• Measurable impact: Establishing clear metrics is
essential for evaluating the effectiveness of Design
Patterns and their contributions to business goals.
• Collaboration and communication: Successful
implementation relies on collaboration among cross-
functional teams to ensure alignment and effective
development processes.
• Continuous improvement: Foster a culture of
learning and iteration, regularly refining Design
Patterns to remain relevant in an evolving tech
landscape.
Questions
1. Which Design Pattern is primarily used by Google
to manage a single instance of a resource-
intensive object?
a. Factory Pattern
b. Singleton Pattern
c. Observer Pattern
d. Composite Pattern
2. What was the main benefit of the Factory Pattern
as implemented by Netflix?
a. Improved real-time notifications.
b. Streamlined inventory management.
c. Dynamic generation of service instances.
d. Enhanced revenue through dynamic pricing.
3. Which lesson learned emphasizes the importance
of collaboration among teams in the
implementation of Design Patterns?
a. Flexibility and scalability
b. Continuous improvement
c. Real-world applications
d. Collaboration and communication
Answers
1. b
2. c
3. d
Index
A
Abstract Factory Pattern 44
Abstract Factory Pattern, advantages 51
Abstract Factory Pattern, configuring 44
Abstract Factory Pattern, guide
Abstract Factory, defining 47
Abstract Products, defining 46
Client Code 49
Concrete Code, implementing 48
Concrete Products, implementing 47
Abstract Factory Pattern, instances
Document Process Libraries 50
Localization/Internationalization 50
UI Component Libraries 50
Abstract Factory Pattern, steps
Abstract Factory 45
Abstract Products, defining 45
Client Code 45
Concrete Factories, implementing 45
Concrete Products, implementing 45
Adapter Design Pattern 72
Adapter Design Pattern, advantages
Open/Close Principle 79
Single Responsibility Principle (SRP) 79
Adapter Design Pattern, applications
Internationalization 79
Legacy Code, integrating 78
Microservices, integrating 79
Third-Party API, integrating 78
Version, upgrading 79
Adapter Design Pattern, guide
Adapter 76, 77
Client Code 77
Existing Class 75
Target Interface 75
Usage 77
Adapter Design Pattern, scenario 72-74
Adapter Design Pattern, steps
Adapter, implementing 74
Client Code 74
Existing Class 74
Target Interface 74
Usage 74
Anti-Pattern 418
Anti-Pattern, points
God Object 419, 420
Premature Optimization 422
Spaghetti Code 420, 421
Async/Await 182, 183
Async/Await, keywords
async 183
await 183
Asynchronous JavaScript 172
Asynchronous JavaScript, sections
Async/Await 182
Callback Functions 175, 176
Callback Hell 177, 178
Generator/Yield 185, 186
Promises 179
Synchronous JavaScript 172
Web APIs 186
B
Batch Processing 208
Batch Processing, advantages 213
Batch Processing, applications
API Requests 212
Database Operations 212
Data Migration 213
Image Processing 212
Job Queues 213
Report Generation 213
Batch Processing, architecture 208, 209
Batch Processing, concepts
Async Task Function, implementing 210
Complex Logic, executing 211
Function, defining 209, 210
Input Data, defining 211
Batch Processing, disadvantages 213, 214
Behavioral Design Patterns 120
Behavioral Design Patterns, architecture 123
Behavioral Design Patterns, configuring 120-122
Behavioral Design Patterns, issues
Dynamic Behaviour 123
Flexibility/Extensibility 123
Loose Coupling 122
Responsibility Chain 123
Bridge Design Pattern 79
Bridge Design Pattern, advantages 86
Bridge Design Pattern, applications
Communication Channels 86
Cross-Platform Applications 85
Database Abstraction Layer 85
Network Communication Framework 86
Bridge Design Pattern, concepts
Abstraction 83
Client Code 84
Concrete Implementors 82
Implementor Interface 82
Refined Abstraction 84
Bridge Design Pattern, scenarios 80
Bridge Design Pattern, steps
Abstraction 81
Client Code 81
Concrete Implementors, reating 81
Implementor Interface, defining 81
Refined Abstractions 81
Builder Pattern 51
Builder Pattern, advantages
Flexibility 60
Reusability 60
SRP 61
Builder Pattern, applications
Composite Objects, assembling 60
Database Query 60
Documents/Reports, building 60
Object Construction 60
Product Variants 60
Builder Pattern, approaches 52
Builder Pattern, architecture 51
Builder Pattern, concepts
Builder Interface 55
Client Code 57
Concrete Builders 56
Director 56
Product 54
Builder Pattern, observations 59
Builder Pattern, steps
Builder Interface, defining 53
Client Code 54
Concrete Builders, creating 53
Director, creating 54
Product, defining 53
C
Callback Functions 175, 176
Callback Hell 177, 178, 398
Callback Hell, architecture 398-401
Callback Hell, reasons
Anonymous Functions 179
Complex Control Flow 179
Error Handling 179
Sequential Asynchronous Operation 179
Chain of Responsibility (CoR) Pattern 123
Class-Based/JavaScript OOP, comparing 29
Collaborative Pattern Development 378
Collaborative Pattern Development, approach 378, 379
Combining Design Patterns 299
Combining Design Patterns, advantages 299, 300
Combining Design Patterns, section
Factory Design With Singleton 300-302
Mediator Design With Observer 302-304
Strategy Design With Decorator 304-306
Command Pattern 131
Command Pattern, applications
GUI Applications 136
Logging/Auditing 136
Smart Home Automation 136
Undo/Redo Functionality 136
Workflow Management 136
Command Pattern, architecture 131
Command Pattern, benefits 137
Command Pattern, concepts
Client Code 135
Command Interface 132
Concrete Command Classes 133
Invoker Classes 134
Receiver Classes 134
Command Pattern, steps
Client Code 132
Command Interface 131
Concrete Command Classes, implementing 132
Invoker Classes, implementing 132
Receiver Classes, implementing 132
Composite Design Pattern 86
Composite Design Pattern, applications
Document Object Model (DOM) 91
E-Commerce Shopping 92
File System Operations 92
GUI Components 91
Menu Systems 92
Tree Structures 91
Composite Design Pattern, concepts
Client Code 90, 91
Component Interface 88
Composite Class, implementing 89
Leaf Class, implementing 88
Composite Design Pattern, pros 92
Composite Design Pattern, scenario 86, 87
Composite Design Pattern, steps
Client Code 87
Component Interface 87
Composite Class, implementing 87
Leaf Class, implementing 87
Computed Properties 272, 273
Computed Properties, advantages 274
Concurrency 206
Concurrency, approaches
Async/Await 208
Callbacks 207
Event Loop 207
Promises 207
Concurrency/Parallelism, comparing 206, 207
Container/Component Pattern 238
Container/Component Pattern, advantages 242
Container/Component Pattern, term
Container 239-241
Presentation 241
Context API Pattern 245, 246
Context API Pattern, advantages 249
Context API Pattern, implementing 246-248
CoR Pattern, advantages 130
CoR Pattern, applications
Chatbot Response, processing 130
[Link] Middleware 130
Form Validation 130
Middleware Processing 130
Workflow Engines 130
CoR Pattern, concepts
Chain, setup 128
Client Code 129
Concrete Handlers 126
Handle Method, implementing 127, 128
Handler Interface 125
CoR Pattern, scenarios 123, 124
CoR Pattern, steps
Chain, setup 125
Client Code 125
Concrete Handlers, creating 125
Handle Method, implementing 125
Handler Interface, defining 125
Creational Design Patterns 30
D
Debouncing 195
Debouncing, advantages 199
Debouncing, architecture 196-198
Debouncing, disadvantages 199
Debouncing, scenarios 198, 199
Decorator Design Pattern 92
Decorator Design Pattern, advantages 98
Decorator Design Pattern, application
Authentication 97
Data Validation 97
Dynamic Configuration 97
Event Handling 97
User Interface 97
Decorator Design Pattern, concepts
Component Interface 94
Concrete Components 94
Concrete Decorators 95
Decorator Class 95
Usage 96
Decorator Design Pattern, disadvantages 98
Decorator Design Pattern, scenarios 92, 93
Decorator Design Pattern, steps
Component Interface 93
Concrete Component 93
Concrete Decorators 93
Decorator Class, creating 93
Usage 94
Decorator Pattern 258, 259
Designing Systems 312
Designing Systems, roles 312, 313
Design Pattern Authoring, process
Problem/Context, defining 366
Problem Scenarios 364
Structure/Elements 368
Design Pattern Authoring Process 364
Design Patterns 6, 7
Design Patterns, advantages
Abstraction/Encapsulation 10
Common Vocabulary 10
Facilitates Evolution/Adaptation 11
Faster Development 11
Maintainability 10
Performance, improving 10
Proven Solutions 10
Reusability 9
Scalability 10
Design Patterns, architecture 8
Design Patterns, aspects
Decorator Pattern 319
Factory Pattern 318
Module Pattern 314, 315
Observer Pattern 317
Proxy Pattern 321
Revealing Module Pattern 315
Singleton Pattern 316
Strategy Pattern 320
Design Patterns Business, impacts 435
Design Patterns, case studies 434
Design Patterns, categories
Behavioural Design 9
Creational Design 9
Structural Design 9
Design Patterns, fundamentals
Four Book Gang 8
Pattern Languages 7, 8
Design Patterns, key takeaways 436, 437
Design Patterns, lists
Decorator Pattern 325
Factory Pattern 327
Observer Pattern 324
Proxy Pattern 326
Singleton Pattern 323
Design Patterns, points
Decorator Pattern 332
Factory Pattern 332
Module Pattern 330
Observer Pattern 331
Proxy Pattern 333
Singleton Pattern 330
DOM Manipulation 405
DOM Manipulation, pitfalls
Direct DOM, accessing 407
Excessive Style 405
Unnecessary DOM 406
Dynamic Load Components 276
Dynamic Load Components, ways 276, 277
E
Effective Design Patterns 373
Effective Design Patterns, strategies
Concise Pattern Descriptions 373
Refinement/Documentation, reviewing 375
Reusability/Flexibility, ensuring 374, 375
Error Handling 408
Error Handling, terms
Lock of Proper 408, 409
Silent Failures 411-413
Event Handling 187
Event Handling, aspects
Event Loop 190
Event Propagation 189
Events 187, 188
Event Loop 190
Event Loop, configuring 190, 191
Event Propagation 189
Event Propagation, phase
Bubbling 189
Capturing 189
F
Façade Design Pattern 98
Façade Design Pattern, applications
APIs, optimizing 103
Browser Compatibility 103
Legacy Code Integration 103
Resource Management 103
Third-Party Library, integrating 103
Façade Design Pattern, concepts
Client Code, uses 102
Encapsulate 101
Interface, simplifying 101
Methods, expose 102
Subsystem Classes 100
Façade Design Pattern, pros 103
Façade Design Pattern, scenario 98
Façade Design Pattern, steps
Client Code 99
Facade Classes, creating 99
Simplified Methods 99
Subsystem Classes, creating 99
Subsystem Logic, encapsulate 99
Factory Method 37
Factory Method, advantages 43
Factory Method, applications
Data Access Layers 43
Game Development 43
Internationalization 43
UI Component Libraries 43
Factory Method, architecture 37
Factory Method, disadvantages 43
Factory Method, implementing 38
Factory Method, steps
Client Code 42
Concrete Creator, implementing 41
Concrete Products, implementing 39
Creator Interface, defining 40
Product Interface 39
Factory Pattern 253
Factory Pattern, optimizing 253-255
Flyweight Design Pattern 104
Flyweight Design Pattern, applications
Collaborative Tools 109
Document Object Model (DOM) 109
Engine Rendering 108
UI Components 108
Flyweight Design Pattern, process
Client Code 105
Concrete Class, implementing 105
Extrinsic States, identifying 104
Flyweight Factory 105
Flyweight Interface, creating 105
Flyweight Design Pattern, pros 109
Flyweight Design Pattern, scenarios 104
Flyweight Design Pattern, steps
Class, implmenting 106
Client Code 108
Extrinsic States 106
Factory, implementing 107
Interface, creating 106
H
Higher-Order Components (HOCs) 236
HOCs, configuring 236, 237
HOCs, patterns
Adapter 238
Composite 238
Decorator 237
Factory 237
Proxy 238
Strategy 237
Hooks 230
Hooks, components
useCallback 235
useMemo 234
Hooks, patterns
Adapter 235
Factory 235
Observer 235
Proxy 235
Strategy 235
Hooks, types
Context 231
Effect 233
Performance 234
Ref 232
State 231
I
Iterator Pattern 137
Iterator Pattern, advantages 143
Iterator Pattern, applications
Database Results, navigating 143
Object Properties, traversing 143
Over Arrays, iterating 142
Parsing Streams 143
Iterator Pattern, architecture 137-139
Iterator Pattern, concept
Aggregate Object 141
Client Code 141
Concrete Iterator 140
Iterator Interface 140
Iterator Pattern, disadvantages 143
Iterator Pattern, steps
Aggregate Object, creating 139
Client Code 139
Concrete iterator, implementing 139
Iterator Interface 139
J
JavaScript 2
JavaScript Conversion, types
Behavioral Pattern 388
Breaking Consistency 385, 386
Structural Pattern 387
JavaScript Design Patterns, approach
Automate Tests 360
Coverage Ensuring 360
Design Pattern 360
Mock Dependencies 360
Test Cases 360
JavaScript Design Patterns, case study
Composite Pattern 430
Factory Pattern 428
Observer Pattern 429
Singleton Pattern 426
Strategy Pattern 432
JavaScript, fundamentals
Inception 2
Revival 3
War Browsers 2, 3
JavaScript, pitfalls
Problem Statement 1 3, 4
Problem Statement 2 4
Problem Statement 3 4, 5
Problem Statement 4 5
Problem Statement 5 6
JavaScript, solutions
Problem Statement 1 11, 12
Problem Statement 2 12, 13
Problem Statement 3 13, 14
Problem Statement 4 14, 15
Problem Statement 5 15, 16
M
Mediator Pattern 144
Mediator Pattern, advantages 149
Mediator Pattern, applications
Chat Applications 148
Game Development 148
Microservices Communication 149
Real-Time Collaborative 149
Mediator Pattern, architecture 144
Mediator Pattern, concepts
Communication mediator 148
Entity Objects 146
Mediator Object 145, 146
Register Entities 147
Mediator Pattern, steps
Communication Mediator 145
Entity Objects, creating 145
Mediator Object, defining 145
Register Entities 145
Memento Pattern 150
Memento Pattern, advantages 155
Memento Pattern, application
Drawing Application 155
Form State Management 154
Game State Management 155
Text Editors/Code Editors 154
Memento Pattern, architecture 150
Memento Pattern, concepts
Caretaker Class 152
Client Code 153
Memento Class 152
Originator Class 151
Memento Pattern, disadvantages 155
Memento Pattern, steps
Caretaker Class 151
Client Code 151
Memento Class 150
Originator Class, creating 150
Memory Management 401
Memory Management, term
Data Handling 404, 405
DOM Manipulation 402, 403
Memory Leak 401, 402
Module Pattern 250, 282
Module Pattern, advantages 285
Module Pattern, disadvantages 285
Module Pattern, implementing 283, 284
Module Pattern, principles
Closures 283
Encapsulation 283
Module Pattern, steps 250-253
Module Pattern, utilizing 250
MVC Pattern 286
MVC Pattern, advantages 290
MVC Pattern, configuring 286-290
MVC Pattern, disadvantages 290
MVMM Pattern 295
MVMM Pattern, advantages 298, 299
MVMM Pattern, components 295
MVMM Pattern, disadvantages 299
MVMM Pattern, implementing 296-298
MV* Patterns 285
MV* Patterns, components
Controller/ViewModel/Presenter 286
Model 286
View 286
MV* Patterns, section
MVC Pattern 286
MVMM Pattern 295
MVP Pattern 290
MVP/MVC Pattern, comparing 291-294
MVP Pattern 290, 291
MVP Pattern, advantages 294
MVP Pattern, disadvantages 295
O
Object Creation 30
Object Creation, fundamentals
Code Organization 30
Encapsulation 30
Reusability 30
Scalability 30
Object Creation, terms
Circular Dependencies 31, 32
Memory Leak 31
Uninitialized Variables 32
Object-Oriented Programming (OOP) 20
Observer Pattern 155, 255
Observer Pattern, applications
Cross-Component Communication 162
Event Handling 162
Model-View-Controller (MVC) 162
UI Components/State Management 162
Observer Pattern, architecture 156
Observer Pattern, concepts
Notification Mechanism, implementing 159, 160
Observer Classes, defining 158
Subscribe/Trigger 160, 161
Subject Class 157, 158
Subscription Mechanism, implementing 158, 159
Observer Pattern, implementing 156, 157
Observer Pattern, pros 162
OOP, concepts
Constructors 24-26
Inheritance 26, 27
Object Prototype 23, 24
Polymorphism 28, 29
OOP, principles
Abstraction 22
Encapsulation 22
Inheritance 22
Polymorphism 22
OOP, section
Classes 21, 22
Objects 21
OOP, types
Classical 20
JavaScript 22
P
Parallelism 200
Parallelism, approaches
Parallel Array 200
Parallel Processing 200
Web Workers 200
Promises 179, 180
Promises, approaches
Chaining 215
Error Handling 216
Memory Management 217
Nested Avoiding 214, 215
Parallel 216
Performance Considerations 218
Reuse 217
Promises, method
catch() 181
then() 180
Promises, states
Fulfilled 180
Pending 180
Rejected 180
Prototype Pattern 61
Prototype Pattern, advantages 64
Prototype Pattern, applications
Dynamic Object, configuring 64
Object Initialization 64
Object States, managing 64
Role-Based Permissions 64
Prototype Pattern, approaches 61
Prototype Pattern, architecture 61
Prototype Pattern, guide
Customizing 63
Instances, creating 63
Instances, testing 63
Object Prototype 62
Prototype Pattern, steps
Instance, creating 62
Instance, customizing 62
Instances, testing 62
Object Prototype, creating 62
Proxy Design Pattern 109
Proxy Design Pattern, advantages 114
Proxy Design Pattern, application
Access Control 114
Caching 114
Lazy Loading 114
Remote Proxy 114
Validation 114
Proxy Design Pattern, architecture 109
Proxy Design Pattern, disadvantages 114
Proxy Design Pattern, implementing 111-113
Proxy Design Pattern, process
Client Code 110
Proxy, creating 110
Proxy, implementing 110
Proxy Interface, defining 110
Real Object, implementing 110
Subject, identifying 110
R
React 222
React, architecture 223, 224
React, challenges
Code Reusability 223
DOM Manipulation 222
Scalability 223
React, patterns
Container/Component Pattern 238
Context API Pattern 245
Render Prop Pattern 242
React, terms
Component-Based Architecture 224
Higher-Order Components (HOCs) 236
Hooks 230
React, ways
Decorator Pattern 258
Factory Pattern 253
Module Pattern 250
Observer Pattern 255
Render Prop Pattern 242
Render Prop Pattern, advantages
Composability 245
Encapsulation 245
Flexibility 245
Reusability 245
Render Prop Pattern, implementing 242-245
S
Scoped Styles 275
Scoped Styles, advantages 276
Scoped Styles, configuring 275
Security Vulnerabilities 413
Security Vulnerabilities, points
Cross-Site Scripting (XSS) 415
CSRF 416-418
Singleton Pattern 413, 414
Sharing Design Patterns 376
Sharing Design Patterns, repositories
E-Books/Print Books 378
GitHub/GitLab 377
Online Pattern 377
Personal Blog/Websites 377
Singleton Pattern 32
Singleton Pattern, configuring 33
Singleton Pattern, cons 37
Singleton Pattern, implementing 34, 35
Singleton Pattern, pros 37
Singleton Pattern, scenarios
Application State, maintaining 37
Caching 36
Database Connection, managing 36
Global Configuration 36
Singleton Pattern, steps 34
Strategy Pattern 163
Strategy Pattern, advantages 168
Strategy Pattern, applications
Authentication Strategies 167
Form Validation 167
Payment Processing 168
Routing Strategies 168
Search Algorithms 168
Sorting Algorithms 167
Strategy Pattern, architecture 163
Strategy Pattern, disadvantages 168
Strategy Pattern, implementing 163, 164
Strategy Pattern, scenarios 164-167
Structural Design Patterns 68
Structural Design Patterns, attributes
Classes Organization 71
Code Reusability 71
Design Problems 72
Flexibility/Adaptability 72
Object Composition 71
Structural Design Patterns, sections
Adapter Pattern 115
Bridge Pattern 115
Composite Pattern 115
Decorator Pattern 116
Facade Pattern 116
Flyweight Pattern 116
Proxy Pattern 116
Synchronous JavaScript 172-174
Synchronous JavaScript, challenges
Blocking Nature 174
Browser Compatibility 175
Concurrency/Stability 175
Network Operations 175
User Experience 175
T
Testing 338
Testing, role
Boost Confidence 339
CI/CD 339
Code Quality, ensuring 339
Collaboration, improving 339
Facilitates Refactoring 339
Maintainability, enhancing 339
Testing, types
End-to-End 339
Integration 339
Unit 338
this keyword 394
this keyword, scenarios 394-396
Throttling 191
Throttling, advantages 195
Throttling, disadvantages 195
Throttling, resources
Applications 194
Implementation 192, 193
Input Events 194
Network Requests 194
Polling 194
Problem Statement 191
Resize Events 194
Scrolling Events 194
UI Updates 194
U
Unit Testing 339, 340
Unit Testing, pattern
Adapter 349, 350
Builder 350
CoR 355-357
Decorator 347, 348
Factory 341, 342
Mediator 353, 354
Observer 343, 344
Prototype 352
Singleton 340, 341
Strategy 345-347
Unit Testing, section
Adapter Pattern 358
Builder 359
CoR 359
Decorator 358
Factory 358
Mediator 359
Observer 358
Prototype 359
Singleton 357
Strategy 358
V
Variable Hosting 390
Variable Hosting, pitfalls
Accidental Global Variables 390, 391
Scope Binding 393
Unexpected Behavior 392
Vue 262
Vue, architecture 263
Vue, features
Custom Directives/Mixins 269, 270
SFCs 264
State Management 267, 268
Vue, issues
Code Reusability 263
DOM Manipulation 262
Scalability 263
Vue, pattern
Computed Properties 272, 273
Dynamic Load Components 276
Scoped Styles 275
Vue, ways
Factory Pattern 278
Observer Pattern 278
Singleton Pattern 277
W
Web APIs 186
Web APIs, fundamentals
Canvas API 187
DOM 186
Fetch API 186
Geolocation API 186
Web Audio API 187
Web Storage 186
XMLHttpRequest (XHR) 186
Web Architect 311
Web Architect, role
Integration/Communication 312
Maintainability/Futureproofing 312
Security Consideration 312
Technology Selection 312
Web Architecture Blueprint 312
Web Architecture 310
Web Architecture, elements
Components 310
Scalability Performance 311
Security 311
Technologies 311
Tiers 311
Web Workers 201
Web Workers, advantages 205, 206
Web Workers, configuring 201-205
Web Workers, disadvantages 206
Web Workers, scenarios
Intensive Calculations 205
Large Data Processing 205
Real-Time Data, visualizing 205
Web-Based Games 205