Способы передачи управления между программными объектами

Программные объекты — это обыкновенные классы из ООП. Под взаимодействием программных объектов я буду подразумевать передачу управления из одного класса в другой.
Это можно сделать двумя способами. Далее я буду называть их: объектный и событийный. Названия взяты из объектно-событийной парадигмы, подразумевающей наличие объектов, запускающих и обрабатывающих какие-то события. Но в моем случае, смысл другой. Так же, чтобы не путаться, назовем первый класс, из которого управления передается управляющим, второй класс, принимающий управление, соответственно, исполнителем.

Так как я привык работать с языком C# и .NET Framework, то в примерах и объяснениях я буду использовать именно их. Сразу стоит сказать, что в C# механизм событий реализован через делегаты — указатели на метод с заранее оговоренным интерфейсом. Для этих же целей подойдут паттерн Command или Observer. Несмотря на отличия в реализации, они равны по смыслу. Оба выполняют одну задачу — оповещают о событии неизвестного подписчика или подписчиков.
Я рассмотрю ситуацию, когда только один из объектов знает о другом. Еще возможны варианты — оба знают друг о друге (сильная связанность классов), или никто не знает ни о ком (универсальность, применение паттерна Facade и др).

Описание

Объектный способ означает, что один класс (исполнитель) предоставляет наружу через свой интерфейс какой-то метод. Второй (управляющий), имея ссылку на экземпляр исполнителя, может вызывать этот метод. При этом происходит передача управления из первого объекта во второй. Вот пример на языке C# (владеющие С++, Java, думаю, тоже поймут):

//исполнитель
class Worker {
public void doWork() {
//....
}
}

//управляющий
class Boss {
Worker worker1 = new Worker();
Worker worker2 = new Worker();

public void startProject() {
worker1.doWork();
worker2.doWork();
}
}

Как видно, класс Worker ничего не знает о том, кто заставил его работать, кому он принадлежит. С другой стороны, Boss прекрасно представляет себе, что за рабочие у него в подчинении, сколько их и какие работы они могут выполнять. Резюмируя: класс-управляющий (Boss) знает интерфейс класса-исполнителя (Worker), но не наоборот. Управляющий при этом должен знать количество исполнителей.
image

Событийный способ передачи управления работает немного по-другому. Один класс через свой интерфейс объявляет о том, что он может инициировать какие-то события. Другой класс, зная об этом, может на эти события подписаться и обрабатывать их в случае их возникновения.

//исполнитель
class Worker {
public Worker(Boss boss) {
boss.projectStarted += new EventHandler(doWork);
}

private void doWork(object sender, EventArgs e) {
//....
}
}

//управляющий
class Boss {
public event EventHandler projectStarted;

public void startProject() {
if (projectStarted != null)
projectStarted.Invoke(this, new EventArgs());
}
}


Теперь же класс Boss ничего не знает о том, кто будет исполнять его команды, он просто объявляет событие “projectStarted”. Те же, кто хочет выполнить свою работу (класс Worker), совершенно точно знают, на кого они работают. Конкретный экземпляр Worker будет исполнять свою работу только при команде конкретного экземпляра Boss. В противоположность объектному подходу, исполнитель знает интерфейс управляющего, а управляющий ничего не знает об исполнителях и их количестве.
image

Применение


Проблема применения соответствующего подхода встала при реализации Model-View-Controller. Управление из контроллера передается в представление и мне нужно было решить, кто о ком должен знать: Controller о View или View о Controller?
Для начала посмотрим на то, как это реализовано у других. Возьмем какой-нибудь UI Control из библиотеки WinForms (да простят меня приверженцы других UI-фреймворков), например, Button.
Первое, что приходит в голову, когда мы думаем о кнопках — это событие Click(). В подавляющем большинстве случаев именно для обработки кликов мы их и используем. В событийном подходе события инициируются управляющим. Значит, кнопка из WinForms, когда ей нужно передать управление, является управляющим. Теперь взглянем на ее методы, например свойство Title. Оно отвечает за надпись, которую мы видим на кнопке во время работы приложения. Кнопка предоставляет методы для работы с собой через свой интерфейс и, следовательно, играет роль исполнителя. Как же так, кнопка и исполнитель и управляющий? Почему бы и нет? К тому же кнопка даже называется соответственно — UI Control. С одной стороны она UI, т.е. представление-исполнитель, с другой, она Contol, т.е. контроллер-управляющий.
При проектировании класса кнопки у нас есть одна проблема — мы не знаем, куда кнопка может передавать управление и откуда она его может получать. Но поведение реализовать нужно. Позволю себе сформулировать очевидный вывод, который тем не менее, позволяет лучше понять природу дизайна системы. Передавая управление в неизвестность, мы используем событийный механизм, получая управление из неизвестности мы используем механизм объектный.
Картинка для лучшего восприятия:
image

