Предварительная вводная: Говоря о классах "реализации" - будем придерживаться нотации Delphi и будем иметь в виду архитектуру библиотеки VCL. Это необязательно. Но это - для определённости.
Пусть есть несколько "похожих"
прецедентов (
http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%86%D0%B5%D0%B4%D0%B5%D0%BD%D1%82_(UML)):
1. Работа с документом
2. Работа со словарной статьёй
3. Работа со статьёй энциклопедии
4. Обзор изменений документа
5. Сравнение соседних редакций документа
Прецеденты взяты из реальной системы. Надеюсь - этому никто не расстроится. Просто
качественно можно говорить об известном предмете, а не о "коне вакууме".
Надеюсь, что с одной стороны - никто не узрит в этом "выдачу технологических секретов", а с другой - "скрытую рекламу". Если бы не эти два обстоятельства - писать было бы сильно проще.
Этап сбора и описания требований - опускаю. Этой теме можно посвятить отдельную статью (если интересно). Пока предположим, что требования собраны и описаны. И частично - детализированы.
Прецеденты - РАЗНЫЕ, но по функциональности - ПЕРЕСЕКАЮТСЯ.
Что у них общего? Это - "работа с документом" в том или ином виде.
Понятное дело, что в реализации этих прецедентов - есть много общего, но и есть различия.
Понятное дело, все эти прецеденты визуализуются РАЗЛИЧНЫМИ формами
отображения.
Понятно, что эти формы - родственны, но при этом - они и различаются.
Что делать? Как будем проектировать функционал этих прецедентов?
Наследование? Визуальное наследование? Агрегация? Выделение бизнес-логики в отдельный слой/слои?
Хочется немножко рассказать на эту тему.
Для начала надо наверное выделить ОБЩИЕ ответственности для данных прецедентов:
1. Отображение документа.
2. Скроллирование документа.
3. Навигация курсором в рамках текста документа.
4. Выделение участков текста документа.
5. Копирование выделенных участков в буфер обмена.
6. Печать и предварительный просмотр всего документа.
7. Печать и предварительны просмотр выделенных участков документа.
8. Переход по ссылкам. Ссылки бывают - внутренние (внутри данного прецедента) и внешние (между различными прецедентами).
9. Логирование функций работы с документом.
10. Экспорт всего документа в определённые форматы.
11. Экспорт выделенного участка текста в определённые форматы.
пока - ограничимся...
Теперь попытаемся выделить функциональные различия данных прецедентов.
Для начала поймём - как эти прецеденты - визуализируются.
На самом деле каждый прецедент (отображаемый для определённости "формой" - наследником от TForm) распадается на несколько "
зон активностей" (или "зон пользовательского фокуса"). На самом деле эти "зоны" (забегая вперёд) это отдельные
вложенные прецеденты. На эту тему я немножко "закинул удочку" тут -
http://18delphi.blogspot.com/2013/08/usecase.html.
Каждая из зон характеризуется тем, что у них у каждой может быть СОБСТВЕННЫЙ набор Toolbar'ов, каждая может принимать ОТДЕЛЬНО фокус ввода, каждая имеет СОБСТВЕННОЕ контекстное меню, каждая может ВЛИЯТЬ на главное (командное) меню приложения.
Каждая из зон может обладать СОБСТВЕННЫМ
состоянием, которое может записываться в историю работы приложения.
Предположим, что распадаются они так (в скобках буду указывать имена "реализующих классов"):
1. Работа с документом (TDocumentForm):
а. Собственно "текст документа" (TDocumentTextViewArea).
б. Оглавление документа (TDocumentContentsViewArea):
б.1 Список структурных единиц документа (TDocumentStructureViewArea).
б.2 Список иллюстраций в документе (TDocumentPicturesViewArea).
в. Список входящих ссылок на документ (TDocumentIncomingLinksViewArea).
г. Список исходящих ссылок из документа (TDocumentOutgoingLinksViewArea).
д. Аннотация к документу (TDocumentAnnotationViewArea).
2. Работа со словарной статьёй (TDictionEntryForm):
а. Русский текст статьи (TRussianTextViewArea).
б. Английский текст статьи (TEnglishTextViewArea).
в. Список словарных статей (TDictionEntriesViewArea).
3. Работа со статьёй энциклопедии (TEncyclopediaForm):
а. Собственно "текст статьи" (TArcticleViewArea).
б. Оглавление энциклопедии (TEncyclopediaContentsViewArea).
4. Обзор изменений документа (TDocumentReviewForm):
а. Собственно "текст обзора документа" (TDocumentReviewTextViewArea).
5. Сравнение соседних редакций документа (TEditionsCompareForm):
а. Текст предыдущей редакции документа (TPrevEditionTextViewArea).
б. Текст "актуальной" редакции документа (TActualEditionTextViewArea).
Что это за классы TXXXViewArea и каково их место в иерархии того же VCL - мы поговорим - чуть позже.
Если вы хотите аналогию из "классического" Delphi и VCL, то скажем так, что TXXXViewArea - "похожи" на TFrame. Именно - ПОХОЖИ. Но НЕ ЯВЛЯЮТСЯ таковыми. Но ПОКА - так думать будет понятно (надеюсь).
Далее надо отметить, что каждый прецедент имеет "точку входа". Это нечто, что принимает "данные для конструирования прецедента" и возвращает собственно ЭКЗЕМПЛЯР прецедента.
В терминах "классики" это - конструктор класса (формы). Но если забегать вперёд и думать об "инъекции зависимостей" (
http://ru.wikipedia.org/wiki/Dependency_Injection), то это - фабрика. Фабрика прецедента.
Но! ПОКА забудем о фабриках. До поры до времени.
ПОКА будем считать, что "точка входа" в прецедент это конструктор объекта в терминах "классики".
Также ПОКА забудем о сигнатуре конструктора TForm (и TComponent) и что на самом деле он выглядит так - Create(anOwner : TComponent).
Также ПОКА забудем о "геометрии" и размещении форм (визуализующих прецеденты) в пространстве, а также о свойстве Parent.
Об этом мы поговорим чуть позже. Обещаю.
ПОКА будем считать, что "точка входа" выглядит примерно так CreateUseCase(aData : TUseCaseData).
Где TUseCaseData - некоторый базовый (в пределе -
шаблонный) класс, обеспечивающий связь данных прецедента и собственно логики и визуализации прецедента.
Забегая вперёд - отметим, что это скорее всего не класс, а -
интерфейс. Но на данном этапе - нам ПОКА это не важно.
Почему ИНТЕРФЕЙС? Потому, что - ПОДСЧЁТ ССЫЛОК. Пока - это - не важно. Но позже - я остановлюсь и на этом моменте.
(Для тех немногих, кто знаком с фреймворком VCM скажу, что TUseCaseData - это собственно и есть IsdsXXX).
Пока дело обстоит так. Есть "визизуализация прецедента" - TXXXForm и "данные прецедента" - TXXXUseCaseData.
TXXXForm обладает конструктором CreateUseCase, с сигнатурой - CreateUseCase(aData : TXXXUseCaseData).
Давайте теперь посмотрим - какие "данные прецедентов" нам понадобятся для визуализации описанных выше прецедентов.
Тут ПОКА - всё очень просто. Отображение тут - 1:1. Т.е. "визуализация прецедента" TXXXForm требует "данных прецедента" TXXXUseCaseData.
Хочется отметить тот факт, что в описываемой "модели" - "данные прецедента" - являются КОНСТАНТНЫМИ. Т.е. они не меняют своё состояние с момента своего рождения до момента своей смерти. Это означает, что собственно прецедент (и его визуализация) могут рассматривать эти данные как инвариант на всём времени своей жизни. Если "данные прецедента" надо поменять (например переходим из одного документа в другой), то это означает, что надо СМЕНИТЬ
текущий прецедент. Создав НОВЫЙ экземпляр, подав в "точку входа" (конструктор/фабрику) новые "данные прецедента". Этот ВАЖНЫЙ факт - мы тоже - обсудим. Чуть позже.
Тут бы уже можно было бы нарисовать модель прецедентов и их данных в терминах UML, но к сожалению - инструмент у меня недоступен - посему "буду писать текстом". Пока. А модель - нарисую чуть позже.
Итак - посмотрим на описанные выше прецеденты и их данные.
Пусть запись вида - TXXXForm <=| TXXXUseCaseData - означает "данные TXXXUseCaseData - могут порождать экземпляр прецедента TXXXForm". Т.е. что данные совместимы с входным параметром соответствующего прецедента.
Итак - прецеденты и их данные:
1. Работа с документом - TDocumentForm <=| TDocumentUseCaseData
2. Работа со словарной статьёй - TDictionEntryForm <=| TDictionEntryUseCaseData
3. Работа со статьёй энциклопедии - TEncyclopediaForm <=| TEncyclopediaUseCaseData
4. Обзор изменений документа - TDocumentReviewForm <=| TDocumentReviewUseCaseData
5. Сравнение соседних редакций документа - TEditionsCompareForm <=| TEditionsCompareUseCaseData
Теперь собственно рассмотрим - "что же такое эти TXXXUseCaseData":
Как мы уже "договорились" - они все наследуются от TUseCaseData.
Т.е. как-то так (ПОКА):
1. Работа с документом - TDocumentForm <=| TDocumentUseCaseData = class(TUseCaseData)
2. Работа со словарной статьёй - TDictionEntryForm <=| TDictionEntryUseCaseData = class(TUseCaseData)
3. Работа со статьёй энциклопедии - TEncyclopediaForm <=| TEncyclopediaUseCaseData = class(TUseCaseData)
4. Обзор изменений документа - TDocumentReviewForm <=| TDocumentReviewUseCaseData = class(TUseCaseData)
5. Сравнение соседних редакций документа - TEditionsCompareForm <=| TEditionsCompareUseCaseData = class(TUseCaseData)
Это - "крупными мазками".
Теперь поговорим об их наполнении.
Мы уже говорили о TXXXViewArea. "Активных зонах".
И "по индукции" было бы логично раз TXXXUseCaseData порождает TXXXForm, то должен найтись TXXXViewAreaData, который порождает TXXXViewArea.
Про "геометрию", Owner'ов и Parent'ов - опять же - ПОКА - не говорим.
Считаем, что TXXXViewArea имеет "конструктор" CreateViewArea(aData : TXXXViewAreaData).
Откуда берутся эти самые TXXXViewAreaData?
В описываемой модели - они "висят" в виде read-only-property на TXXXUseCaseData. И наследуются (забегая вперёд) от TViewAreaData.
(Для тех немногих, кто знаком с VCM скажу TXXXViewAreaData это ни что иное как IdsXXX)
И тогда картина описываемых прецедентов приобретает следующий вид:
1. Работа с документом - TDocumentForm <=|
TDocumentUseCaseData = class(TUseCaseData)
property DocumentText: TDocumentTextViewAreaData;
property DocumentContents: TDocumentContentsViewAreaData = class(TViewAreaData)
property DocumentStructure: TDocumentStructureViewAreaData;
property DocumentPictures: TDocumentPicturesViewAreaData;
end;//TDocumentContentsViewAreaData
property DocumentIncomingLinks: TDocumentIncomingLinksViewAreaData;
property DocumentOutgoingLinks: TDocumentOutgoingLinksViewAreaData;
property DocumentAnnotation: TDocumentAnnotationViewAreaData;
end;//TDocumentUseCaseData
2. Работа со словарной статьёй - TDictionEntryForm <=|
TDictionEntryUseCaseData = class(TUseCaseData)
property RussianText: TRussianTextViewAreaData;
property EnglishText: TEnglishTextViewAreaData;
property DictionEntries: TDictionEntriesViewAreaData;
end;//TDictionEntry
3. Работа со статьёй энциклопедии - TEncyclopediaForm <=|
TEncyclopediaUseCaseData = class(TUseCaseData)
property Arcticle: TArcticleViewAreaData;
property EncyclopediaContents: TEncyclopediaContentsViewAreaData;
end;//TEncyclopediaUseCaseData
4. Обзор изменений документа - TDocumentReviewForm <=|
TDocumentReviewUseCaseData = class(TUseCaseData)
property DocumentReviewText: TDocumentReviewTextViewAreaData;
end;//TDocumentReviewUseCaseData
5. Сравнение соседних редакций документа - TEditionsCompareForm <=|
TEditionsCompareUseCaseData = class(TUseCaseData)
property PrevEditionText: TPrevEditionTextViewAreaData;
property ActualEditionText: TActualEditionTextViewAreaData;
end;//TEditionsCompareUseCaseData
Итак - мы ПОКА имеем ЧЕТЫРЕ основных понятия - TXXXForm (прецедент), TXXXViewArea (активная зона/вложенный прецедент), TXXXUseCaseData (данные прецедента) и TXXXViewAreaData (данные активной зоны).
Теперь давайте более детально спроектируем один прецедент. Самый "большой". Первый. "Работа с документом":
Ранее мы ПОКА договорились, что TXXXViewArea это "нечто вроде TFrame". Пока этих знаний - нам достаточно.
Но кто же реально визуализирует данные и осуществляет интерактив с пользователем?
Этот момент - мы сейчас более-менее подробно - разберём.
Итак - наш прецедент "работа с документом" с точки зрения представления пользователю ПОКА выглядит вот так:
1. Работа с документом (TDocumentForm):
а. Собственно "текст документа" (TDocumentTextViewArea).
б. Оглавление документа (TDocumentContentsViewArea):
б.1 Список структурных единиц документа (TDocumentStructureViewArea).
б.2 Список иллюстраций в документе (TDocumentPicturesViewArea).
в. Список входящих ссылок на документ (TDocumentIncomingLinksViewArea).
г. Список исходящих ссылок из документа (TDocumentOutgoingLinksViewArea).
д. Аннотация к документу (TDocumentAnnotationViewArea).
Давайте разберёмся - какие конечные
компоненты визуализируют те или иные "активные зоны".
Пусть из требований следует что:
а. Собственно "текст документа" (TDocumentTextViewArea) - отображается в виде "текста с гиперссылками и прочим оформлением".
б. Оглавление документа (TDocumentContentsViewArea):
б.1 Список структурных единиц документа (TDocumentStructureViewArea) - отображается в виде "древесной структуры".
б.2 Список иллюстраций в документе (TDocumentPicturesViewArea) - отображается в виде "одноуровневого списка".
в. Список входящих ссылок на документ (TDocumentIncomingLinksViewArea) - отображается в виде "одноуровневого списка".
г. Список исходящих ссылок из документа (TDocumentOutgoingLinksViewArea) - отображается в виде "одноуровневого списка".
д. Аннотация к документу (TDocumentAnnotationViewArea)- отображается в виде "текста с гиперссылками и прочим оформлением" (как и сам текст документа).
Итак мы ПОКА имеем три вида отображения данных:
1. "текст с гиперссылками и прочим оформлением".
2. "древесная структура".
3. "список".
(Не могу не отметить, что для БОЛЬШИНСТВА бизнес-приложений этих способов представления данных - БОЛЕЕ чем достаточно)
Пусть у нас УЖЕ ЕСТЬ компоненты, которые умеет отображать структуры данных в заданном виде:
1. "текст с гиперссылками и прочим оформлением" - TDocumentView.
2. "древесная структура" - TTreeView.
3. "список" - TListView.
Тогда - будем ПОКА считать, что в каждую TXXXViewArea (которая ПОКА по договорённости является TFrame) - вставлен -
соответствующий компонент. Например как alClient (это для любителе Delphi). Больше про "геометрию", Owner'а и Parent'а - опять же - ПОКА не говорим.
Теперь как выглядит описание прецедента с точки зрения представления?
А вот так:
type
TDocumentForm = class(TForm)
DocumentTextViewArea : TDocumentTextViewArea = class(TFrame)
Text : TDocumentView;
end;//TDocumentTextViewArea
DocumentContentsViewArea: TDocumentContentsViewArea = class(TFrame)
DocumentStructureViewArea: TDocumentStructureViewArea = class(TFrame)
Tree : TTreeView;
end;//TDocumentStructureViewArea
DocumentPicturesViewArea: TDocumentPicturesViewArea = class(TFrame)
List : TListView;
end;//TDocumentPicturesViewArea
end;//TDocumentContentsViewArea
DocumentIncomingLinksViewArea: TDocumentIncomingLinksViewArea = class(TFrame)
List : TListView;
end;//TDocumentIncomingLinksViewArea
DocumentOutgoingLinksViewArea: TDocumentOutgoingLinksViewArea = class(TFrame)
List : TListView;
end;//TDocumentOutgoingLinksViewArea
DocumentAnnotationViewArea: TDocumentAnnotationViewArea = class(TFrame)
Text : TDocumentView;
end;//TDocumentAnnotationViewArea
end;//TDocumentForm
Проведём предварительный рефакторинг получившейся картины путём выделения "похожих" классов:
type
TDocumentForm = class(TForm)
TDocumentTextViewArea = class(TFrame)
Text : TDocumentView;
end;//TDocumentTextViewArea
DocumentTextViewArea : TDocumentTextViewArea;
TListViewArea = class(TFrame)
List : TListView;
end;//TListViewArea
DocumentContentsViewArea: TDocumentContentsViewArea = class(TFrame)
DocumentStructureViewArea: TDocumentStructureViewArea = class(TFrame)
Tree : TTreeView;
end;//TDocumentStructureViewArea
DocumentPicturesViewArea: TListViewArea;
end;//TDocumentContentsViewArea
DocumentIncomingLinksViewArea: TListViewArea;
DocumentOutgoingLinksViewArea: TListViewArea;
DocumentAnnotationViewArea: TDocumentTextViewArea;
end;//TDocumentForm
Но теперь встаёт вопрос - "откуда же берутся данные для соответствующих компонент"? Как TXXXUseCaseData и TXXXViewAreaData коррелируют с данными для
конечных компонентов?
Это вопрос - мы сейчас и разберём.
В общем - всё - по индукции.
TXXXUseCaseData - содержит в себе TXXXViewAreaData в виде read-only-property, значит - логично предположить, что TXXXViewAreaData содержит в себе TXXXViewData в виде всё тех же read-only-property.
В общем - так оно и есть.
Картина приобретает следующий вид:
type
TDocumentUseCaseData = class(TUseCaseData)
property DocumentText: TDocumentTextViewAreaData = class(TViewAreaData)
property Text: TDocument;
end;//TDocumentTextViewAreaData
property DocumentContents: TDocumentContentsViewAreaData = class(TViewAreaData)
property DocumentStructure: TDocumentStructureViewAreaData = class(TViewAreaData)
property DocumentStructure : TTree;
end;//TDocumentStructureViewAreaData
property DocumentPictures: TDocumentPicturesViewAreaData = class(TViewAreaData)
property Pictures : TList;
end;//TDocumentPicturesViewAreaData
end;//TDocumentContentsViewAreaData
property DocumentIncomingLinks: TDocumentIncomingLinksViewAreaData = class(TViewAreaData)
property IncomningLinks : TList;
end;//TDocumentIncomingLinksViewAreaData
property DocumentOutgoingLinks: TDocumentOutgoingLinksViewAreaData = class(TViewAreaData)
property OutgoingLinks : TList;
end;//TDocumentOutgoingLinksViewAreaData
property DocumentAnnotation: TDocumentAnnotationViewAreaData = class(TViewAreaData)
property Annotation: TDocument;
end;//TDocumentAnnotationViewAreaData
end;//TDocumentUseCaseData
(Для тех кто в курсе про VCM: TDocument это и есть TXXXDocumentContainer)
И ОПЯТЬ! Проведём предварительный рефакторинг получившейся картины путём выделения "похожих" классов:
type
TDocumentUseCaseData = class(TUseCaseData)
TDocumentTextViewAreaData = class(TViewAreaData)
property Text: TDocument;
end;//TDocumentTextViewAreaData
property DocumentText: TDocumentTextViewAreaData;
TListViewAreaData = class(TViewAreaData)
property List : TList;
end;//TListViewAreaData
property DocumentContents: TDocumentContentsViewAreaData = class(TViewAreaData)
property DocumentStructure: TDocumentStructureViewAreaData = class(TViewAreaData)
property DocumentStructure : TTree;
end;//TDocumentStructureViewAreaData
property DocumentPictures: TListViewAreaData;
end;//TDocumentContentsViewAreaData
property DocumentIncomingLinks: TListViewAreaData;
property DocumentOutgoingLinks: TListViewAreaData;
property DocumentAnnotation: TDocumentTextViewAreaData;
end;//TDocumentUseCaseData
Идея понятна?
Вот тут наверное подходящий момент, чтобы остановиться и спросить - "а зачем так сложно?"
Я сейчас постараюсь это пояснить.
Во-первых - мне кажется, что в общем - НИЧУТЬ не сложно. Процедура - ФОРМАЛЬНАЯ. И устроена - как "матрёшка", что само по себе (меня лично - радует).
Выделяем прецеденты, потом описываем вложенные прецеденты, потом описываем данные прецедентов и вложенных прецедентов. Потом - детализируем прецеденты и получаем компоненты, которые взаимодействуют с пользователем, потом - описываем - данные для компонентов. Процедура - ФОРМАЛЬНАЯ и рекурсивная.
А во-вторых - вся эта "сложность" во-первых направлена на взаимозаменяемость и гибкость архитектуры, а во вторых - на упрощение рефакторинга и повышение ПОВТОРНОГО использования.
Как? Поясню - чуть позже.
Собственно это станет видно - когда мы перейдём к оставшимся прецедентам.
(Тут "синергия" видна ИМЕННО для СЛОЖНЫХ систем и для систем в которых есть МНОЖЕСТВО "похожих", но в то же время "не похожих" прецедентов)
.....
Теперь вернёмся к списку ответственностей (требований) наших прецедентов.
ПРЕДПОЛОЖИМ, что указанные ответственности:
1. Отображение документа.
2. Скроллирование документа.
3. Навигация курсором в рамках текста документа.
4. Выделение участков текста документа.
5. Копирование выделенных участков в буфер обмена.
-- ПОЛНОСТЬЮ реализуются компонентом TDocumentView (ну - ПОВЕЗЛО нам так с указанным компонентом).
Соответственно эти ответственности - мы смело можем считать реализованными (ну по крайней мере - пока требования не поменяются). И мы СМЕЛО можем их вычеркнуть из списка ответственностей, которые нам надо реализовать. Считаем их - УЖЕ реализованными.
Как быть с ОСТАЛЬНЫМИ ответственностями? А именно:
6. Печать и предварительный просмотр всего документа.
7. Печать и предварительны просмотр выделенных участков документа.
8. Переход по ссылкам. Ссылки бывают - внутренние (внутри данного прецедента) и внешние (между различными прецедентами).
9. Логирование функций работы с документом.
10. Экспорт всего документа в определённые форматы.
11. Экспорт выделенного участка текста в определённые форматы.
На этот вопрос - мы сейчас попробуем ответить. Реализуя далее наш "большой" прецедент - "работа с документом":
Делать мы это будем тут -
http://18delphi.blogspot.com/2013/08/mvc_8.html
to be continued...