Let’s Dive to Design Patterns

Şerifhan Işıklı
28 min readDec 6, 2024

--

I promise to be the most detailed resource on this platform.

1. Introduction

What are Design Patterns?

Design patterns are cliché solutions to challenges that architects of software come across. They are patterns and offer the prospect of fashioning a solution to problems in the design of software. These patterns can help the developers to make the design process faster for the development of new solutions, instead of completely new solutions every time.

Why Do We Need Design Patterns?

Software development can be complicated, and generating clean, maintainable code is never easy. The designers find design patterns useful because they provide ready-made solutions to recurring issues. They also make it easier for developers because everyone comprehends patterns in like manner. Further, using these design patterns leads to development of more resilient software the chances of an update or modification creating more problems are minimized.

Real-Life Example: Why Do We Need Design Patterns?

Consider, for instance that you are constructing a house.In the beginning, everyone wants to start trying to redesign the whole house — including thinking of how the walls, roof, the door, etc should be constructed. from scratch — coming up with new ways to build the walls, the roof, the doors, etc. However this undertaking may take a lot of time and is not so effective. Instead, architects and builders call upon standardized techniques and plans that have a tested track record of effectiveness. These common methods make work faster, less error-prone, and guarantee that the house will stand for years.

Likewise, in the software engineering, it is counterproductive as well as erroneous to try to start from scratch and design a solution to each problem. With design patterns we get ready to use solutions to solve common problems that can aid in the creation of sound and robust software. As it is logical to use standard construction methods to built a strong house, design patterns assist a person to develop highly reliable software.

For the purpose of our article, we can take the Logger class as the base, to which we will adapt different design models. This will be adjusted premised on many design factors The Electronic documentation will be developed based on many design principles. I wanted to keep it simple:

class Logger {
log(message) {
throw new Error("Subclasses should implement this method");
}
}

// Example of a Simple Logger that will be used for design patterns
class SimpleLogger extends Logger {
log(message) {
console.log(`Log: ${message}`);
}
}

First, let’s explain under which major headings we will group the design patterns.

1- Creational Patterns

Purpose of Creational Patterns

Creational patterns assist in creating objects in software in a more suitable manner. These patterns are not actually creating objects directly in many places but provides a intelligent way to create objects.

In general, the main notion is to maintain the object creation process isolated from other functional components. This way, if we have to bring a change in how objects are created then we do not require any other changes.

When making objects is difficult or requires extra conditions, creational patterns facilitate the work and enhancements the comprehensibility.

In short, these patterns:

Ease and make more flexible the process by which objects are created.
To keep code cleaner and more organized.
Limit interconnection between various sub systems of the system.
They assist us to develop software that it can be easily modified or enhanced without any effect on the other code.

What we will see in creational patterns;

Singleton,
Factory Methods,
Abstract Factory,
Prototype,
Builder,

1.a- Singleton Pattern

What is meant by Singleton Pattern?
The Singleton pattern assure that a class only has one instance, and that this instance is used throughout the application. It means that only one copy of the class is made and all components of the program have access to it or work with it.

When and Where to Use Singleton?

  • In a class, for a number of students which are many, you require only one object.
  • The class holds something which is common; such as a database connection or several settings.
  • More suits you don’t want many objects performing a similar service, which can lead to issues.

Example
Think of a logging system. You will only have one logger to be able to handle all the log messages. The logger objects can if there were many of them, log at the same time and result in creation of more errors. By means of the Singleton pattern, the whole program uses a single logger object, excluding these problems.

Real-Life Example: Singleton Pattern

If I tell you to think of a president of a country. In a country, one cannot be mistaken by referring to its president as being in another state or region; rather, there is the one person whom you are given the information is the president when you want to speak to him or her.
Regardless of the difference of the city or the place, you do not get to select and create a new president to open up a conversation with; you always convey with the same president. This is quite similar to the Singleton pattern where all the instantiation is done to only one object that is the president.

Singleton Pattern with Logger Class in Node.js

class Logger {
// Step 1: Create a private static variable to hold the single instance
static _instance = null;

// Step 2: Private constructor to prevent direct instantiation
constructor() {
if (Logger._instance) {
throw new Error("Logger is already initialized! This class is a singleton.");
}
Logger._instance = this;
this.logs = []; // This will store the log messages
}

// Step 3: Method to get the single instance of the class
static getInstance() {
if (!Logger._instance) {
new Logger(); // If no instance exists, create one
}
return Logger._instance;
}

// Method to log messages
log(message) {
this.logs.push(message);
console.log(`Log: ${message}`);
}

// Method to get all logs
getLogs() {
return this.logs;
}
}

// Example Usage
try {
const logger1 = Logger.getInstance();
logger1.log("This is the first log.");
logger1.log("This is the second log.");

console.log(logger1.getLogs()); // Output: ["This is the first log.", "This is the second log."]

// Trying to create another instance will raise an error
const logger2 = new Logger(); // Error: Logger is already initialized! This class is a singleton.
} catch (error) {
console.error(error.message);
}

