Сообщений 0    Оценка 0        Оценить  
Система Orphus

Неудачные решения в Delphi - 2

Автор: Гумеров Максим Маратович
Опубликовано: 05.12.2012
Исправлено: 10.12.2016
Версия текста: 1.1
Проблемы интерфейсов как инструмента абстракции
ProcessMessages
Проблемы отладки
Управление исходными текстами
Вместо заключения

Во втором номере журнала за 2012 год была опубликована моя статья "Неудачные решения в Delphi". В ней был обозначен ряд проблем, для некоторых из которых были предложены решения, относительно других же предполагалось, что новые соображения по ним будут освещены в отдельной статье. В целях придания законченности материалу из №2, приведу здесь короткие комментарии и дополнения к отдельным его пунктам - вряд ли есть смысл воспроизводить здесь всю предыдущую статью.

Проблемы интерфейсов как инструмента абстракции

Другая проблема, обусловленная бесконтрольной оптимизацией, состоит в объявлении функций, получающих интерфейс как const-параметр. ...

Конечно, всегда доступно простое решение: выполнять приведение всякий раз, когда экземпляр передается в функцию. Это частично решает заодно и рассмотренную проблему усечения интерфейсов, но выглядит лексически и логически избыточно. Можно, с другой стороны, избегать объявления интерфейсов как const-параметров, и это еще удобнее, но функция, в которой три параметра const, а четвертый – нет, заставляет сомневаться о причинах этого. Еще одна интересная возможность состоит в повсеместном следовании принципам внедрения внешних зависимостей (dependency injection): вместо создания в методе экземпляров класса передавать в этот метод фабрику, создающую такие элементы и возвращающую их интерфейсы. В этом случае можно создание почти любых экземпляров спрятать внутрь фабрик, а создание самих фабрик сосредоточить на верхнем уровне приложения, а то и вовсе автоматизировать. Тем самым, поскольку на instance к моменту вызова f(instance) уже имелась интерфейсная ссылка, уничтожение ему не грозит.

...Второй неприятной стороной применения опциональных интерфейсов является повышенная сложность создания объектов-оберток. Если необходимо накрыть оберткой класс Derived, зная о нем только, что он является экземпляром класса Base (одного из предков для Derived), то, при условии, что принцип подстановки Б. Лисков выполняется, мы понимаем, что любой из методов Derived ведет себя как соответствующий метод Base, и если в коде, которому передается создаваемая обертка, нет условной логики, пытающейся привести Base к конкретным его подклассам (в т.ч. Derived), то обертка-мост Bridge должна всего лишь реализовать методы Base (это нормально, т.к. обертка в любом случае должна быть подклассом Base). Если есть условная логика, что, как уже сказано выше, само по себе плохо по ряду причин, то обертка должна быть уже подклассом Derived! Чтобы заинтересованный условный код мог привести ее к Derived. Ясно, что если обертка является подклассом оборачиваемого класса, то это выглядит неестественно, и вдобавок при внесении изменений в Derived (добавлении новых методов, например) соответственно необходимо не забыть изменить и Bridge. Не говоря уже о том, что нельзя сделать обертку над неким классом, не зная точно, что он является экземпляром Derived (т.е. не зная точный класс экземпляра), а зная лишь, что это потомок Base. Конечно, эти проблемы никак не связаны с использованием интерфейсов. Но интерфейсы их усугубляют.

Во-первых, преобразования типов к их потомкам нередко даже больше напрашиваются при работе с интерфейсами, чем с классами, особенно если интерфейсы ортогональны: клиентский код может не пытаться приводить интерфейсы-предки к их потомкам (понимая, что это неудачный подход), но при этом приводить интерфейсы к ортогональным им опциональным интерфейсам (считая это меньшим грехом). Так, если в класс добавляется метод Dispose, вызвать его, имея интерфейс IBase, можно, приведя IBase к IDerived, и это явно плохой подход. Но программистам может показаться, что завести новый интерфейс IDisposable и поддержать его в классе – более удачная идея: ведь таким образом концепция «IDisposable» отделяется от тех функций, которые выполняет интерфейс IBase. И для вызова Dispose не нужно приводить интерфейс IBase к какому-то его потомку IDerived, а достаточно привести к ортогональному интерфейсу IDisposable. Это провоцирует применение программистами таких преобразований типов.

