Проблема с Open/Closed принципом

Итак, SOLID. Мы все знаем, что SOLID - это хорошо. Но я вообще не понимаю, что хотел сказать автор под “код должен быть открыт к расширению, но закрыт к модификации”. Как это определить? Какие критерии? Если взять, например, два варианта класса “Calculator”, какой из них более Open?
Откуда это понятно?
Оба хорошие? Или оба плохие?
Что можно сделать лучше?

class Calculator {
	constructor() {
		this.allowedTypes = {
			number: true
		};
	}

	add(a, b) {
		this.__sanitize(a, b);
		return a + b;
	}

	sub(a, b) {
		this.__sanitize(a, b);
		return a - b;
	}

	multiply(a, b) {
		this.__sanitize(a, b);
		return a * b;
	}

	divide(a, b) {
		this.__sanitize(a, b);
		return a / b;
	}

	__sanitize(...numbers) {
		numbers.forEach(num => {
			const type = typeof num;
			if (!this.allowedTypes[type]) {
				throw new TypeError(`Operators of type ${type} are not allowed`);
			}
		});
	}
}

class Calculator {
	constructor() {
		this.allowedTypes = {
			'number': true,
		};

		this.__operations = {
			'+': this.add,
			'-': this.sub,
			'/': this.divide,
			'*': this.multiply,
		}
	}

	compute(a, b, operator) {
		this.__sanitize(a, b, operator);
		this.__operations[operator](a, b);
	}

	__add(a, b) {
		...
	}
	...
}

SOLID попахивает для меня каргокультностью. Потому что

  • мало кто может внятно объяснить суть
  • эти принципы таскаются в те области где они могут не работать (динамические языки и фронтенд).

Я пытался разобраться в вопросе и ясности тоже не достиг.

Самое лучшее объяснение open/closed что я видел это

It tells you to write your code so that you will be able to add new functionality without changing the existing code. That prevents situations in which a change to one of your classes also requires you to adapt all depending classes. Unfortunately, Bertrand Mayer proposes to use inheritance to achieve this goal:


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

Тут можно еще ряд примеров привести как open/closed неприменим к фронту.

В целом я забил думать про этот принцип при разработке на js/ts потому что код он усложняет, а преимущества неочевидны. Гомонящая толпа и зияющий автор принципов неубедительны.


По поводу примера оба класса (если смотреть по определению) не открыты к расширению. Они были бы открыты если бы операторы можно было определять в конструкторе или добавлять через метод.

1 лайк

Еще статья с примером: http://joelabrahamsson.com/a-simple-example-of-the-openclosed-principle/

Спасибо! Если вдруг у кого-то есть еще примеры - насыпайте.

https://wiki.c2.com/?OpenClosedPrinciple

Ресурс шикарен сам по себе: стоит походить по ссылкам

1 лайк

Итак, пообщавшись сразу с двумя умными людьми, я получил такой результат (на псевдо-тайпскрипте):

interface Operator {
   compute(Array<number>): number
}

class Calculator {
   constructor(operationsFactory) {
   	this.allowedTypes = {
   		number: true
   	};

   	this.operationsFactory = operationsFactory;
   }

   compute (operation, numbers) {
   	this.__sanitize(numbers);
   	this.operationsFactory(operator).compute(numbers);
   }

   __sanitize(numbers) {
   	numbers.forEach(num => {
   		...
   	});
   }
}

class Sum implements Operator {
   compute(numbers) {
   	return numbers.reduce((acc, cur) => acc+cur; 0);
   }
}

class Multiply implements Operator {
   compute(numbers) {
   	return a * b;
   }
}

function operationsFactory (operator: string) {
   switch(operator) {
   	case '+':
   		return new Sum; // можно создавать на лету. Можно заранее.
   	case '*':
   		return new Multiply;
   }
}

Главный принцип, который я выяснил, это то, что мы не меняем интерфейс. Остальное открыто к интерпретации:

  1. Параметры, передаваемые в калькулятор, могут быть объектом, потому что порядок, в общем-то, не важен (с одной стороны. С другой, объект - это более гибкий интерфейс, и больше возможностей передать какую-то фигню).
  2. numbers могут быть массивом, потому что их может быть больше или меньше двух (но это хуже, потому что опять же - гибкий интерфейс). А могут быть отдельными числами (но тогда придется морочиться с, например, унарными операциями).

*да, в коде есть пропущенные edge-кейсы, но они сейчас вообще не важны, потому что мы разбираем принцип, а не детали реализации.

1 лайк