Hints for Singleton Logger:

  • Private static instance: The _instance variable may store one object of the Logger class.
  • Private constructor: The constructor ensures that no other Logger object cannot be instantiated if one has been already created.
  • getInstance() method: This method includes a check and asks whether the Logger object is already created. If none, it creates one; if there is, it returns the one that is there.
  • Logging features: The log() method writes messages into an array and displays them into the console. The getLogs() method gives you an ability to view the saved messages.

Logger is created only once in the program.
The same Log object is governing all the log messages.
This maintains consistency and the fact that there shouldn’t be more than one Logger.

1.b- Factory Methods

What is the Factory Method Pattern ?

Factory Method pattern lets you create objects that you don’t really specify the class for. That offers a way to create objects employing a method, that allows subclasses or particular techniques to decide on how what they are making.

factory-methods

When is it Useful?

  • You have your main class which contains several smaller classes, and you need to allow a method to choose which of the smaller classes to work with.
  • You do not want to complicate the creation of new objects and you do not want to force the code to use specific type of classes.

When to Use the Factory Method

  • You do not know in advance, which object has to be created.
  • In your current set-up, you want to avoid having object-making scattered all over the place.
  • For the next level of comprehension, you want your code to be easily modifiable when new object types come up.

Real-Life Example
Suppose clients are ordering such a product as food that may be pizza, pasta, or salad in a restaurant. People do not have to be informed how it was prepared. The kitchen (factory) accept the order and determines what type of food to prepare from the order taken.

This is similar to the Factory Method pattern where the client code doesn’t need to know the exact type of object being created, only that it will receive a meal.

Factory Method Example Using Logger in Node.js

The Factory Method can be applied in order to make the Logger objects of a certain type, like ConsoleLogger, FileLogger or ErrorLogger. Each of these loggers will log messages in various ways but the client that use the logger does not have to necessarily have knowledge of the logger type being used.

Code Example:

// Base Logger class
class Logger {
log(message) {
throw new Error("Subclasses must implement this method.");
}
}

// ConsoleLogger subclass
class ConsoleLogger extends Logger {
log(message) {
console.log(`Console log: ${message}`);
}
}

// FileLogger subclass (simulated for this example)
class FileLogger extends Logger {
log(message) {
console.log(`File log: ${message}`); // Pretend we're logging to a file
}
}

// ErrorLogger subclass
class ErrorLogger extends Logger {
log(message) {
console.error(`Error log: ${message}`);
}
}

// Factory method to create loggers based on the type
class LoggerFactory {
static createLogger(type) {
if (type === "console") {
return new ConsoleLogger();
} else if (type === "file") {
return new FileLogger();
} else if (type === "error") {
return new ErrorLogger();
} else {
throw new Error("Unknown logger type.");
}
}
}

// Example Usage
const logger1 = LoggerFactory.createLogger("console");
logger1.log("This is a console log."); // Output: Console log: This is a console log.

const logger2 = LoggerFactory.createLogger("file");
logger2.log("This is a file log."); // Output: File log: This is a file log.

const logger3 = LoggerFactory.createLogger("error");
logger3.log("This is an error log."); // Output: Error log: This is an error log.

Some hints:

  • Base Class (Logger): This class has a main() method and is a main class for this lesson. Since every subclass should be its own smaller class, there is exactly this method somewhere in each of them.
  • Subclasses: This industry consists of three types of loggers:
    — ConsoleLogger logs in to the console.
    — FileLogger is a logging target that is used to save log in a file.
    — They include ErrorLogger which deals with error messages.
  • Factory Method (LoggerFactory.createLogger()): This method creates the right logger that is according to the type you may provide such as “console”, “file”, or “error”. The user doesn’t have to understand how the logger works; he simply tells it the type of logger he is interested in.

Real-Life Example:

So, let’s use the restaurant example again. LoggerFactory is the kitchen where the cook prepares a dish, pizza or pasta, depending on the customer’s order. The customer does not bother where or how the food is prepared — they demand it and it is served. Likewise, the LoggerFactory returns the correct Logger while also hiding how it is generated.

1.c- Abstract Factory

Abstract Factory pattern proposes a way of creating family of related or dependent objects without defining detailed class. Unlike other patterns that involve plain creation of objects from scratch, this pattern only details creation of object factories with base classes or interfaces of the created object archive.

The main concept is that a factory is also abstract and thus you can have concrete factories which create different groups of Objects. Every factory can have objects in its manufacturing process, which will complement each other.

abstract factory

Here you will discover knowledge about conditions where the Abstract Factory should actually be implemented in a software product.

