Принцип открытости/закрытости: расширение кода без модификации

Принцип открытости/закрытости (open/closed principle, OCP) гласит: объекты, или сущности, должны быть открыты для расширения, но закрыты для модификации. Иначе говоря, программные сущности должны быть расширяемы без изменения их основной реализации. В объектно-ориентированном программировании этот принцип применяется путем создания новых классов, расширяющих исходный класс и переопределяющих его методы вместо того, чтобы модифицировать исходный класс напрямую.

В функциональном программировании это достигается за счет использования функций-оберток, когда можно вызвать исходную функцию и применить к ней новую функциональность, не изменяя саму исходную функцию.

Шаблон “декоратор” также является полезным инструментом для соблюдения этого принципа проектирования. С помощью декораторов можно прикрепить к объектам новые задачи или поведение, не изменяя их исходный код, и тем самым сохранить их закрытыми для модификации и открытыми для расширения.


В приведенном ниже примере расширим метод updateAvatar, включив в него валидацию, что позволит предотвратить загрузку расширений, не относящихся к изображениям. Начнем с модификации метода updateAvatar.

public async updateAvatar(id: number, file: any) {
try {
// Получение расширения файла
const fileExtension = this.fileService.getFileExtension(file);

// Проверка того, является ли формат JPG. Если нет - выбрасывается ошибка
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);
}
}

Как видно, приведенный код нарушает принцип открытости/закрытости (являющийся вторым принципом SOLID), поскольку требует модификации кода для добавления дополнительных расширений.

Теперь выполним рефакторинг этого кода, чтобы он соответствовал OCP.

  1. Создадим валидатор файлов, которому можно передать файл и ожидаемое расширение.
  2. Созданный валидатор будет проверять, соответствует ли расширение файла ожидаемому расширению, прежде чем разрешить выполнение операции.

Начнем с добавления новых методов в класс File.

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 '';
}
}

Как видно, прежний метод загрузки файла не был изменен, а просто добавлены новые методы в класс File.

Вторым шагом будет добавление метода валидации в функцию updateAvatar.

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);
}
}

А что, если будет больше поддерживаемых форматов? Опасения совершенно справедливы. В таком случае придется модифицировать данный метод. Итак, проведем еще один рефакторинг кода. Отделим поддерживаемые форматы изображений от метода updateAvatar и перенесем их в глобальные переменные или в конфигурацию приложения, где будем управлять только константами, а не логикой.

// config.js

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

Теперь можно использовать эти константы в методе updateAvatar.

this.fileService.validate(file, SUPPORTED_IMAGE_FORMATS);

Примечание: мы не стали напрямую импортировать эту константу в метод validate, чтобы сохранить возможность повторного использования. Таким образом метод validate можно применять для видео, документов и других расширений.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Reza Erami: Open-Closed Principle: Extending Your Code Without Modification

Предыдущая статья7 фреймворков для работы с LLM
Следующая статьяПочему в React важен порядок вызова хуков?