Принцип открыт/закрыт (Open-Closed Principle, OCP)
Программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для модификации.
-
Они открыты для расширения.
Это означает, что поведение модуля можно расширить. Когда требования к приложению изменяются, мы добавляем в модуль новое поведение, отвечающее изменившимся требованиям. Иными словами, мы можем изменить состав функций модуля.
-
Они закрыты для модификации.
Расширение поведения модуля не сопряжено с изменениями в исходном или двоичном коде модуля. Двоичное исполняемое представление модуля, будь то компонуемая библиотека, DLL или EXE-файл, остается неизменным.
При этом понятия открытости и закрытости определяются так:
-
Модуль называют открытым, если он еще доступен для расширения. Например, имеется возможность расширить множество операций в нем или добавить поля к его структурам данных.
-
Модуль называют закрытым, если он доступен для использования другими модулями. Это означает, что модуль (его интерфейс - с точки зрения скрытия информации) уже имеет строго определенное окончательное описание. На уровне реализации закрытое состояние модуля означает, что модуль можно компилировать, сохранять в библиотеке и делать его доступным для использования другими модулями (его клиентами).
Но всегда ли нам нужно создавать наследника? Нет, не всегда! Даже такой ОО-гуру как Мейер, который описывает в своей книге 12 видов наследования, относится к расширяемости модулей путем создания наследников с разумным прагматизмом:
«Если имеется возможность переписать исходную программу так, чтобы она, без излишнего усложнения, смогла удовлетворять потребности нескольких разновидностей клиентов, то следует это сделать».
Естественно, модуль должен модифицироваться при наличии в нем ошибок:
«Как принцип Открыт-Закрыт, так и переопределение в механизме наследования не позволяет справиться с дефектами разработки, не говоря уже об ошибках в программе. Если в модуле что-то не в порядке, то следует это сразу исправить в исходной программе, не пытаясь разбираться возникающей проблемой в производном модуле»
abstract class Importer
{
public abstract void ImportData();
}
static class ImporterFactory
{
public static Importer Create(string fileName)
{
Contract.Requires(!string.IsNullOrEmpty(fileName));
Contract.Ensures(Contract.Result<Importer>() != null);
var extension = Path.GetExtension(fileName);
switch (extension)
{
case "json":
return new JsonImporter();
case "xls":
case "xlsx":
return new XlsImporter();
default:
throw new InvalidOperationException(
"Extension is not supported");
}
}
Отвечает ли реализация такой фабрики принципу Open/Close? Или для соответствия этому принципу нам нужно вводить интерфейс IImporterFactory и еще одну иерархию - иерархию фабрик? А кто будет создавать фабрику? С помощью фабрики фабрик?
Фабричный метод сам по себе и так уже прячет от своих клиентов способ получения нужного Importer-а и дополнительный уровень косвенности здесь не нужен.
Вот что пишет Бертран Мейер по этому поводу:
«Необходимо допускать возможность того, что список вариантов, заданных и известных на некотором этапе разработки программы, может в последующем быть изменен путем добавления или удаления вариантов. Чтобы обеспечить реализацию такого подхода к процессу разработки программного обеспечения, нужно найти способ защитить структуру программы от воздействия подобных изменений. Отсюда следует принцип Единственного Выбора»
Принцип единственного выбора: всякий раз, когда система программного обеспечения должна поддерживать множество альтернатив, их полный список должен быть известен только одному модулю системы.
Смысл принципа OCP довольно прост: дизайн системы должен быть простым и устойчивым к изменениям.
-
Во-первых, за счет абстракции и инкапсуляции. Мы выделяем существенные части системы в виде интерфейсов и абстрактных классов, не задумываясь о реализации, которая будет скрыта от клиентов. Но даже конкретный класс, унаследованный от
System.Objectпредставляет собой абстракцию. КлассStringабстрагирует нас от конкретного представления строки и многих других подробностей, хотя и не реализует интерфейсIString.Любой класс моделирует абстракцию: его открытый интерфейс акцентирует внимание на важной составляющей класса, оставляя в закрытой части ненужные подробности. Это позволяет думать о классе, как о черном ящике, который представляет определенные услуги лишь ему известным способом.
-
Во-вторых, за счет наследования. Мы используем наследование для полиморфного поведения, что позволяет использовать базовые классы в подавляющем числе мест приложения, а также использовать повторно значительные куски кода.
!!! Абстракция не подразумевает наличие наследования. Абстракция не может существовать без инкапсуляции! *(книга Гради Буча «Объектно-ориентированный анализ и проектирование»)
Что такое OCP? Это фиксация интерфейса класса/модуля, и возможность изменения или подмены реализации/поведения.
Цели OCP: Борьба со сложностью и ограничение изменений минимальным числом модулей.
Как мы реализуем OCP? С помощью инкапсуляции. Она позволяет изменять реализацию без изменения интерфейса и с помощью наследования. Наследование уже в свою очередь позволяет заменить реализацию, которая не затронет существующих клиентов базового класса.