When to Use the Abstract Factory

  • You need to define groups of similar things, for example, producers with an emphasis on how they work (for example, developers, producers).
  • They also wanted to make certain that the objects that are created are of similar structure or design.
  • They also want to switch from one family of objects to another without significant changes in code.

Real-Life Example: Abstract Factory Pattern

For example, such an innovative product may be related to a car manufacturing company. The company produces cars for different markets: electric cars and gas cars. In the electric cars category, they have the engines, fuel systems, while in the gas cars category, they have slight differences of those two components. The car factory synthesizes cars with similar family of components that include electric and other gaseous elements. The client merely requires to request for a type of car, and the factory still has to come up with the right engine and fuel system.

Therefore in this case the car factory symbolizes the Abstract Factory, and the concrete factories being factories that produce electric cars or gas cars.

Abstract Factory Example Using Logger in Node.js

I want utilize the Abstract Factory to build the different families of Logger like DevelopmentLogger and ProductionLogger which contain different loggers as console, file and error logger based on the environment.

Code Example:

// Base Logger class
class Logger {
log(message) {
throw new Error("Subclasses must implement this method.");
}
}

// ConsoleLogger subclass
class ConsoleLogger extends Logger {
log(message) {
console.log(`Console log: ${message}`);
}
}

// FileLogger subclass
class FileLogger extends Logger {
log(message) {
console.log(`File log: ${message}`); // Simulating file logging
}
}

// ErrorLogger subclass
class ErrorLogger extends Logger {
log(message) {
console.error(`Error log: ${message}`);
}
}

// Abstract factory interface
class LoggerFactory {
createConsoleLogger() {
throw new Error("This method must be implemented in a subclass.");
}

createFileLogger() {
throw new Error("This method must be implemented in a subclass.");
}

createErrorLogger() {
throw new Error("This method must be implemented in a subclass.");
}
}

// Concrete factory for development loggers
class DevelopmentLoggerFactory extends LoggerFactory {
createConsoleLogger() {
return new ConsoleLogger();
}

createFileLogger() {
return new FileLogger(); // File logger in development logs to console for simplicity
}

createErrorLogger() {
return new ErrorLogger();
}
}

// Concrete factory for production loggers
class ProductionLoggerFactory extends LoggerFactory {
createConsoleLogger() {
return new ConsoleLogger(); // Console logs in production can be minimal
}

createFileLogger() {
return new FileLogger(); // File logger in production logs to actual file
}

createErrorLogger() {
return new ErrorLogger(); // Errors should still go to error logger
}
}

// Client code to use the Abstract Factory
function logMessages(loggerFactory) {
const consoleLogger = loggerFactory.createConsoleLogger();
const fileLogger = loggerFactory.createFileLogger();
const errorLogger = loggerFactory.createErrorLogger();

consoleLogger.log("This is a console log.");
fileLogger.log("This is a file log.");
errorLogger.log("This is an error log.");
}

// Example Usage

// Using the Development Logger Factory
const devLoggerFactory = new DevelopmentLoggerFactory();
logMessages(devLoggerFactory);

// Using the Production Logger Factory
const prodLoggerFactory = new ProductionLoggerFactory();
logMessages(prodLoggerFactory);

Some hints :

  • Base Class (Logger): We have the abstract Logger class which contains log() method for logging.
  • Concrete Loggers: We have certain concrete logger classes: ConsoleLogger, FileLogger and ErrorLogger, which though, implement the log() method in the different ways.
  • Abstract Factory (LoggerFactory): While this class provides instantiation definitions for various types of loggers, details of actual implementation are not included here.
  • Concrete Factories (DevelopmentLoggerFactory and ProductionLoggerFactory): These classes actually provide the loggers whose creation the abstract factory method is supposed to resolve according to some environment select (development or production).
    Such a logging to the console could be at the level of the development factory for all loggers.
    The production factory can write to actual files for file logs, and more restricted console logs.

Real-Life Analogy:

In the metaphor of car manufacturing, the DevelopmentLoggerFactory is a factory that produces cars that have electric engines and parts, the ProductionLoggerFactory is a factory for making cars with gas engines. While both factories assemble different components that are related (an electric or gas engine for a car, development or production for a loggers in our case).

1.d- Prototype Pattern

prototype pattern

Prototype Pattern is an object creation through the copying of an existing object (the prototype). Instead of instantiating them from scratch your copies or duplicate a prototype object. This pattern helps you to create objects efficiently where object creation is complicated or costly.

When to Use the Prototype Pattern?

  • Generation of an object is computationally expensive while duplication of an existing object is more desirable.
  • You do not want to clone objects in order to create object hierarchies.
  • You have to make sure that new objects are created already in the prototype state.

Real-Life Example: Prototype Pattern

