This content originally appeared on Level Up Coding - Medium and was authored by Hayk Simonyan
What are Design Patterns?
Design patterns are repeatable solutions to commonly occurring problems in software design.
They are like templates or blueprints that can be used to solve specific software design problems by providing a standard way of building software.
The pattern is not a specific piece of code, so you can’t just find a pattern and copy it into your program, but you can follow the pattern details and implement a solution that suits your own program.
This saves time and effort and also results in more maintainable and scalable code.
Design Pattern Categories
Design patterns can be categorized by their intent and divided into three groups
- Creational patterns — used to create objects in a way that provides more flexibility and abstraction
- Structural patterns — used to define the structure of objects and classes
- Behavioral patterns — used to manage communication between objects and classes
Popular Design Patterns
There are some design patterns that are the most popular and are worth knowing. Let’s start with creational design patterns.
Creational Design Patterns
1. Singleton Pattern
Singleton is a creational design pattern that lets you ensure that a class has only one instance while providing a global access point to this instance.
Here’s how it works: imagine that you created an object, but after a while decided to create a new one. Instead of receiving a fresh object, you’ll get the one you already created.
Example
Suppose you are creating a database connection for a web application, and you want to ensure that there is only one instance of the database object throughout the entire application. You can use the singleton pattern to achieve this:
class Database {
private static instance: Database | null = null;
private constructor() {}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
public query(sql: string): void {
console.log(`Executing SQL: ${sql}`);
// Execute query logic
}
}
const database1 = Database.getInstance();
const database2 = Database.getInstance();
console.log(database1 === database2); // Output: true
database1.query('SELECT * FROM users');
database2.query('SELECT * FROM users');
In this case, the Database class has a private constructor and a static getInstance() method that returns the instance of the class. It also has a query(sql) method which executes the SQL query on the database
We can create multiple instances of the Database class using the getInstance() method and then verify that they are the same instance using the equality operator. Since both instances are created using the same method, they are the same instance, and the output of the equality check is true.
After that, we can use the query() method on both instances of the Database class, which would execute the same SQL query logic on the shared instance of the class.
In this way, we can ensure that there is only one instance of the Database class throughout the entire application.
2. Factory Pattern
The Factory method is a creational design pattern that provides a way to encapsulate object creation in a separate object that is responsible for creating instances of other objects.
This pattern is used to create objects without specifying the exact class of object that will be created.
Example
Imagine a logistics company, where we can have Trucks and Ships which deliver the orders.
interface Transport {
move(): void;
}
class Truck implements Transport {
move(): void {
console.log("Driving a truck");
}
}
class Ship implements Transport {
move(): void {
console.log("Driving a ship");
}
}
enum TransportType {
Truck,
Ship,
}
class TransportFactory {
static createTransport(type: TransportType): Transport {
switch (type) {
case TransportType.Truck:
return new Truck();
case TransportType.Ship:
return new Ship();
default:
throw new Error("Invalid vehicle type");
}
}
}
const truck = TransportFactory.createTransport(TransportType.Truck);
truck.move();
const ship = TransportFactory.createTransport(TransportType.Ship);
ship.move();
We can have a common Transport interface and both Truck and Ship classes will implement the Transport interface and define their own implementation of move() method
We also have an enum that defines the types of Transports that can be created.
The TransportFactory class is responsible for creating instances of Transport objects. It has a createTransport static method that takes a TransportType parameter and returns an instance of the corresponding implementation.
Clients of the factory can use this method to create new Transport objects without knowing the specific implementation details.
3. Dependency Injection
Dependency Injection is a programming design pattern that makes a class independent of its dependencies. It achieves that by separating object creation from object usage.
With Dependency Injection, classes are more focused on their core functionality, and they don’t have to worry about the details of how objects are created or configured. Instead, the objects are created and configured outside the class, and they are passed to the class as dependencies.
Many popular frameworks such as Angular, NestJS, and Spring use Dependency Injection as a core principle. By using Dependency Injection, these frameworks make it easier to manage complex applications with a large number of dependencies.
It improves the flexibility of the code and makes it easier to maintain.
Example
Imagine an application Logger class that has one method called log which simply logs a message to the console.
class Logger {
log(message: string) {
console.log(message);
}
}
class UserService {
private logger: Logger;
constructor(logger: Logger) {
this.logger = logger;
}
getUsers() {
this.logger.log('Getting users...');
// Get users logic
}
}
const logger = new Logger();
const userService = new UserService(logger);
userService.getUsers();
The UserService class has a private property called logger which is an instance of the Logger class. It also has a constructor which accepts an instance of the Logger class as an argument and assigns it to the logger property. We then create a method called getUsers which logs the message "Getting users..." using the log method of the Logger instance.
In the last few lines of code, a new instance of the Logger class is created and stored in the logger constant. Then, a new instance of the UserService class is created and passed the logger instance as an argument. Finally, the getUsers method of the userService instance is called, which logs the message "Getting users..." to the console using the log method of the Logger instance.
This demonstrates how the UserService class depends on the Logger class to log messages.
In this case, an instance of the Logger class is injected into the UserService instance's constructor using dependency injection.
Structural Design Patterns
Now let’s look at some popular structural design patterns
4. Proxy Pattern
The Proxy Pattern is a structural design pattern that allows us to create an intermediary object that acts as a proxy for a target object. The proxy can be used to add additional functionality to the target object, such as caching, logging, or security checks, without changing its interface (before or after forwarding the call to the target object)
Example
Let’s say we have an expensive operation, such as loading a large image, that we want to delay until it’s absolutely necessary. We can create a proxy object that acts as a placeholder for the real object and only loads the image when it’s actually needed. Here’s some sample code:
interface Image {
display(): void;
}
class RealImage implements Image {
private filename: string;
constructor(filename: string) {
this.filename = filename;
this.loadFromDisk();
}
display(): void {
console.log(`Displaying ${this.filename}`);
}
private loadFromDisk(): void {
console.log(`Loading ${this.filename} from disk`);
}
}
class ProxyImage implements Image {
private realImage: RealImage;
private filename: string;
constructor(filename: string) {
this.filename = filename;
}
display(): void {
if (!this.realImage) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
}
const image = new ProxyImage('largeImage1.png');
image.display(); // Output: Loading largeImage1.png from disk
image.display(); // Output: Displaying largeImage1.png (loaded from cache)
The RealImage class represents the actual image and has a private filename property. It loads the image from the disk in its constructor and implements the display() method to display the image.
The ProxyImage class acts as a placeholder for the real image and also has a filename property. When the display() method is called on a ProxyImage instance, it checks to see if the real image has already been loaded, and if not, it creates an instance of the RealImage class and calls its display() method.
The real image is only loaded on the first display() call, after that, it’s returned from the cache.
This way, we can delay the loading of the real image until it’s actually needed, which will improve the performance.
5. Facade Pattern
The next Structural pattern is the Facade pattern. It allows clients to interact with the system through a simplified interface, hiding the complexity of the underlying implementation. The main idea of the Facade pattern is to provide a higher-level interface that makes it easier to use a subsystem or a set of related classes.
Example
A simple example of the Facade pattern could be a Car class that can be started and stopped, a Radio class that can be turned on and off, and an AirConditioner class that can be turned on and off.
class Car {
start() {
console.log('Car started');
}
stop() {
console.log('Car stopped');
}
}
class Radio {
turnOn() {
console.log('Radio turned on');
}
turnOff() {
console.log('Radio turned off');
}
}
class AirConditioner {
turnOn() {
console.log('Air conditioner turned on');
}
turnOff() {
console.log('Air conditioner turned off');
}
}
class CarFacade {
private car: Car;
private radio: Radio;
private airConditioner: AirConditioner;
constructor() {
this.car = new Car();
this.radio = new Radio();
this.airConditioner = new AirConditioner();
}
start() {
this.car.start();
this.radio.turnOn();
this.airConditioner.turnOn();
}
stop() {
this.car.stop();
this.radio.turnOff();
this.airConditioner.turnOff();
}
}
// usage
const car = new CarFacade();
car.start(); // Car started, Radio turned on, Air conditioner turned on
car.stop(); // Car stopped, Radio turned off, Air conditioner turned off
6. Observer Pattern
This pattern allows an object (subject) to notify other objects (observers) when its state changes.
The object which notifies other objects is called a subject, and the other objects are called observers. It’s also known as the Publish/Subscribe pattern.
React uses a simplified version of the Observer pattern. This pattern allows components to subscribe to changes in state or props and re-render themselves when these changes occur.
Example
Imagine Twitter where someone can tweet and act as the subject, and the followers act as the observers.
Twitter notifies the followers of this person (observers) whenever a new tweet is published.
interface TwitterObserver {
tweetPosted(tweet: string): void;
}
class TwitterSubject {
private observers: TwitterObserver[] = [];
public addObserver(observer: TwitterObserver) {
this.observers.push(observer);
}
public postTweet(tweet: string) {
console.log(`Tweet Posted: "${tweet}"`);
this.observers.forEach((observer) => observer.tweetPosted(tweet));
}
}
class Follower implements TwitterObserver {
private username: string;
constructor(username: string) {
this.username = username;
}
public tweetPosted(tweet: string) {
console.log(`@${this.username} received a new tweet: "${tweet}"`);
}
}
const twitter = new TwitterSubject();
const john = new Follower("johndoe");
const jane = new Follower("janedoe");
twitter.addObserver(john);
twitter.addObserver(jane);
twitter.postTweet("Hello World!"); // Output: Tweet Posted: "Hello World!"
// @johndoe received a new tweet: "Hello World!"
// @janedoe received a new tweet: "Hello World!
In this case, the TwitterSubject is the object being observed, and the Follower is the observer. The Follower class implements the TwitterObserver interface, which requires the tweetPosted method to be implemented. The TwitterSubject maintains a list of observers and notifies them when a new tweet is posted via the postTweet method. The Follower instances are added as observers using the addObserver method.
7. Iterator Pattern
The third category is behavioral patterns.
The iterator pattern is an example of a behavioral design pattern. It allows you to iterate over the elements of a collection without knowing the underlying implementation.
The iterator pattern consists of two main components: the iterator and the aggregate. The iterator defines an interface for accessing and traversing the elements of the aggregate, while the aggregate defines the interface for creating an iterator.
Example
// Iterator object that generates a sequence of numbers
const numberIterator = {
[Symbol.iterator]() {
let current = 1;
return {
next() {
if (current <= 5) {
return { value: current++, done: false };
} else {
return { done: true };
}
}
};
}
};
// Iterate over the sequence of numbers using a for-of loop
for (let num of numberIterator) {
console.log(num);
}
Imagine implementing a for loop where we define an iterator object using the ES6 Symbol.iterator method. The iterator generates a sequence of numbers from 1 to 5. We can then use a for-of loop to iterate over the sequence and log each number to the console. The iterator pattern allows us to abstract away the details of generating the sequence and provide a simple interface for iterating over it.
Resources
Some of the examples and explanations in this article are from https://refactoring.guru, which is a great place to master your design pattern skills and dive deeper into specific design patterns which you want to learn further.
Thanks for reading and follow to not miss weekly developer content!
Level Up Coding
Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the Level Up Coding publication
- 💰 Free coding interview course ⇒ View Course
- 🔔 Follow us: Twitter | LinkedIn | Newsletter
🚀👉 Join the Level Up talent collective and find an amazing job
7 Design Patterns You Should Know was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Hayk Simonyan
Hayk Simonyan | Sciencx (2023-04-16T20:40:53+00:00) 7 Design Patterns You Should Know. Retrieved from https://www.scien.cx/2023/04/16/7-design-patterns-you-should-know/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.