середу, 13 жовтня 2010 р.

MVC pattern in Delphi

MVC pattern in Delphi (Model-view-controller , "Модель-представление-поведение", "Модель-представление-контроллер").

Как известно, реализаций одной идеи может быть несколько, а на практике желательно выбрать оптимальную для конкретного случая. Однако хороших реализаций хороших идей не так много и думаю, имеет смысл их обсуждать :) Хочется рассказать и обсудить эволюцию реализации шаблона проектирования MVC, которая происходила в моих проектах на Delphi.



Для начала, что бы определится с терминами, процитирую описание шаблона:
Шаблон MVC позволяет разделить данные, представление и обработку действий пользователя на три отдельных компонента
  • Модель (Model). Модель предоставляет данные (обычно для View), а также реагирует на запросы (обычно от контроллера), изменяя своё состояние.
  • Представление (View). Отвечает за отображение информации (пользовательский интерфейс).
  • Поведение (Controller). Интерпретирует данные, введённые пользователем, и информирует модель и представление о необходимости соответствующей реакции.
Важно отметить, что как представление, так и поведение зависят от модели. Однако модель не зависит ни от представления, ни от поведения. Это одно из ключевых достоинств подобного разделения. Оно позволяет строить модель независимо от визуального представления, а также создавать несколько различных представлений для одной модели.
Первая реализация MVC, которую я увидел, придя в довольно большой проект, выглядела классически - на каждый элемент шаблона была своя иерархия самописных классов, которые стыковались между собой в соответствии с логикой шаблона и приложения. Все хорошо работало, разделение кода было на высоте, но был момент, который несколько огорчал - в контроллерах, работавших с базой данных все датасэты и привязки к данным нужно было создавать/прописывать руками. Если вспомнить, что Delphi - это RAD средство, призванное максимально уменьшить количество типового кода, то этот факт огорчал вдвойне ;) Для приложений, не работающих с БД, я продолжал использовать такой вариант реализации абсолютно без проблем. Но пару лет назад, после 3-х летнего перерыва опять пришлось писать приложение, работающее преимущественно с базой. Пришлось задумался, как не потерять возможность работы в редаторе для настройки интерфейса и биндингов, при использовании шаблонов. Из нескольких вариантов у меня победила реализация MVC c использованием фреймов (TFrame).
Итак, описание моей реализации MVC для Delphi:
  1. Модель (Model) - тут ничего не меняется - это либо данные из БД, либо какой-то класс.
  2. Поведение (Controller) - базовый контроллер - наследник от TFrame.
  3. Представление (View) - базовое представление - наследник от фрэйма-Controller'а.
Дальше, наверно, будет лучше с примером. Допустим, у нас есть сотрудники(employee), и отделы(departments) в которых они работают.
Иерархия классов будет выглядеть так:
  • Контроллеры
TFrame -> TFraBase -> TFraEmployeeBase
                                  -> TFraDepartmentsBase
  • Представление - вот тут появляется нюанс, из-за которого я все это и пишу ;) а то знаете ли объяснять одно и то же много раз таки накладно ;) Поскольку было большое желание оставить все плюшки работы в IDE появился такой нюанс - некоторые представления(View) - наследники описанных контроллеров, НО в них - ни единой строки бизнес-логики, только код, отвечающий за GUI:

TFraEmployeeBase(Controller)     -> TFraEmployee(View, работа с 1-й записью, например на каждое поле - свой Edit)
                                                     -> TFraEmployeeTbl(View, работа в табличном виде)
TFraDepartmentsBase(Controller) -> TFraDepartments(View, работа с 1-й записью)
                                                     -> TFraDepartmentsTbl(View, работа в табличном виде)
ну и 2-й вид View, собственно окна, которые видит пользователь
TForm -> TFrmBase -> TFrmEmployeeList(допустим, просто список служащих - тогда содержит TFraEmployeeTbl)
                                 -> TFrmDepartmentsList(если просто список отделов- тогда содержит TFraDepartmentsTbl)
                                 -> TFrmEmployeeByDepartments(список служащих по отделам - TFraDepartmentsTbl и TFraEmployeeTbl, связанные как мастер-детали)

Теперь попробую объяснить, зачем это было городить ;)
  1. Поскольку контроллер - фрэйм - все датабиндинги настраиваются в IDE, что не сравнить с ручным созданием всей этой обвязки.
  2. Поскольку конкретное отображение конкретного куска биснес-логики лежит на фрейме-наследнике, то привязки к гридам(Grid) и эдитам(Edit) делается в IDE так, что многие завидуют - МЫШКОЙ ;)
  3. С учетом п.1 и 2 на конечном представление - на форме лежат фреймы, между которыми можно делать, например связи мастер-детали прямо из IDE. В итоге в модуле формы только код по инициализации фремов, возможно еще несколько строк по взимодействию между фреймами, но ВООБЩЕ НЕТ бизенс-логики - она вся во фреймах *Base.
  4. Такая реализация дает возможность очень быстро менять вид конечной формы - накидали нужных фреймов, связали между собой - все новый вариант отображения ;) В общем, форму выкинуть не жалко - кода там почти нет :)
  5. В умных книгах регулярно пишут, что злоупотреблений наследованием - это недоработка в вашей архитектуре. При описанной схеме максимальный уровень наследования контроллеров 4-5, обычный 2-3 и не растет даже призначительных переделках. Про отображение можно сказать тоже самое.
Также очень легко было поддерживать минимальное кол-ва связей между разными кусками кода. Конечная иерархия классов в реальном проекте - строго древовидная, никаких перекрестных связей. Каждый фрейм знает только про своих предков и ничего не знает про другие Controller/View. Все связи между фреймами появляются  только на форме.

Не раз приходилось видеть, как люди с "альтернативным мышлением" ;) в проектах рисовали иерархию классов, основываясь на визуальном поведении, а не бизнес логике. Этот кошмар часто выглядел  примерно так:
1-но табличный Справочник -> 2-х табличный Справочник ->...
Потом к этому ужасу пишут изощренные инициализаторы, что бы это работало с разной бизнес логикой, а когда не получается - ветвят формы и через полгодна мало кто до конца помнит, почему восемь 2-х табличных справочников и пять 3-х табличных ;)
А теперь сравним, что произойдет, когда нужно будет добавить, 1 поле к таблице сотрудников из описанного выше примера (сотрудники и отделы). У альтернативщиков это целая РАБОТА - надо же не поломать соседнюю бизнес-логику ;) Унас - поправить TFraEmployeeBase(код), если нужно отображение добавить - то поправить TFraEmployee и TFraEmployeeTbl(мышкой ;) и все - сколько бы ни было форм, где идет работа с сотрудниками, изменения произойдут везде.

Сразу хочется ответить на типичные вопросы, которые не раз слышал от всяких неучей ;) :
  1. Delphi живее всех живых.
  2. Фрэймы не зло - вы просто не умеете их готовить ;)
  3. Все получается быстро и удобно.

Ну и конце, таки попробую найти недостатки ;)
  1. Ну, на начальном этапе, пока вся обвязка только пишется, первые пол дня вы не сможете показать форму с кнопкой "Счастье" (хотя, когда обвязка готова, то в рамках такой реализации, "Счастье" будет появляться очень быстро ;)
  2. Единственный реальный недостаток - на формах в фреймами иногда подглючивает редатор и в dfm может попасть мусор, вплоть до того, что форма перестанет открыватся в IDE. Но ведь все пользуются хорошей системой контроля версий, делают регулярные коммиты и умеют читать/править dfm без IDE, правда? :)
Скачать пример

Немає коментарів:

Дописати коментар