Suppose you are planning a party, and you require preparing messages to extend invitations to each guest. Whereas, when making invitations, everyone gets an invitation that is uniquely created for him or her, they use a predesigned model (the prototype). Then you just copy the format and insert each guest’s name into the list. This is time-saving strategy and secludes for achieving uniformity.

Prototype Design Pattern Example Using Logger in Node.js

We’ll use a Logger which is cloneable so as to create other instances which are configured in the same way.

Code Example:

// Base Logger class with clone functionality
class Logger {
constructor(logLevel = "info") {
this.logLevel = logLevel; // Default log level
this.logs = []; // Store logs
}

// Method to log messages
log(message) {
this.logs.push(`[${this.logLevel}] ${message}`);
console.log(`[${this.logLevel}] ${message}`);
}

// Clone method to create a copy of the logger
clone() {
const clone = new Logger(this.logLevel);
clone.logs = [...this.logs]; // Copy logs to the new instance
return clone;
}
}

// Example Usage
const mainLogger = new Logger("debug"); // Create a logger with debug level
mainLogger.log("This is a debug log."); // [debug] This is a debug log.

const clonedLogger = mainLogger.clone(); // Clone the main logger
clonedLogger.logLevel = "error"; // Change log level for the cloned logger
clonedLogger.log("This is an error log."); // [error] This is an error log.

// Original logger remains unchanged
mainLogger.log("Another debug log."); // [debug] Another debug log.

// Display logs from both loggers
console.log("Main Logger Logs:", mainLogger.logs);
console.log("Cloned Logger Logs:", clonedLogger.logs);

Some hints:

  • Base Class (Logger): The Logger class is the prototype of the objects/b Covers. This class has fields logLevel and logs , and methods log() and clone().
  • Clone Method: The clone() method has been used to create a new Logger instance with same logLevel and old messages are copied to new Logger instance. This guarantees that the new object should be in the state of the older one.
  • Example Usage: mainLogger is the key example in object-oriented programming (the first instance of the given class, or a prototype type thereof).
    clonedLogger is also created from the mainLogger but with a different state such as a different log level.

Real-Life Analogy:
To use the analogy of the party invitation, it fits here well. This mainLogger is much like the invitation template. Every clonedLogger is a copy of an invitation with some change (for instance the guest’s name). In using the template one is ensured of consistency and through cloning the process is much more effective than having to construct each invitation.

2. Structural Patterns

What are Structural Patterns?

Structural patterns are concerned with how individual and groups of objects and classes come together to make up larger structures, while at the same time, keeping the system open and scalable. These patterns facilitate the arrangement of relations between entities and can contribute to the simplicity of the system’s whole architecture and its maintenance.

The key idea is to provide descriptively named interfaces to individual components that impose an invasively prescribed architecture of how the abstractions should interact when ultimately compiled together with other parts of the greater program.

As mentioned earlier, the rationale behind Structural Patterns is to deal with the complexity rises from the size and interaction between the components within a system.

Purpose of Structural Patterns

  • Simplify Relationships: Relationship patterns make it easier to work with relations that link object or class, especially in cases of complicated hierarchies or systems.
  • Encourage Reusability: As classes of reusable structures, these patterns decrease unnecessary code similarity and enhance the code’s maintainability.
  • Increase Flexibility: Solutions introduced using structural patterns offer means to add new features or change the behavior of a system without intervening in the existing code.
  • Promote Separation of Concerns: It contributes to the division of the work load of classes and objects and aids in making the code more comprehensible.

What we will see in Structural patterns;

Adapter,
Decorator,
Proxy,

2.a- Adapter Pattern

What is the Adapter Pattern?

In concept, the Adapter Pattern serves to link the incompatible interfaces. It enable two or more classes or objects sharing one thing, method or behavior without modifying one’s source codes. An adapter takes an interface of one class and transform it into another that a client requires.

When to use the Adapter Pattern?

adapter-pattern
  • You will need to use an existing class but it has a different interface with what you require.
  • There has been newly developed code added to the system and now is the time to blend third party/library or old code into the system.
  • There is no way you want to have workability between two incompatible systems.

Real-Life Example: Adapter Pattern

Suppose you have a power socket that supplies power to specific attributes say electrical power in the European format, 220v power while your phone charger requires power in the American format, 110v power. Instead of trying to change the shape of the socket or the charger, you connect a power adapter that changes 220V into 110V on the other end.

Similarly in software, the Adapter Pattern changes the interface of one form to the interface of another to fit it in.

Adapter Example Using Logger in Node.js

For the sake of the example let’s give it a name Logger for which you have a log(message) method and there is a third party library log4js that uses instead writeLog(message). We decided not to mutate the Logger or third-party library; we will add an adapter that we develop.

Code Example:

// Existing Logger class
class Logger {
log(message) {
console.log(`Log: ${message}`);
}
}

// Third-party logger with a different interface
class ThirdPartyLogger {
writeLog(message) {
console.log(`Third-Party Log: ${message}`);
}
}

