Показаны сообщения с ярлыком рефакторинг. Показать все сообщения
Показаны сообщения с ярлыком рефакторинг. Показать все сообщения

вторник, 6 августа 2013 г.

"Синтез" логики форм приложения на тему "MVC". Или "анализ прецедентов сверху вниз". А также - "рефакторинг при проектировании". Пока БЕЗ UML

Предварительная вводная: Говоря о классах "реализации" - будем придерживаться нотации 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...

понедельник, 29 апреля 2013 г.

А вот тут - реальная проблема


TA = class(TInterfacedObject)
 procedure Subscribe(aList : TList);
end;
 
TB = class(TB, IUnknown)
 procedure UnSubscribe(aList : TList);
end;
 
procedure TA.Subscribe(aList : TList);
begin
 aList.Add(Pointer(IUnknown(Self))); // - кладём один указатель
end;
 
procedure TB.UnSubscribe(aList : TList);
begin
 aList.Remove(Pointer(IUnknown(Self))); // - пытаемся удалить - другой указатель, в итоге - не удаляем и получаем провисшую ссылку, а позже - огребаем AV
end;


-- понятно, что рафинированный пример крив и надуман. Но я встречался с подобным в жизни. Коллеги не дадут соврать. А пример лишь призван проиллюстрировать это.

Имейте эту проблему - в виду. Особенно когда у вас IUnknown "где-то в листах "залип"".

Сокращение времени жизни интерфейса

Gunsmoker про это тоже писал, но опять же хочется поставить акцент:


procedure Process;
begin
 DoSomething(TFactory.MakeInterface);
 DoSomething1;
 DoSomething2;
 // - интерфейс доживёт до сюда, это может быть проблемой
end;


Лучше сделать так:

procedure Process;
 
 procedure ProcessInterface;
 begin
  DoSomething(TFactory.MakeInterface);
  // - интерфейс доживёт лишь до сюда
 end;
 
begin
 ProcessInterface;
 DoSomething1;
 DoSomething2;
end;


Понятно, что тут есть проблема? (3)

Gunsmoker про это уже писал. Обстоятельно.

Но я просто хочу поставить акцент и проиллюстрировать проблему простым примером:

destructor TA.Destroy;
var
 l_Intf : ISomeInterface;
begin
 l_Intf := Self;
 DoSomething(l_Intf);
 inherited;
end;


- можем получить бесконечный вызов Destroy.

Подумайте об этом.

Один из способов избежать этого описан тут - http://18delphi.blogspot.com/2013/04/beforerelease.html

Понятно, что тут есть проблема? (2)

Код:

destructor TA.Destroy;
begin
 FreeAndNil(fA);
 FreeAndNil(fB);
 FreeAndNil(fC);
 inherited;
end;


- вообще говоря потенциально содержит проблему.

Корректнее было бы конечно написать так:

destructor TA.Destroy;
begin
 try
  FreeAndNil(fA);
 finally
  try
   FreeAndNil(fB);
  finally
   try
    FreeAndNil(fC);
   finally
    inherited;
   end; 
  end;
 end;
end;

Хотя конечно же - во всём надо знать меру.

Но! Если скажем внутри Free освобождается какой-нибудь ресурс - например закрывается файл или освобождается критическая секция, то может быть стоит подумать и о перфекционизме.

Один мой знакомый придумал другую технику - он складывает все ресурсы в "массив" (список etc) и освобождает в цикле, позволив выполнится каждой итерации. А вот Exception он поднимает на выходе.

В общем - поле для размышлений - тут есть.

суббота, 27 апреля 2013 г.

Почему я вдруг говорю о правках в VCL?

Сразу скажу, что далеко не все правки мои лично. И более того - многие люди, которые их делали - работают уже в иных организациях, нежели чем я.

Понятное дело, что МЫ свои проблемы - решили. И будем решать - дальше. И максимально возможное число правок перенесём в Delphi XE3 и XE4. И заодним напишем тесты - там где они не написаны.

Я не верю, что Embarcadero вдруг послушает нас и внесёт эти правки в очередной релиз. Я бы сам бы не стал слушать "какого-то Шуру Люлина со стороны". Более того - может быть найдёт контр-аргументы. Очень их интересно было бы послушать.

