Open-Closed Principle: Extending Your Code Without Modification

Reza Erami
3 min readOct 6, 2023

--

The Open-Closed Principle states that objects or entities should be open for extension but closed for modification. This means they should be extendable without altering their core implementation. This principle can be applied in Object-Oriented Programming (OOP) by creating new classes that extend the original class and override its methods, rather than modifying the original class directly.

In functional programming, this can be achieved through the use of function wrappers, where you can call the original function and apply new functionality to it without changing the original function itself.

The decorator design pattern is also a useful tool for adhering to this design principle. With decorators, you can attach new responsibilities or behaviors to objects without modifying their source code, thus keeping them closed for modification and open for extension.

In the example below, we are going to expand the “updateAvatar” method to include validation, preventing any non-image extensions from being uploaded. Let’s begin by modifying the “updateAvatar” method first.

public async updateAvatar(id: number, file: any) {
try {
// Get the file extension
const fileExtension = this.fileService.getFileExtension(file);

// Check if it's JPG, throw an error if not
if (["jpg", "png"].includes(fileExtension.toLowerCase())) {
throw new Error('Unsupported avatar format. Only image is allowed.');
}

const avatarPath = await this.fileService.upload(file);
await this.update(id, {avatar: avatarPath});
console.log('Avatar updated successfully.');
} catch (error) {
console.error('Avatar update failed:', error);
}
}

As you can see, the code above violates the Open-Closed Principle (OCP), which is the second principle of SOLID, as it requires us to modify our code if we want to add more extensions. Now, let’s refactor this code to adhere to the OCP.

  1. We will create a file validator where we can pass a file and the expected extension.
  2. Then, our validator will check if the file extension matches the expected extension before allowing it to proceed.

Let’s begin by adding new methods to the “File” class.

class FileService {
public async upload(file: any, extension: string): Promise<string> {
const s3 = new AWS.S3();
const params = {
Bucket: 'my-bucket',
Key: `avatars/${Date.now()}.${extension}`,
Body: file,
};

const uploadResult = await s3.upload(params).promise();
return uploadResult.Location;
}

public validate(file: any, supportedFormats: string[]): boolean {
const fileExtension = this.getFileExtension(file);

if (supportedFormats.includes(fileExtension.toLowerCase()))
return true;

throw new Error("File extension not allowed!");
}

public getFileExtension(file: any): string {
const fileName = file.name || '';
const parts = fileName.split('.');
if (parts.length > 1)
return parts[parts.length - 1];
return '';
}
}

As you can see, the old method for uploading a file has not been modified; instead, we have simply added new methods to the File class.

The second step is to add the validation method to our “updateAvatar” function.

public async updateAvatar(id:number, file:any) {
try {
this.fileService.validate(file, ["jpg"]);
const avatarPath = await this.fileService.upload(file);
await this.update(id, {avatar: avatarPath});
} catch (error) {
console.error('Avatar update failed:', error);
}
}

Although there is still a minor concern — what if we had more supported formats? — you are completely correct. In that case, we would need to modify this method. So, let’s perform another refactoring on the code. We will separate the supported image formats from the “updateAvatar ” method and move them to global variables or application configuration, where we will only manage the constants, not the logic.

// config.js

export const SUPPORTED_IMAGE_FORMATS = ["jpg", "png", "jpeg", "svg", "webp"];

And then we can use these constants in our “updateAvatar” method.

this.fileService.validate(file, SUPPORTED_IMAGE_FORMATS);

Note: The reason we haven’t directly imported this constant into the “Validate” method is to keep it reusable. This way, we can use the “Validate” method for videos, documents, and other extensions as well.

--

--