Во-вторых, при наличии в коде таких преобразований типов, создать обертку, предоставляющую один интерфейс IBase, поддерживаемый данным экземпляром Class, можно только если в этой обертке поддержать все поддерживаемые интерфейсы класса Class. А это даже еще хуже, чем необходимость знать точный класс и все его методы (с чем мы сталкиваемся при условной логике на классах), т.к. каждый из этих интерфейсов может изменяться в будущем, как и их набор.

Еще один источник неприятностей при работе с интерфейсами связан с процессами рефакторинга, это функции, принимающие нетипизированные параметры. Примером опасной функции такого рода является FreeAndNil из SysUtils, которая принимает var-параметр, приводит его к TObject, вызывает ему деструктор, а затем очищает переданный параметр (тоже в виде, приведенном к TObject). Если повсеместно в программе использовался некий класс, а затем он был спрятан за интерфейс, и где-то при var instance: IUnknown забыли убрать вызов FreeAndNil(instance), то это вызов успешно скомпилируется, но эффект его будет непредсказуем.

ProcessMessages

...Автор рекомендует воздерживаться от использования ProcessMessages. 

Учитывая, что компоненты VCL могут сами вызывать ProcessMessages, один из относительно надежных способов предотвратить проблемы такого рода – блокирование попыток запуска новых операций до окончания текущей. Это несложно сделать, если в приложении есть унифицированный механизм запуска операций (к примеру, к его образованию может приводить следование паттерну «команда», включающему как раз обособление операций). Такой механизм может отследить начало и окончание выполнения операции, или заблокировать запуск.

Проблемы отладки

...следующий фрейм принадлежит уже не F, а той функции, которая вызвала F...

Такие функции можно перехватить (instrument) функциями со стековым фреймом, и эти перехватчики будут видны в стеке вместо перехваченных функций. Конечно, перехват вызовов функций – в принципе явление не очень распространенное, а в среде Delphi – тем более; но, возможно, существуют готовые решения: как минимум, это появившийся в Delphi XE «штатный» перехватчик вызовов виртуальных методов TVirtualMethodInterceptor. Можно и осуществить перехват самостоятельно, информацию по этой теме найти не так сложно, хотя бы в известной книге Дж. Рихтера "Programming Applications for Microsoft Windows", раздел “DLL injection and API hooking”. 

Управление исходными текстами

...При этом в .dpr-файл точно так же добавится инструкция $R, но дополнительно в .dproj добавится элемент <RcCompile>, вызывающий компиляцию .rc при сборке проекта.

Следует отметить, что в этой инструкции .res-файл указывается без полного пути к нему. Можно указать и полный путь, но в этом случае следует также указать в настройках проекта выходную папку компилятора ресурсов, т.к. если она не задана, то компиляция из IDE разместит .res-файл «рядом» с .rc-файлом, тогда как MSBuild разместит его в папке файла проектной группы (а искать его затем будет по тому пути, который указан в $R). А раз так, то либо IDE не найдет файл, либо MSBuild.

Вместо заключения

Около месяца назад я получил доступ к лицензии на Delphi XE3. Проведенные «на скорую руку» проверки показали, что почти все проблемы, освещенные в предыдущем номере журнала, в новой версии Delphi сохранились, – но исчезли проблемы, связанные с отладкой. Runtime-пакеты стандартной библиотеки Delphi в XE3 скомпилированы со включенными стековыми фреймами, так что исключение в Win32Check больше не приводит к гаданиям. Оператор «as» в вычислителе работает нормально. А использованный автором ранее для воспроизведения ошибки C0000029 образец группы проектов в XE3 к ошибке не приводит. Отрадно знать, что некоторые недостатки продукта, сохранившиеся даже в XE2, наконец отходят в прошлое.


    Сообщений 0    Оценка 0        Оценить