// Adapter to make ThirdPartyLogger compatible with Logger
class LoggerAdapter {
constructor(thirdPartyLogger) {
this.thirdPartyLogger = thirdPartyLogger;
}

log(message) {
// Adapts the call to the third-party method
this.thirdPartyLogger.writeLog(message);
}
}

// Client code using the Logger interface
function logMessages(logger) {
logger.log("This is a standard log.");
}

// Example Usage

// Using the standard Logger
const standardLogger = new Logger();
logMessages(standardLogger); // Log: This is a standard log.

// Using the ThirdPartyLogger with the adapter
const thirdPartyLogger = new ThirdPartyLogger();
const adaptedLogger = new LoggerAdapter(thirdPartyLogger);
logMessages(adaptedLogger); // Third-Party Log: This is a standard log.

Some hints:

  • Existing Logger: Standard log(message) method is provided with the help of Logger class.
  • Third-Party Logger: ThirdPartyLogger class implies the method writeLog(message) which is against the Logger class and interface.
  • Adapter (LoggerAdapter): I simplify the ThirdPartyLogger class and take the writeLog() method and adjust it to be named log(), which is what is expected of a class that is a client to it.
  • Client Code: Through the adapter, the logMessages() function is compatible with the original logger as well as third-party logger.

Real-Life Analogy

Let’s dream of the adapter as being similar to a power cord converter. It makes the usage of a piece of equipment developed to plug into one kind of socket (for instance, your telephone charger), capable of being connected to an entirely different type of socket (such as 220V type as opposed to the 110V). In the same way, the LoggerAdapter allows two incompatible logging systems work together at the same time without changing their core code.

2.b- Decorator Pattern

So what is the Decorator Pattern?

The Decorator Pattern can help add new functionality to an object, while remaining independent of both the structure and the code of the object’s core implementation. It employs the composition mechanism instead of the inheritance mechanism; the decorator object surrounds the wrapped objects.

When to use the Decorator Pattern?

decorator — pattern
  • You have to work in a situation where you want to add more and more features to an object but do not wish to change the original class of that object.
  • You need to apply behaviors or features during runtime of your application.
  • You do not want to deal with the creation of many subclasses to accommodate each and every feature combination.

Real-Life Example: Decorator Pattern

Think about a coffee shop. Your basic object is a coffee, say, an espresso. You can add other additional features such as milk, sugar, or whipped cream to it. Rather than making a new class for each combination as we have seen in the CoffeeWithMilkAndSugar for example, what is done is one adds the required extras as ‘layers’ to the base coffee object.

Decorator Example Using Logger in Node.js

We’ll extend the functionality of the Logger class by adding decorators to:

Timestamp each log.
Record the logs to a file (using an array as a precursor to a file).
Transform the logs to uppercase to make our outputs look more emphatic.

Code Example:

// Base Logger class
class Logger {
log(message) {
console.log(`Log: ${message}`);
}
}

// Base Decorator class (implements the same interface)
class LoggerDecorator {
constructor(logger) {
this.logger = logger;
}

log(message) {
this.logger.log(message);
}
}

// TimestampDecorator adds a timestamp to each log
class TimestampDecorator extends LoggerDecorator {
log(message) {
const timestamp = new Date().toISOString();
super.log(`[${timestamp}] ${message}`);
}
}

// UppercaseDecorator converts logs to uppercase
class UppercaseDecorator extends LoggerDecorator {
log(message) {
super.log(message.toUpperCase());
}
}

// FileLoggerDecorator writes logs to a "file" (simulated with an array)
class FileLoggerDecorator extends LoggerDecorator {
constructor(logger) {
super(logger);
this.logs = []; // Simulating a file
}

log(message) {
this.logs.push(message);
super.log(message);
}

getLogs() {
return this.logs;
}
}

// Example Usage
const baseLogger = new Logger();
const timestampLogger = new TimestampDecorator(baseLogger);
const uppercaseLogger = new UppercaseDecorator(timestampLogger);
const fileLogger = new FileLoggerDecorator(uppercaseLogger);

// Log messages using the fully decorated logger
fileLogger.log("This is a test log.");
fileLogger.log("Another log message.");

// Display logs stored in the "file"
console.log("Logs in file:", fileLogger.getLogs());

Some hints

  • Base Logger: There is only the base log(message) method in the Logger class.
  • Decorator Classes:
    -
    LoggerDecorator
    : The base decorator class contains only the constructor and passes all the calls to the Logger.
    - TimestampDecorator: Aggregates the provided log message with a timestamp.
    - UppercaseDecorator: Transforms the log message to the log message in uppercase letters.
    - FileLoggerDecorator: Logs data into a file (array) mimicing real writing.
    - Dynamic Decoration: This means the decorators themselves can be stacked, which gives you the ability to create the logger behavior you want in any combination.

