Паттерн Strategy (Стратегия), теория

Паттерн Strategy (Стратегия) похож на паттерн Factory подходом. В Factory в зависимости от поданного на вход параметра создаётся и возвращается объект определённого типа. А в Strategy — выполняется определённый для каждого типа параметра произвольный набор действий.

Схема паттерна Factory

В самом простом виде Стратегия реализуется через один «суперкласс»/функцию, включающую в себя все ветки условий:

function formatText(text, formatType) {
if (formatType === "uppercase") {
return text.toUpperCase();
} else if (formatType === "lowercase") {
return text.toLowerCase();
} else if (formatType === "capitalize") {
return text[0].toUpperCase() + text.slice(1).toLowerCase();
} else {
throw new Error("Unknown format type");
}
}
// Использование
console.log(formatText("hello", "uppercase")); // HELLO
console.log(formatText("WORLD", "lowercase")); // world

В случае, если «режимов» работы немного, и они не меняются, а также если не хочется делать логику программы слишком комплексной, такого подхода достаточно. Но у него есть свои недостатки:

Но могут быть и дополнительные требования и условия к программе:

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

Как будет выглядеть Стратегия с таким подходом:

textFormatter.js
// Базовый класс стратегии
class TextFormatStrategy {
format(text) {
throw new Error("Метод format должен быть переопределен!");
}
}
// Конкретные стратегии
class UppercaseStrategy extends TextFormatStrategy {
format(text) {
return text.toUpperCase();
}
}
class LowercaseStrategy extends TextFormatStrategy {
format(text) {
return text.toLowerCase();
}
}
class CapitalizeStrategy extends TextFormatStrategy {
format(text) {
return text[0].toUpperCase() + text.slice(1).toLowerCase();
}
}
// Контекст
export class TextFormatter {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
formatText(text) {
return this.strategy.format(text);
}
}
app.js
import { TextFormatter } from "./textFormatter";
// Использование
const formatter = new TextFormatter();
formatter.setStrategy(new UppercaseStrategy());
console.log(formatter.formatText("hello")); // HELLO
formatter.setStrategy(new LowercaseStrategy());
console.log(formatter.formatText("WORLD")); // world
formatter.setStrategy(new CapitalizeStrategy());
console.log(formatter.formatText("tExT")); // Text

При добавлении новой стратегии, прежние не затрагиваются:

class ReverseStrategy extends TextFormatStrategy {
format(text) {
return text.split("").reverse().join("");
}
}
formatter.setStrategy(new ReverseStrategy());
console.log(formatter.formatText("hello")); // olleh

Что получаем с таким подходом: