AVK Selected

Показавшиеся интересными, на мой вкус, посты

Принцип открытости-закрытости

MozgC MozgC
Т.к. считается, что принцип открытости-закрытости был введен Бертраном Мейером, а позже несколько перефразирован и конкретизирован Бобом Мартином, то для лучшего понимания принципа стоит рассмотреть, как он описывался в обоих источниках: у Мейера и у Мартина.

Начнем с рассмотрения принципа в книге Мейера "Объектно-ориентированное конструирование программных систем":

Открыт-Закрыт

Любой метод модульной декомпозиции должен удовлетворять принципу семафора: Открыт-Закрыт:

Принцип Открыт-Закрыт

Модули должны иметь возможность быть как открытыми, так и закрытыми.


Противоречие является лишь кажущимся, поскольку термины соответствуют разным целевым установкам:

  • Модуль называют открытым, если он еще доступен для расширения. Например, имеется возможность расширить множество операций в нем или добавить поля к его структурам данных.
  • Модуль называют закрытым, если он доступен для использования другими модулями. Это означает, что модуль (его интерфейс — с точки зрения скрытия информации) уже имеет строго определенное окончательное описание. На уровне реализации закрытое состояние модуля означает, что модуль можно компилировать, сохранять в библиотеке и делать его доступным для использования другими модулями (его клиентами ). На этапе проектирования или спецификации закрытие модуля означает, что он одобрен руководством, внесен в официальный репозиторий утвержденных программных элементов проекта — базу проекта (project baseline ), и его интерфейс опубликован в интересах авторов других модулей.

Необходимость закрывать модули и необходимость оставлять их открытыми вызываются разными причинами. Для разработчиков ПО естественным состоянием модуля является его открытость, поскольку почти невозможно заранее предусмотреть все элементы — данные, операции — которые могут потребоваться в процессе создания модуля. Поэтому разработчики стараются сохранять гибкость ПО, допускающую последующие изменения и дополнения. Но необходимо, особенно с точки зрения руководителя проекта, закрывать модули. В системе, состоящей из многих модулей, большинство модулей зависимы. Например, модуль интерфейса пользователя может зависеть от модуля синтаксического разбора (parsing module) — синтаксического анализатора и от модуля графики. Синтаксический анализатор может зависеть от модуля лексического анализа, и так далее. Если не закрывать модуль до тех пор, пока не будет уверенности, что он уже содержит все необходимые компоненты, то невозможно будет завершить разработку многомодульной программы: каждый из разработчиков будет вынужден ожидать, когда же завершат свою работу все остальные.

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

Два рисунка, приведенные ниже, иллюстрируют ситуацию, в которой трудно согласовать потребности в открытых и закрытых состояниях модуля. На первом рисунке модуль A используется модулями-клиентами B, С, D, которые сами могут иметь своих клиентов — E, F и так далее.

http://files.rsdn.org/34572/Meyer_open_closed_1.gif
Рис. 3.12. Модуль А и его клиенты

В процессе течения времени ситуация изменяется и появляются новые клиенты — F и другие, которым требуется расширенная или приспособленная к новым условиям версия модуля A, которую можно назвать A':

http://files.rsdn.org/34572/Meyer_open_closed_2.gif
Рис. 3.13. Старые и новые клиенты

При использовании не ОО-методов, возможны лишь два решения этой проблемы, в равной степени неудовлетворительные:

  • N1 Можно переделать модуль A так, чтобы он обеспечивал расширенную или видоизмененную функциональность, требуемую новым клиентам.
  • N2 Можно сохранить A в прежнем виде, сделать его копию, изменить имя копии модуля на A', и выполнить все необходимые переделки в новом модуле. При таком подходе новый модуль A' никак не будет связан со старым модулем A.

Возможные катастрофические последствия решения N1 очевидны. Модуль A мог использоваться длительное время и иметь многих клиентов, таких как B, С и D. Переделки, необходимые для удовлетворения потребностей новых клиентов, могут нарушить предположения, на основе которых старые клиенты использовали модуль A ; в этом случае изменения в A могут "запустить" катастрофическую цепочку изменений у клиентов, у клиентов этих клиентов, и так далее. Для руководителя проекта это будет настоящим кошмаром: внезапно целые части ПО, считавшегося давным-давно завершенным и сданным в эксплуатацию, окажутся заново открытыми, что "запустит" новый цикл разработки, тестирования, отладки и документирования. Многие ли из руководителей проектов ПО захотят видеть себя в роли Сизифа — быть приговоренными вечно катить камень на вершину горы лишь для того, чтобы видеть, как он всякий раз вновь скатывается вниз — и все из-за проблем, вызванных необходимостью заново открывать ранее закрытые модули.

На первый взгляд решение N2 кажется лучшим: оно позволяет избежать синдрома Сизифа, поскольку не требует модификации уже существующих программных средств (показанных в верхней части последнего рисунка). Но в действительности, это решение может иметь еще худшие последствия, поскольку оно лишь отодвигает час расплаты. Экстраполируем воздействие этого решения на множество модулей, — потребуется множество модификаций, занимающих длительное время. В конечном счете, последствия оказываются ужасными: бурный рост числа вариантов исходных модулей, многие из которых очень похожи, хотя и не вполне идентичны.

Для многих организаций по разработке ПО такое изобилие модулей, не согласующееся с количеством выполняемых функций (многие из вариантов, кажущихся различными, оказываются, по существу, клонами), создает серьезную проблему управления конфигурацией ПО. И эту проблему обычно пытаются преодолеть путем использования сложных инструментальных средств. Полезные сами по себе, эти инструментальные средства пытаются "лечить" программу в ситуациях, когда предпочтительней было бы первое из рассмотренных решений. Ведь лучше избежать избыточности, чем создавать ее.
Несомненно, управление конфигурацией окажется полезным, но лишь в случае, если удастся найти модули, нуждающиеся в повторном открытии после возникших изменений, и в то же время избежать повторной компиляции модулей, не нуждающихся в этом. (В упражнении У3.6 предлагается выяснить, какова будет необходимость управления конфигурацией в объектно-ориентированной среде программирования.)

Но как можно получить модули, которые были бы одновременно и открытыми и закрытыми? Можно ли сохранить неизмененным модуль A и всех его клиентов в верхней части рисунка, и в то же время предоставить модуль A' клиентам в нижней части, избегая дублирования программных средств? Благодаря механизму наследования (inheritance), ОО-подход обеспечивает особенно изящный вклад в решение этой проблемы.

Механизм наследования подробно рассматривается в последующих лекциях, а здесь дается лишь общее представление об этом. Для разрешения дилеммы, — изменять или повторно выполнять — наследование позволяет определить новый модуль A' на основе существующего модуля A, констатируя лишь различия между ними. Опишем A' как

class A' inherit
       A
            redefine f, g, ... end
feature
       f is ...
       g is ...
       ...
       u is ...
       ...
end

где предложение feature содержит как определение новых компонент, характерных для A', например u, так и переопределение тех компонент (таких как f, g,, представление которых в A' отличается от того, которое они имели в A.

Для графической иллюстрации наследования используется стрелка от "наследника" (heir) (нового класса A' ) к "родителю" (parent) (классу A ):

http://files.rsdn.org/34572/Meyer_open_closed_3.gif
Рис. 3.14. Адаптация модуля к новым клиентам

Благодаря механизму наследования ОО, разработчики могут осуществлять гораздо более последовательный подход к разработке ПО, чем это было возможно при использовании прежних методов. Один из способов описания принципа Открыт-Закрыт и следующих из него ОО-методов состоит в рассмотрении их как организованного хакерства. Под "хакерством" здесь понимается небрежный (slipshod) подход к компоновке и модификации программы (а вовсе не несанкционированное и, конечно, недопустимое проникновение в компьютерные сети). Хакера можно считать плохим человеком, но часто намерения его чисты. Он может разглядеть полезный фрагмент программы, который почти пригоден для реализации текущих потребностей, намного превосходящих потребности, предусмотренные при первоначальной разработке программы. Вдохновленный похвальным желанием не создавать повторно то, что можно повторно использовать, наш хакер начинает модифицировать исходный текст программы, дополняя его средствами для выполнения новых задач. Конечно, такой порыв неплох, но результатом часто оказывается "засорение" программы многочисленными выражениями вида: if(этот_частный_случай) then. После нескольких повторений, возможно, осуществляемыми разными хакерами, программа начинает походить на ломоть швейцарского сыра, оставленного слишком долго на августовской жаре (безвкусность этой метафоры оправдывается тем, что она хорошо воспроизводит появление в такой программе как "дырок", так и "наростов").

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

Небольшое предупреждение: здесь не предлагается неорганизованное хакерство. В частности:

  • Если имеется возможность переписать исходную программу так, чтобы она, без излишнего усложнения, смогла удовлетворять потребности нескольких разновидностей клиентов, то следует это сделать.
  • Как принцип Открыт-Закрыт, так и переопределение в механизме наследования не позволяют справиться с дефектами разработки, не говоря уже об ошибках в программе. Если в модуле что-то не в порядке, то следует это сразу исправить в исходной программе, не пытаясь разбираться с возникающей проблемой в производном модуле. Возможным исключением из этого правила является случай некорректной программы, которую не разрешено модифицировать. Принцип Открыт-Закрыт и связанные с ним методы программирования, предназначены для адаптации "здоровых" модулей, то есть модулей, которые хотя и не могут решать некоторые новые задачи, однако отвечают строго определенным требованиям в интересах своих клиентов.

(с) Текст взят из книги, купленной за смешные деньги здесь: http://www.intuit.ru/goods_store/ebooks/8212

Теперь посмотрим, как описывает принцип Боб Мартин в своей книге Agile Principles, Patterns and Practices in C#:

As Ivar Jacobson has said, "All systems change during their life cycles. This must be borne in mind when developing systems expected to last longer than the first version." How can we create designs that are stable in the face of change and that will last longer than the first version? Bertrand Meyer gave us guidance as long ago as 1988 when he coined the now-famous open/closed principle. To paraphrase him:

The Open/Closed Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

When a single change to a program results in a cascade of changes to dependent modules, the design smells of rigidity. OCP advises us to refactor the system so that further changes of that kind will not cause more modifications. If OCP is applied well, further changes of that kind are achieved by adding new code, not by changing old code that already works. This may seem like motherhood and apple piethe golden, unachievable idealbut in fact, there are some relatively simple and effective strategies for approaching that ideal.

Description of OCP

Modules that conform to OCP have two primary attributes.

  • They are open for extension. This means that the behavior of the module can be extended. As the requirements of the application change, we can extend the module with new behaviors that satisfy those changes. In other words, we are able to change what the module does.
  • They are closed for modification. Extending the behavior of a module does not result in changes to the source, or binary, code of the module. The binary executable version of the modulewhether in a linkable library, a DLL, or a .EXE fileremains untouched.

It would seem that these two attributes are at odds. The normal way to extend the behavior of a module is to make changes to the source code of that module. A module that cannot be changed is normally thought to have a fixed behavior.

How is it possible that the behaviors of a module can be modified without changing its source code? Without changing the module, how can we change what a module does?

The answer is abstraction. In C# or any other object-oriented programming language (OOPL), it is possible to create abstractions that are fixed and yet represent an unbounded group of possible behaviors. The abstractions are abstract base classes, and the unbounded group of possible behaviors are represented by all the possible derivative classes.

It is possible for a module to manipulate an abstraction. Such a module can be closed for modification, since it depends on an abstraction that is fixed. Yet the behavior of that module can be extended by creating new derivatives of the abstraction.

Figure 9-1 shows a simple design that does not conform to OCP. Both the Client and Server classes are concrete. The Client class uses the Server class. If we want for a Client object to use a different server object, the Client class must be changed to name the new server class.

Figure 9-1. Client is not open and closed.
http://files.rsdn.org/34572/Martin_open_closed_1.png

Figure 9-2 shows the corresponding design that conforms to the OCP by using the STRATEGY pattern (see Chapter 22). In this case, the ClientInterface class is abstract with abstract member functions. The Client class uses this abstraction. However, objects of the Client class will be using objects of the derivative Server class. If we want Client objects to use a different server class, a new derivative of the ClientInterface class can be created. The Client class can remain unchanged.

Figure 9-2. STRATEGY pattern: Client is both open and closed.
http://files.rsdn.org/34572/Martin_open_closed_2.png

The Client has some work that it needs to get done and can describe that work in terms of the abstract interface presented by ClientInterface. Subtypes of Client-Interface can implement that interface in any manner they choose. Thus, the behavior specified in Client can be extended and modified by creating new subtypes of ClientInterface.

You may wonder why I named ClientInterface the way I did. Why didn't I call it AbstractServer instead? The reason, as we will see later, is that abstract classes are more closely associated to their clients than to the classes that implement them.

Figure 9-3 shows an alternate structure using the TEMPLATE METHOD pattern (see Chapter 22). The Policy class has a set of concrete public functions that implement a policy, similar to the functions of the Client in Figure 9-2. As before, those policy functions describe some work that needs to be done in terms of some abstract interfaces. However, in this case, the abstract interfaces are part of the Policy class itself. In C#, they would be abstract methods. Those functions are implemented in the subtypes of Policy. Thus, the behaviors specified within Policy can be extended or modified by creating new derivatives of the Policy class.

Figure 9-3. TEMPLATE METHOD pattern: Base class is open and closed.
http://files.rsdn.org/34572/Martin_open_closed_3.png

These two patterns are the most common ways of satisfying OCP. They represent a clear separation of generic functionality from the detailed implementation of that functionality.


На первый взгляд может показаться, что описание принципа у Мейера и Мартина различается, но если присмотреться, то принцип один и тот же. Чуть-чуть перефразируем Мейера:

Модули должны иметь возможность быть как открытыми (ещё доступны для расширения), так и закрытыми (доступен для использования другими модулями).

В общем-то то же самое и у Мартина:

Программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для модификации.

Основная проблема, от которой призван избавить принцип Открыт-закрыт, у обоих авторов одинаковая — это сложность (проблематичность) изменения модуля, который уже используется разными клиентами. Мейер правда упоминает альтернативу изменения модуля — его дублирование и изменение уже дублированного модуля (при этом, напомним, Мейер эту альтернативу признает неудовлетворительной).

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

Я бы не сказал, что каждый программист трактует этот принцип по-разному. По крайней мере такого не должно быть, если углубиться в его понимание. Т.е. проектировать модули надо так, чтобы они поддерживали расширение там, где это может понадобиться. А где реально это может понадобиться — это отдельная тема, вечная проблема и сложнейший вопрос, в котором могут помочь опыт и чутьё архитектора, здравый смысл, дополнительные исследования и опрос пользователей/бизнес-аналитиков. Т.е. нужно стараться соблюдать грань, между проектированием с учетом возможного расширения и overengeneering (когда окажется, что модуль спроектировали с учетом возможного расширения, а этого расширения спустя годы так и не произошло). Тут уже у каждого свой подход: кто-то не заморачивается вообще, пока это реально не нужно, кто-то все заранее делает через интерфейсы (передаю привет кое-кому, кого я очень уважаю и ценю ).

Тот же Мартин в главе по OCP предлагает следующий подход:

In general, no matter how "closed" a module is, there will always be some kind of change against which it is not closed. There is no model that is natural to all contexts!

Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close the design, must guess at the kinds of changes that are most likely, and then construct abstractions to protect against those changes.

This takes a certain amount of prescience derived from experience. Experienced designers hope that they know the users and the industry well enough to judge the probability of various kinds of changes. These designers then invoke OCP against the most probable changes.

This is not easy. It amounts to making educated guesses about the likely kinds of changes that the application will suffer over time. When the designers guess right, they win. When they guess wrong, they lose. And they will certainly guess wrong some of the time.

Also, conforming to OCP is expensive. It takes development time and effort to create the appropriate abstractions. Those abstractions also increase the complexity of the software design. There is a limit to the amount of abstraction that the developers can afford. Clearly, we want to limit the application of OCP to changes that are likely.

How do we know which changes are likely? We do the appropriate research, we ask the appropriate questions, and we use our experience and common sense. And after all that, we wait until the changes happen!

Putting the "Hooks" In

How do we protect ourselves from changes? In the previous century, we said that we'd "put the hooks in" for changes that we thought might take place. We felt that this would make our software flexible.

However, the hooks we put in were often incorrect. Worse, they smelled of needless complexity that had to be supported and maintained, even though they weren't used. This is not a good thing. We don't want to load the design with lots of unnecessary abstraction. Rather, we want to wait until we need the abstraction and then put them in.

Fool me once

"Fool me once, shame on you. Fool me twice, shame on me." This is a powerful attitude in software design. To keep from loading our software with needless complexity, we may permit ourselves to be fooled once. This means that we initially write our code expecting it not to change. When a change occurs, we implement the abstractions that protect us from future changes of that kind. In short, we take the first bullet and then make sure that we are protected from any more bullets coming from that particular gun.

Stimulating change

If we decide to take the first bullet, it is to our advantage to get the bullets flying early and frequently. We want to know what kinds of changes are likely before we are very far down the development path. The longer we wait to find out what kinds of changes are likely, the more difficult it will be to create the appropriate abstractions.

Therefore, we need to stimulate the changes. We do this through several of the means discussed in Chapter 2.

  • We write tests first. Testing is one kind of usage of the system. By writing tests first, we force the system to be testable. Therefore, changes in testability will not surprise us later. We will have built the abstractions that make the system testable. We are likely to find that many of these abstractions will protect us from other kinds of changes later.
  • We use very short development cycles: days instead of weeks.
  • We develop features before infrastructure and frequently show those features to stakeholders.
  • We develop the most important features first.
  • We release the software early and often. We get it in front of our customers and users as quickly and as often as possible.

HrorH
HrorH
09.01.2014 10:51
Здравствуйте, MozgC, Вы писали:

Спасибо за такой очень полный обзор проблемы.
Тем не менее не могу согласиться, что принцип открытости-закрытости у Мейера и Мартина это одно и то же.
Конечно есть много общего, но есть и важные различия (или может я чего не понимаю).

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

Далее, под закрытостью Мейер понимает доступность модуля для использования другими модулями, и раскрывает значение понятия закрытости по-разному в зависимости от контекста, включая туда например публикацию интерфейса (документации на интерфейс?).
Мартин же упоминает еще и о классах и функциях, т.е говорит исключительно о стабилизации кода и контракта.

Во-вторых, предлагаемые способы реализации отличаются.
Мейер использует метафору, в которой модуль является неким набором фич, и наследование позволяет добавить или заменить эти фичи.
Это метафора наследования реализации.
Мартин предлагает использовать интерфейсы и абстрактные методы.
MozgC
MozgC
09.01.2014 01:21
Здравствуйте, HrorH, Вы писали:

HH>Спасибо за такой очень полный обзор проблемы.

HH>Тем не менее не могу согласиться, что принцип открытости-закрытости у Мейера и Мартина это одно и то же.
HH>Конечно есть много общего, но есть и важные различия (или может я чего не понимаю).

На самом деле по своей невнимательности я пропустил кое-что у Мартина, хотя читал эту главу несколько раз. Но в личной беседе Сергей Тепляков указал мне на это различие, которое я не заметил. Я надеюсь, что он скоро отпишется по этому поводу в этой теме.

PS. Очень жаль, что на RSDN до сих пор нельзя отредактировать сообщение, а то я бы сейчас исправил свой предыдущий пост.
HrorH
HrorH
14.01.2014 12:33
Здравствуйте, MozgC, Вы писали:

MC>На самом деле по своей невнимательности я пропустил кое-что у Мартина, хотя читал эту главу несколько раз. Но в личной беседе Сергей Тепляков указал мне на это различие, которое я не заметил. Я надеюсь, что он скоро отпишется по этому поводу в этой теме.


А можно хотя бы намекнуть, о чем идет речь?
MozgC
MozgC
14.01.2014 12:57
Здравствуйте, HrorH, Вы писали:

HH>А можно хотя бы намекнуть, о чем идет речь?


Можно. Процитируем Мартина опять:

They are closed for modification. Extending the behavior of a module does not result in changes to the source, or binary, code of the module. The binary executable version of the modulewhether in a linkable library, a DLL, or a .EXE fileremains untouched.


Т.е. по Мартину нельзя вносить изменений в код вообще. Например, получается, что нельзя изменить внутреннюю реализацию метода (а не публичный интерфейс), нельзя даже исправить баг. Тут, конечно, Мартин, мягко говоря, погорячился, и с такой его трактовкой принципа согласиться трудно.
HrorH
HrorH
14.01.2014 03:02
Здравствуйте, MozgC, Вы писали:

MC>Можно. Процитируем Мартина опять:


MC>

They are closed for modification. Extending the behavior of a module does not result in changes to the source, or binary, code of the module. The binary executable version of the modulewhether in a linkable library, a DLL, or a .EXE fileremains untouched.


MC>Т.е. по Мартину нельзя вносить изменений в код вообще. Например, получается, что нельзя изменить внутреннюю реализацию метода (а не публичный интерфейс), нельзя даже исправить баг. Тут, конечно, Мартин, мягко говоря, погорячился, и с такой его трактовкой принципа согласиться трудно.


Тут написано, что при расширении поведения модуля его код нельзя менять.
Насчет того, что при исправлении баг код нельзя менять здесь не сказано.
И это по-моему как раз совпадает с тем, что у Мейера (точнее это частный случай).
Другое дело, что в другом месте Мартин пишет о принципе открытости-закрытости по отношению к классам и методам, а вот это уже сильно меняет смысл, потому что получается, что класс может быть закрыт, когда модуль еще не закрыт (???)
MozgC
MozgC
14.01.2014 07:08
Здравствуйте, HrorH, Вы писали:

HH>Тут написано, что при расширении поведения модуля его код нельзя менять.

HH>Насчет того, что при исправлении баг код нельзя менять здесь не сказано.

Да, я опять был недостаточно внимателен. Но правда в своей первой версии статьи об OCP Мартин писал именно про изменения:

They are “Closed for Modification”. The source code of such a module is inviolate. No one is allowed to make source code changes to it.

Правда позже он сам признал, что погорячился.

HH>И это по-моему как раз совпадает с тем, что у Мейера (точнее это частный случай).

HH>Другое дело, что в другом месте Мартин пишет о принципе открытости-закрытости по отношению к классам и методам, а вот это уже сильно меняет смысл, потому что получается, что класс может быть закрыт, когда модуль еще не закрыт (???)

Я думаю, что Мартин не имел в виду закрытость классов в отрыве от модулей. Просто он рассматривал OCP на примере связи классов, предполагая что эти классы находятся в разных модулях. Вот как более простыми словами Мартин описывал OCP в своем блоге в 2013-м году:

And that's really the essence of the OCP. It should be easy to change the behavior of a module without changing the source code of that module. This doesn't mean you will never change the source code, this doesn't mean you can stop using your version control systems (sheesh!). What it means is that you should strive to get your code into a position such that, when behavior changes in expected ways, you don't have to make sweeping changes to all the modules of the system. Ideally, you will be able to add the new behavior by adding new code, and changing little or no old code.

LeonidV
LeonidV
17.01.2014 06:43
OCP это все-таки, принцип проектирования, а не поддержки. Поэтому насчет бага, на мой взгляд, это не о нем. Насчет развития самой библиотеки — тоже.

Я его трактую так. Есть библиотека, поведение которой хочется изменить в каком-то месте. Если я могу это сделать не трогая исходный код библиотеки — значит принцип OCP действует. Иначе — нет.

Вот пример. Мы делаем некую библиотеку, в которая предоставляет функционал будильника (для мобильника). На этапе проектирования мы знаем, что будильник имеет два режим: подать сигнал звуком или подать сигнал вибрацию. Соотвественно в коде мы можем сделать что-то типа такого:

final class Alarm {
 boolean soundMode = true;

 private doAlarm() {
   if (soundMode) {
     playAlarmSound();
   } else {
     vibrateAlarm();
   }
 }
}


Использование:


Alarm alarm = new Alarm();
alarm.setTime(after(5).s);
alarm.turnOn();



Понятно, что изменения поведения будильника (например, добавления режима звук+вибрация) можно сделать только путем изменения исходного кода класса Alarm. Если же выделить страгию AlarmAction, то мы можем расширить поведение будильника НЕ меняя его исходный код. Поэтому он будет удовлетворять принципу OCP (с т.з. функционала режима сигналов).