Real-Life Analogy
The coffee example fits perfectly:

  • Base Logger: Espresso (minimum specifications).
  • Timestamp Decorator: Add milk (new behavior).
  • Uppercase Decorator: Add sugar (another behavior).
  • File Logger Decorator: Finally layer the whipped cream.

Nobody tweaks the espresso but every decorator, blesses the structure with an added feature. They may be used freely together to construct the ideal coffee (or logger)!

2.c- Proxy Pattern

What is the Proxy Pattern?

The Proxy Pattern offers an object that takes on the role of standing in for another object and regulate access to it. It behaves as a middleman between the existing client and the real object, which reduces direct access on the object to a certain extent. This control can be in the form of access control, where intelligent objects are initialized only on first use, logging or caching.

When the Proxy Pattern Should Be Used?

proxy pattern
  • You need to protect the object for some reason (security, authentication…).
  • That is, you have to insert an extra layer of functionality such as logging or caching into an object without changing it.
  • The actual object is costly to build up and you would like to postponed its creation as long as possible (lazy initialization).

Real-Life Example: Proxy Pattern

Think of a security guard standing at the door of a store. The guard acts as a proxy for the building:

  • They ensure whether or not you have permission to go in.
  • They log your entry time.
  • They guarantee that only the right personnel is allowed to enter the compound.
  • In resource terms, just as in software, a proxy regulates access and can insert processing before communicating with the actual resource.

Proxy Example Using Logger in Node.js

We shall use a Proxy for the Logger class to incorporate access control and to log method calls while using Logger without altering it.

Code Example:

// Base Logger class
class Logger {
log(message) {
console.log(`Log: ${message}`);
}
}

// Proxy class for Logger
class LoggerProxy {
constructor(logger, userRole) {
this.logger = logger; // Reference to the actual logger
this.userRole = userRole; // User role to control access
}

log(message) {
if (this.userRole !== "admin") {
console.log("Access denied: Only admins can log messages.");
return;
}

// Log the access
console.log(`[Proxy] Logging action performed: "${message}"`);

// Forward the call to the real logger
this.logger.log(message);
}
}

// Example Usage

// Create a real Logger instance
const realLogger = new Logger();

// Create a Proxy for the Logger with different user roles
const adminLoggerProxy = new LoggerProxy(realLogger, "admin");
const guestLoggerProxy = new LoggerProxy(realLogger, "guest");

// Logging with admin access
adminLoggerProxy.log("This is an admin log."); // Allowed: Logs the message
// [Proxy] Logging action performed: "This is an admin log."
// Log: This is an admin log.

// Logging with guest access
guestLoggerProxy.log("This is a guest log."); // Denied
// Access denied: Only admins can log messages.

Some hint

  • Base Logger: This Logger class offers the main functionality of the plug-in to log messages.
  • Proxy Class: Then LoggerProxy class manages the access to Logger. It verifies the user and gets the task done on the actual logger while documenting the process at the same time.
  • Client Code:
    - If the logged name is equal to “admin”, the proxy permits the request and passes the log request to the true logger.
    - If the user role is not admin then it blacklists the origin.

Real-Life Analogy
The security guard analogy works here:

  • Logger: The building (real object).
  • LoggerProxy: The security guard (proxy).
  • The main task of a guard is to inspect your status (employee, employer, guest, etc.) and then allow entry or not.

3. Behavioral Patterns

Behavioral patterns, therefore, refer to the manner in which objects within a system reveal how they can be used and how they can actually work. The objects assist in the coordination of responsibilities between objects, and work to reduce fleets of work and essential interactions into understandable rules. These patterns make for object usage that is more flexible and sustainable at the same time.

Not the feeble attempt to win allies, but the relentless objectification of women, objectification of violence and self-objectification were the identified behavioral patterns.

Purpose of Behavioral Patterns

  • Simplify Communication: Set up proper and effective interaction between the objects or classes.
  • Improve Flexibility: Supportspecification of how objects are to interact without the.
  • Encourage Reusability: Through this kind of decoupling, the objects of the plan can be reused in different contexts while there is no direct call between the plan and its objects.
  • Streamline Complex Processes: Organise work and functions that are less complex and difficult to operate within establishing smooth processes for procedure.

What we will see in Behavioral patterns;

Observer,
Strategy,
Command,

3.a- Observer Pattern

What is the Observer Pattern?

The Observer Pattern is a behavioral pattern that lets a number of observers get information about the state of an object (called subject). It aids in the development of a one to many dependency between the subject object and observer objects, so that whenever the subject object is changed in any way, then the observer objects are automatically notified.

When to Use It?

  • On the other hand, you want to broadcast status change of a subject to many objects.
  • In a system, one can realize the need to maintain object consistency.
  • Ideally, you want the subject of observation to be separated from the observers in order that it may be modified.