Но это очевидность касается только “неизвестности”. А что же делать, если мы проектируем обе части и у нас нет неизвестности? Надо придумать себе неизвестность. Но какой подход выбрать? Я пришел к выводу, что любой. Все зависит от того, откуда мы начинаем проектировать дизайн, что для нас первично.
Вернемся к разговору про View и Controller. Например, мы пишем систему вывода статистической информации на экран в виде графиков. Именно задача рисования красивых графиков первична — т.е. первичен исполнитель. Далее представляем, что сама статистика для нас не важна (хотя, на самом деле это может быть совершенно не так) и представляем, что управление передается из неизвестности. Следовательно, применяем объектный подход — описываем для View методы рисования графиков, передаем ссылку на View в Controller, и из контроллера в нужных местах производим вызовы методов.
Другоая ситуация — нам важна статистика (управляющий), а как она будет представлена, уже вторично. Теперь для нас неизвестность — это то, как будет статистика отображаться. Применяем событийный подход. Описываем события и делаем их инициацию в нужных местах. Теперь для нас не важно, кто и как их обработает. Модуль рисования графиков при этом сам подписывается на нужные события.
В первом случае удобнее вызывать рисование графиков для разных видов информации, например, не только статистику, но и оперативные данные. Т.е. использование разных контроллеров для одного представления не вызывает трудностей. Во втором же наоборот, мы можем нашу статистику рисовать (причем одновременно) и в виде красивых графиков, и в виде облака значений и любыми другими способами. Другими словами, для одного контроллера легко задать несколько представлений.

Зачем?


Как-то я реализовывал взаимодействие разных частей приложения. В тот момент я еще не задумывался так глубоко об этой теме и сделал самое простое, что мне тогда показалось, а потом еще раз самое простое. В итоге дизайн получился смешанным. Controller и View знали друг о друге — где-то контроллер вызывал методы представления, где-то View подписывалось на события. Вот от этого я и хотел бы предостеречь. Потому что это приводит к обоюдному знанию объектов друг о друге, и как следствие:
  1. Трудность в собственном понимании устройства системы;
  2. Затруднение модификации классов;
  3. Трудность в объяснении устройства системы постороннему человеку.

Так как разрабатываемая система на данном этапе достаточно простая, то первой проблемы пока не возникает. Вторая пока тоже не давала о себе знать. Но это совершенно не значит, что их не будет в будущем. А вот с приходом в команду нового человека, третья очень даже привнесла трудностей. Из-за этого и пришлось структурировать свои знания и описывать все в единых терминах.
Про названные и другие паттерны можно прочитать в книге “Приемы объектно-ориентированного проектирования. Паттерны проектирования” Э. Гаммы, Р. Хелма, Р. Джонсона, Дж. Влиссидеса. Про UML см. книгу “UML. Руководство пользователя” Грэйди Буча, Джеймс Рамбо, Айвар Джекобсона.

Комментарии 8

    +3
    Вы не поверите, есть ещё процедурный способ передачи управления. И ещё командный.
      0
      Отчего же не поверю? Я был уверен, что есть и другие способы =)
      Спасибо за направление дальнейшего поиска. Если еще поможете конкретными авторами и их работами, то буду очень признателен.
      +1
      Фаулера в литературе забыли, причём и про проектирование, и про UML

      Пример со статистикой и графикой, имхо, не корректный — статистика это модель (про которую вы вообще забыли :( а ведь самое интересное на мой взгляд творится именно там )
        0
        Да, кинги Фаулера тоже будут, несомненно, полезны. Сейчас я как раз в процессе их изучения и не хочу рекомендовать то, что сам не прочитал. ГоФов и Буча вполне, имхо, достаточно, чтобы понять, что такое паттерны, и как их описывать на диаграммах классов.

        Под статистикой я имел ввиду не модель, а контроллер, модуль, который занимается сбором и отображением статистики. Естественно, сами данные тоже должны где-то храниться, но то, как устроена модель не рассматривал специально, так как в примере хотел описать именно взаимодействие контроллера и представления.
          0
          Без паттернов банды можно обойтись, а вот без паттернов архитектуры сложновато. Кривая реализация хорошей архитектуры лучше чем хорошая кривой :)
        0
        А еще лучше, если у вас и задача отрисовки графиков, и задача подсчета статистики «логикоёмки», чтобы они друг о друге не знали вообще.
        Задача организации взаимодействия в таком случае — это отдельная зона ответственности и лучше, когда её выполняет отдельный объект. Как доп. плюс в этом случае — взаимодействие лучше продумывается, и идёт обмен законченными структурами данных, а не «по кускам». И тестируются обе части замечательно.

        Но это, конечно, применимо не в случаях, где и View и контроллер достаточно сложны.

        А статья хороша, хотя бы как повод вспомнить о «добром и вечном» :)
          0
          Что-то не особо понятно чем же все таки отличается событияная модель передачи от объектной. С помощью объектов можно довольно многое моделировать, события в том числе. В вашем примере Boss зависит от EventHandler. В том что он в свою очередь ссылается на метод а не на объект не имеет никакого значения для Boss. Boss мог бы быть Observeble и в методе startProject просто вызывать notifyObservers(). Разницы вызывать метод notifyObservers или projectStarted.Invoke(this, new EventArgs()) с точки зрения дизайна никакой.
            0
            Речь не о том, как моделировать передачу управления. Действительно, разницы между объектами-наблюдателями и вызовом метода из делегата .NET с точки зрения факта передачи нет никакой. На самом деле, делегат — это тоже объект. Названия выбраны скорее из идеологических соображений =)

            Разница же в области видимости и связанности объектов. Посмотрите картинку про Неизвестность и SomeClass. В этом и отличие — событиями мы оповещаем неизвестных подписчиков, а вот через интерфейсные методы позволяем этим же незнакомцам управлять собой. Повторюсь, как эта идея реализована технически, разницы нет.

            Наверное, все-таки название «объектный способ» выбрано неудачно и только запутывает. Может, кто подскажет более подходящее?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое