Pull to refresh

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

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

Так как я привык работать с языком 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. Руководство пользователя” Грэйди Буча, Джеймс Рамбо, Айвар Джекобсона.
Tags:
Hubs:
+18
Comments8

Articles