Real-Life Example: Observer Pattern
Think about social media notifications:

  • In case you post something to your forum, all the followers (observers) of this post receive an alert automatically.
  • Hence the subject (you) doesn’t need to know how these notifications are delivered; it simply broadcasts the event.

Observer Example Using Logger in Node.js

We will improve the Logger class to let developers or monitoring services know about the logs they create.

Code Example:

// Subject: Logger class with observer support
class Logger {
constructor() {
this.observers = []; // List of observers
}

// Add an observer
addObserver(observer) {
this.observers.push(observer);
}

// Remove an observer
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}

// Notify all observers
notifyObservers(message) {
this.observers.forEach(observer => observer.update(message));
}

// Log a message and notify observers
log(message) {
console.log(`Log: ${message}`);
this.notifyObservers(message);
}
}

// Observer: Base observer class
class Observer {
update(message) {
// To be implemented by concrete observers
}
}

// Concrete observer 1: Developer notification
class DeveloperObserver extends Observer {
update(message) {
console.log(`Developer notified: ${message}`);
}
}

// Concrete observer 2: Monitoring system
class MonitoringObserver extends Observer {
update(message) {
console.log(`Monitoring system logged: ${message}`);
}
}

// Example Usage

// Create the Logger (subject)
const logger = new Logger();

// Create observers
const devObserver = new DeveloperObserver();
const monitoringObserver = new MonitoringObserver();

// Register observers
logger.addObserver(devObserver);
logger.addObserver(monitoringObserver);

// Log messages
logger.log("Application started.");
// Log: Application started.
// Developer notified: Application started.
// Monitoring system logged: Application started.

logger.log("An error occurred.");
// Log: An error occurred.
// Developer notified: An error occurred.
// Monitoring system logged: An error occurred.

Some hints:

  • Logger (Subject): Observers are also kept in a list by Logger and are informed when a log is created.
  • Observers:
    - DeveloperObserver generate notification for developers.
    - MonitoringObserver writes for the purposes of a monitoring system.
  • Dynamic Updates: As a result, observers, which represent one of the pattern’s key components, can be more or less at any time of the program’s execution

Real-Life Analogy
Think of YouTube subscriptions:

  • Subject: YouTube channels have a new video uploaded.
  • Observers: Subscribers read a notification.
  • If a subscriber is removed, he is no longer notified, while the system will continue to function for the rest.

3.b - Strategy Pattern

What is the Strategy Pattern?

This is because the Strategy Pattern lets you define the set of related algorithms (strategies), wraps every one of them, and can swap them easily. It enables the selection of the right behavior or algorithm for use at the time of execution without changing the objects that will have them.

When to Use It?

  • You need to apply a certain operation (for example, sort, log) in several ways and you should be able to switch between them easily.
  • You don’t want your method to be full of if-else or switch-case construction — this is ugly.
  • Algorithms should be encapsulated independently as you add new ones so that the encapulation won’t affect any other part of the code.
strategy-pattern

Real-Life Example: Strategy Pattern
Imagine a navigation app:

  • The app offers multiple strategies for travel: driving, cycling, or walking.
  • Depending upon the selected strategy the application finds out the route.
  • Each strategy is independent but serves the same purpose: finding a route.

An example of a strategy is a Logger in Node.js

Let’s add more functionality in Logger class to provide the different strategies of logging. For example, we can write to the console, file or database instantly depending on the selected strategy.

Code Example:

// Strategy Interface
class LoggingStrategy {
log(message) {
throw new Error("log() method must be implemented");
}
}

// Concrete Strategy 1: Console Logging
class ConsoleLoggingStrategy extends LoggingStrategy {
log(message) {
console.log(`Console Log: ${message}`);
}
}

// Concrete Strategy 2: File Logging (simulated with an array)
class FileLoggingStrategy extends LoggingStrategy {
constructor() {
super();
this.logs = [];
}

log(message) {
this.logs.push(message); // Simulate writing to a file
console.log(`File Log: ${message}`);
}

getLogs() {
return this.logs;
}
}

// Concrete Strategy 3: Database Logging (simulated)
class DatabaseLoggingStrategy extends LoggingStrategy {
log(message) {
console.log(`Database Log: ${message}`); // Simulate database logging
}
}

// Context: Logger class that uses a logging strategy
class Logger {
constructor(strategy) {
this.strategy = strategy; // Default strategy
}

// Set a new strategy dynamically
setStrategy(strategy) {
this.strategy = strategy;
}

// Log using the current strategy
log(message) {
this.strategy.log(message);
}
}

// Example Usage

// Create logging strategies
const consoleStrategy = new ConsoleLoggingStrategy();
const fileStrategy = new FileLoggingStrategy();
const dbStrategy = new DatabaseLoggingStrategy();

// Create a Logger with an initial strategy
const logger = new Logger(consoleStrategy);

