Ермаков И. Е. Пример компонентного проектирования с сильной абстракцией/изоляцией компонентов

Пример из обсуждения: Оберон и проектирование систем

А.С. Усов:

Строго говоря… элементы не должны взаимодействовать между собой… напрямую. Они должны делать только то, что им предписано… свыше. Сделали и отдали наверх, а кто там и как дальше работает, они не ведают. Если элемент (компонент/объект) А взаимодействует с элементом Б, то это взаимодействие должно быть определено, как минимум в элементе А, но, возможно, и в элементе Б, а это, в свою очередь, означает, что элемент А не может использоваться без элемента Б и, наоборот. Такая связь «цементирует» элементы и лишает систему гибкости (возможности перестраивать связи). «Слабые связи» в системе исчезают… а за ними и сама система становится монолитом, то есть, не-системой.

Конечно, это так. Но во многих случаях надсистема не занимается «ручной передачей» каждого события между компонентами. Надсистема соединяет их какими-нибудь коммуникациями, трубами. Коммуникации и их установление относятся к надсистеме, конечно. Но протекание события от одной подсистемы другой потом уже идёт без прямого вмешательства надсистемы (хотя она всегда может пересоединить, если что). Так что косвенный вызов через полиморфный указатель, когда вызывающий знает только абстрактный тип (интерфейс) и не знает, какая там реализация на другом конце вызова, как раз похож на такую трубу.

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

Рассмотрим пример на КП. Имена на русском для понятности.

MODULE Телефония;
 
   TYPE
      Линия* = POINTER TO ABSTRACT RECORD END;
      Сеанс* = POINTER TO ABSTRACT RECORD END;
      Реализация* = POINTER TO ABSTRACT RECORD END;
 
   VAR
      стандартная-: Реализация;
 
   PROCEDURE (л: Линия) Звонить* (номер): Сеанс, NEW, ABSTRACT;
   PROCEDURE (л: Линия) Входящий* (): Сеанс, NEW, ABSTRACT;
 
   PROCEDURE (сн: Сеанс) Сказать* (сообщение), NEW, ABSTRACT;
   PROCEDURE (сн: Сеанс) Слушать* (сообщение), NEW, ABSTRACT;
   PROCEDURE (сн: Сеанс) Закрыть*, NEW, ABSTRACT;
 
   PROCEDURE (р: Реализация) НоваяЛиния* (): Линия, NEW, ABSTRACT;
 
   PROCEDURE ЗадатьСтандартную* (реализация: Реализация);
   BEGIN
      стандартная := реализация
   END ЗадатьСтандартную;
 
END Телефония.
 
MODULE Секретари;
   IMPORT Телефония;
 
   TYPE
      Секретарь* = ABSTRACT RECORD END;
      Реализация* .... аналогично ....
 
    VAR
      стандартная*: Реализация;
 
   .... разные процедуры секретаря ...
   PROCEDURE (с: Секретарь) СестьНаТелефон* (линия: Телефония.Линия), NEW, ABSTRACT;
   PROCEDURE (с: Секретарь) Помогать* (кому: Секретарь), NEW, ABSTRACT;
 
END Секретари.

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

MODULE Config;
   PROCEDURE Setup;
   BEGIN
      Телефония.ЗадатьСтандартную(ТелефонияВариант1.реализация);
      Секретари.ЗадатьСтандартную(СекретариВариант1.реализация)
   END Setup;
END Config;

Специфические для нашего приложения особенности вынесем в наш модуль конфигурации:

MODULE НашиРесурсы;
   IMPORT Телефония, СекретариВариантЭкстра;
   VAR
      телефония-: Телефония.Реализация;
      секретари-: Секретари.Реализация;
 
   PROCEDURE Иниц;
   BEGIN
      телефония := Телефония.стандартная;
      секретари := СекретариВариантЭкстра.реализация;
   END Иниц;
 
BEGIN
   Иниц
END НашиРесурсы;

Наконец, есть наш прикладной модуль:

MODULE НашОфис;
   IMPORT Телефония, Секретари, НашиРесурсы;
 
   VAR
      телЛиния: Телефония.Линия;
      секретарь1, секретарь2: Секретари.Секретарь;
 
   PROCEDURE Иниц;
   BEGIN
      телЛиния := НашиРесурсы.телефония.НоваяЛиния();
      секретарь1 := НашиРесуры.секретари.НовыйСекретарь();
      секретарь2 := НашиРесурсы.секретари.НовыйСекретарь();
      секретарь1.СестьНаТелефон(телЛиния);
      секретарь2.Помогать(секретарь1)
   END Иниц;
 
END НашОфис.

Ни реализация секретарей, ни реализация телефонии не знают друг про друга. Модуль НашОфис вообще ничего не знает про то, где реализованы эти компоненты. Он создаёт их экземпляры через абстрактные фабрики и соединяет. После чего они начинают вместе работать. Модуль НашиРесурсы, как отдел обеспечения, знает немного больше про то, какие реализации используются. Но тщательно скрывает эти детали от модуля НашОфис. Но и то, телефонию он использует стандартную, и только для секретарей «заморачивается» знанием конкретной реализации.

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

Получаем расширяемую, изменяемую, в том числе динамически, систему. Компонентную.

© 2005-2018 OberonCore и коллектив авторов.