И уж точно я не хочу учить программировать "этих чудаков" из Borland'а. У меня - не тот уровень, чтобы кого-то учить.

Но! Пока мы считали, что Delphi - умирает. А Borland - уже умер - мы вносили эти правки по-тихому сами. И были счастливы. И мы думали, что наши правки так и останутся с нами на всю жизнь. И мы не думали, что они кому-то интересны.

Но тут нам показалось, что Delphi обретает вторую жизнь.

А посему - нам показалось, что было бы интересно поделиться нашими правками с окружающими.

Вдруг кому что пригодится. И вдруг люди осознают, что мы правы. Ну или укажут - где мы неправы. Что тоже было бы интересно. Может быть у нас просто - "руки кривые". А если - не кривые - может быть - пригодится. (Жаль кстати, что в блоге так мало критических комментариев - создаётся впечатление, что либо всё понятно, либо совсем всё не понятно, либо - пишу я полную чушь)

И вот тогда - если наберётся критическая масса - может быть и Borland (тьфу Embarcadero) прислушается к нам.

А пока - я пойду писать тесты. На то, что смогу.

Ни в коем случае этими правками - не хочу задеть ни Borland, ни Embarcadero. Ошибки делают все. У нас например - их больше. Но мы кстати и Turbo Vision когда-то правили. По делу. Например с STD и CLD. В поиске.

Мы давно выработали себе за правило - в первую очередь искать ошибку у СЕБЯ, а потом только в стандартных библиотеках.

Но и ошибки в стандартных библиотеках - встречаются. Ну или недопонимание.

Надеюсь, что эти посты будут вам полезны.

P.S. Я думаю, что на часть правок - мы сможем найти "тикеты" в нашей базе ошибок. Но думаю, что к сожалению - не на все.

Понятно, что тут есть проблема?


constructor TA.Create;
begin
 inherited;
 FB := TB.Create;
 DoSomethingThatCanRaise;
 FC := TC.Create;
end;
 
destructor TA.Destroy;
begin
 FreeAndNil(FB);
 FC.CallSomeMethod;
 FreeAndNil(FC);
 inherited;
end;


Ведь если DoSomethingThatCanRaise - обломится. То вызовется деструктор. Это гарантирует компилятор. А FC - не СОЗДАН. Ну и на FC.CallSomeMethod - огребём AV.

пятница, 26 апреля 2013 г.

Что я ещё обычно правлю в VCL и других сторонних библиотеках

Заменяю Free на FreeAndNil.

Понятно почему?

Обычный сценарий таков:


TA = class
private
 FField : TComponent;
protected
 procedure DoSomething;
 procedure DoSomething1; virual;
 destructor Destroy; override;
end;
 
TB = clas(TA)
protected
 procedure DoSomething1; override;
end;
 
 
destructor TA.Destroy;
begin
 FField.Free;
 DoSomething1;
 inherited;
end;
 
procedure TA.DoSomething;
begin
 if (FField <> nil) then
  FField.CallSomeMethod;
end;
 
procedure TA.DoSomething1;
begin
end;
 
procedure TB.DoSomething1;
begin
 DoSomething;
 inherited;
end;


-- никакой магии и никакого "хоккея" (http://18delphi.blogspot.com/2013/04/blog-post_8.html), но огребаем AV.

Посему - я обычно тупо беру FAR и Alt-F7. И тупо заменяю все Free на FreeAndNil. Хуже - уж точно не становится.

Update. Вот и обоснование - http://www.gunsmoker.ru/2009/04/freeandnil-free.html

И цитата оттуда:
"Заметьте, что речь идёт именно о замене Free на FreeAndNil везде. Не просто об использовании FreeAndNil, когда вы хотите проверять ссылку на nil, а именно - целиком и полностью везде. Т.е. не писать Free вообще никогда. Да, включая сценарии с локальными переменными.

Почему? Ну причина проста - нет никаких доводов так не делать (пожалуйста, дочитайте до конца). Зато есть доводы против использования Free в этих ситуациях."