// Log messages using the console strategy
logger.log("Logging to console");
// Console Log: Logging to console

// Switch to file logging
logger.setStrategy(fileStrategy);
logger.log("Logging to file");
// File Log: Logging to file

// Check file logs
console.log("File Logs:", fileStrategy.getLogs());
// File Logs: [ 'Logging to file' ]

// Switch to database logging
logger.setStrategy(dbStrategy);
logger.log("Logging to database");
// Database Log: Logging to database

Some hints:

  • Strategies:
    - ConsoleLoggingStrategy
    is used to write out log messages to the character device, console.
    - MockFileLoggingStrategy writes log like they are going to be written to a file.
    - DatabaseLoggingStrategy stands as a mock database log storage.
  • Logger (Context): The Logger class changes the current working strategy by using a setStrategy method.
  • Flexibility: Implementing a fresh logging technique means procreating a new strategy class which does not affect the Logger.

Real-Life Analogy
Think of a payment system:

Options could be credit card, PayPal or via cryptocurrency.
The user chooses a type of a payment and the system conducts the payment according to the chosen scenario.
It is useful since moving between methods does not involve modification in the system’s fundamental process.

3.c- Command Pattern

What is the Command Pattern?

The Command Pattern condenses a request into an object so that it can be passed around and varied, pending time and placing, logged, and even revoked. It de-links the sender of a request from the taker by making the management of commands flexible.

When can the Command Pattern be used?
Use the Command Pattern when:

  • You have to control your operations or requests more, for instance, to support undo, redo, and actions logging.
  • You have to en-queue or prioritize tasks.
  • You’d like to disentangle the invoker (request originator) from the receiver (request processor).
command-pattern

Real-Life Example: Command Pattern
Imagine a remote control for a TV:

  • Every button (command) does a particular function, for example, when you want to switch on the television or switch it off.
  • The sender or the remote control does not have to understand how a TV operates, all it has to do is to issue commands.
  • There are also command queuing (e.g., on, HDMI).

Command example using logger in Node.js

Now we will create commands to put in commands to control the log activities such as log message, enable logging, disable logging and so on and a command manager class to handle these commands.

Code Example:

// Receiver: Logger class
class Logger {
log(message) {
console.log(`Log: ${message}`);
}

enableLogging() {
console.log("Logging enabled.");
}

disableLogging() {
console.log("Logging disabled.");
}
}

// Command Interface
class Command {
execute() {
throw new Error("execute() method must be implemented");
}
}

// Concrete Command 1: Log Command
class LogCommand extends Command {
constructor(logger, message) {
super();
this.logger = logger;
this.message = message;
}

execute() {
this.logger.log(this.message);
}
}

// Concrete Command 2: Enable Logging Command
class EnableLoggingCommand extends Command {
constructor(logger) {
super();
this.logger = logger;
}

execute() {
this.logger.enableLogging();
}
}

// Concrete Command 3: Disable Logging Command
class DisableLoggingCommand extends Command {
constructor(logger) {
super();
this.logger = logger;
}

execute() {
this.logger.disableLogging();
}
}

// Invoker: Command Manager
class CommandManager {
constructor() {
this.commandQueue = [];
}

addCommand(command) {
this.commandQueue.push(command);
}

executeCommands() {
while (this.commandQueue.length > 0) {
const command = this.commandQueue.shift();
command.execute();
}
}
}

// Example Usage

// Create a Logger (receiver)
const logger = new Logger();

// Create commands
const logCommand = new LogCommand(logger, "This is a log message.");
const enableLoggingCommand = new EnableLoggingCommand(logger);
const disableLoggingCommand = new DisableLoggingCommand(logger);

// Create a command manager (invoker)
const commandManager = new CommandManager();

// Add commands to the queue
commandManager.addCommand(enableLoggingCommand);
commandManager.addCommand(logCommand);
commandManager.addCommand(disableLoggingCommand);

// Execute all commands
commandManager.executeCommands();
// Logging enabled.
// Log: This is a log message.
// Logging disabled.

Some hints:

  • Receiver: The Logger class performs the operations (such as logging or enabling and disabling logging).
  • Commands: In its simplest terms, each of them encapsulates a single operation such as logging a message.
  • Invoker (Command Manager): The CommandManager implements and coordinates commands, it offers flexibility in the tasks’ organization.

Real-Life Analogy
Think of a smart home system:

  • Receiver: Lamps, fans, air conditioning systems, alarms perform actions.
  • Commands: Switch the lights on, heat or cool the room and set off the alarm.
  • Invoker: A central controller (or an app) schedules commands to be sent to devices, making certain that actions take place in the right sequence.

Thanks….

And…

--

--

Şerifhan Işıklı
Şerifhan Işıklı

Written by Şerifhan Işıklı

Senior Software Engineer @Dogus Teknoloji.

No responses yet