Make your own free website on Tripod.com

 

 
Introduction Downloads Applications News FAQ
Forum Subscribe Articles Links Other

 

 Key Objects Library     

 Key Objects Library     

Почему так, а не иначе...
(идеологическое FAQ)

   У тех, кто уже освоился с Delphi VCL, при первом знакомстве с KOL часто возникает сомнение в необходимости делать все иначе, чем это делается в VCL (и даже в XCL или ACL). Но, если мы ставим задачу - получить наиболее компактный код, то приходится кое-чем жертвовать. Например, в объектно-ориентированном программировании наследование является одним из основопологающих принципов (но не самым основным, все-таки). Очень заманчиво при разработке библиотеки использовать механизмы наследования для создания дерева визуальных объектов (например). Однако, в этом случае пришлось бы использовать виртуальные методы, а их реализация не настолько хороша, как этого бы хотелось. Например, компилятор Delphi не умеет определять на этапе компиляции, какие из виртуальных методов не используются. И следовательно, предотвратить включение их в код конечной программы. Что, в свою очередь, влечет вставку кода, на который имеются ссылки из этих методов, и т.д.
   По этой причине при разработке KOL сделана попытка использовать наследование в крайних случаях. Например, все визуальные контролы представлены единственным объектом типа TControl, который, в зависимости от того, какой функцией-конструктором был создан, может выполнять роль различных объектов - формы, окна приложения, кнопки, метки, и т.д. Виртуальные методы здесь заменяются указателями на функции, устанавливаемые при конструировании объекта (что очень напоминает механизм событий).
   Разумеется, при таком подходе пришлось максимально обобщить представление о визуальных контролах, и независимо от того, для какой цели объект TControl создан, он имеет некоторый общий набор свойств, методов и событий. Причем, для данного конкретного визуального элемента некоторые свойства могут просто не иметь смысла.
   Позволю себе привести аналогию из реального мира. Если, например, попытаться создать объект, обобщающий в себе стол, телевизор и будильник, то придется ввести операцию (назовем ее Activate), которая будет иметь смысл для будильника (завод) и телевизора (включение), но для которой вряд ли найдется аналог в отношении стола. Если же провести аналогию со свойством Parent, то ясно, что и телевизор, и будильник могут иметь в качестве родителя стол (т.е., их можно поставить на стол), а при желании - будильник можно поставить на телевизор (т.е. телевизор может быть "родителем" для будильника). А вот сам будильник, скорее всего, уже ничьим "родителем" быть не может (т.е. нельзя на него поставить никакой другой предмет мебели). Тем не менее, программист вряд ли будет сильно огорчаться по поводу того, что у будильника есть избыточное свойство Parent, а у стола - пустая функция Activate, и просто не будет их  использовать.

   Второй вопрос - почему используются не классы, а объекты. Ответ: потому, что объектов более чем достаточно для того, чтобы делать объектно-ориентированную библиотеку. Это (несколько) экономит код и сокращает расходуемую память. И кроме всего прочего, повышает требования к разработчику библиотеки, заставляя его (меня!) избегать использования некоторых возможностей классов, щедро предоставленных фирмой Inprice Inc., но влекущих за собой генерацию крайне неэффективного кода. Кроме того, объекты всегда можно превратить в классы (это на случай, если в следующей версии Delphi от поддержки объектов откажутся).

   Третий вопрос. Зачем тогда вообще объекты, если не используется наследование? Неужели только ради красоты, в смысле, возможности написать Button1.Left = 20 вместо SetLeft( Button1handle, 20 ) ? То есть, в чем преимущество объектного подхода перед обычным структурным программированием - только ли в возможности использовать синтаксическую конструкцию вида объект.метод(параметры) вместо процедура(объект,пераметры)? Ответ будет таков, что не только. Есть еще такое понятие как "ошибка". Объектный подход позволяет избежать программисту, использующему объекты, большего числа ошибок, по сравнению с использованием просто набора глобальных процедур и функций. Причем во многих случаях - еще на этапе написания программы (code insight предложит только те методы/свойства, которые относятся к данному типу объектов, компилятор откажется компилировать вызов метода, не описанного в данном объектом типе и т.д.).
   Другое дело, что я сам себя как разработчика библиотеки KOL такой возможности во многом лишаю. (Так как фактическая реализация объектов в KOL опирается во многом на использование обычных процедур и функций, лежащих вне объектов, которые этими процедурами / функциями обслуживаются). Но это ведь доставлет неудобство только мне, не правда ли?

   Не увеличивает ли (уже само по себе) большое число методов и свойств объекта размер конечной программы? Ни в коем случае. Для тех, кто не в курсе, сообщаю, что компилятор Delphi умеет делать так называемый "smart-linking", т.е. не включать в код методы, не использующиеся в программе. А библиотека KOL спроектирована так, что большинство свойств и методов друг от друга не зависят, т.е. соответствующие методы действительно попадают в откомпилированную программу только в случае наличия к ним обращений из того кода, который сделал программист в своем проекте.
   Более того, некоторые подчиненные объекты не создаются априори - в конструкторе главного объекта, и их создание откладывается на более поздний срок - до того момента, когда произойдет обращение (изменение) соответствующих свойств или вызов какого-то метода, играющего роль активатора.
   Хороший пример - это реализация свойства Font для объектов TControl в бибиотеке KOL. Только в случае доступа к этому свойству происходит создание соответствующего объекта типа TGraphicTool, и компилятор получает информацию о необходимости включения дополнительных методов. И хотя визуальные объекты в KOL умеют выполнять "наследование" шрифта от своего родителького объекта (имеется в виду именно отношение родительское окно - подчиненное окно, а не наследование в смысле ООП), это приводит изначально лишь к включению в код очень маленькой процедурки TGraphicTool.Assign. В случае, если к свойству Font обращений в программе не было, и такие объекты не создавались, то Assign получит в качестве @Self пустой указатель, и не будет ничего делать. Если же создавались, то Assign вызовет соответствующую процедуру по ее указателю, в данном случае - AssignFont.
   Возможно возражение: объект TControl приходится снабжать довольно большим количеством полей данных, которые могут не использоваться. Но во-первых, добавление неиспользованного поля данных увеличивает размер памяти, потребляемой экземпляром объекта, незначительно (указатель или число Integer занимает 4 байта, а перечислимый тип или boolean - 1 байт). А во-вторых, эта память выделяется динамически и на размер исполнимого файла вообще не влияет.

   В дополнение к предыдущему ответу, замечу (для тех, кто не в курсе), что свойство (объявленное как property) - до тех пор, пока оно не включено в раздел published, является не более чем синтаксической конструкцией и не увеличивает код само по себе. Фактически, при обращении к свойству компилятор заменяет его на вызовы соответствующих методов (указанных в полях read и write в декларации свойства). Никакой речи о включении свойства в RTTI (даже класса) речь не идет, пока оно не находится в разделе published. Тем более разговор об RTTI объекта не имеет смысла.

   Как работает механизм внутренних "событий", заменяющий собой виртуальные методы? Или, иначе - не увеличивается ли код программы из-за того, что некоторый метод объекта TControl должен делать разные вещи в зависимости от того, для какого объекта этот метод вызван?
   Собственно, первый вариант вопроса уже содержит в себе значительную часть ответа. Из него ясно, что никакой проверки на предмет, каким именно визуальным объектом является Self, и ветвления по возможным случаям не происходит. Обычно просто вызывается процедура / функция по указателю. А указатель устанавливается где-нибудь в конструкторе визуального объекта данного вида. В некоторых случаях, когда различие в функционировании сводится к различию в номере сообщения, посылаемого окну объекта - решение еще проще: просто номер сообщения выбирается из (заполненной в конструкторе) таблицы. Иногда присваивание указателю конкретной функции откладывается на более поздний момент - до обращения к определенным свойствам или методам.
   Все это помогает компилятору отбросить ненужные методы и существенно сократить размер исполнимой программы.

   Почему используются базовые визуальные объекты Windows - вместо рисования своих (как в XCL)? Ведь это приводит к увеличению потребления ресурсов.
   Главная причина - это стремление воспользоваться стандартными средствами и избежать большого кода. Использование встроенных в Windows визуальных объектов в этом плане очень облегчает задачу. Рисуют они себя сами, большинство сообщений отрабатывают автоматически. Потребляют ресурсы, конечно, больше. Но мы ведь делаем маленькие приложения, содержащие не так уж много визуальных элементов на форме. Да и новые версии Windows менее критичны по отношению к ресурсам.
   Есть так же мнение некоторых пользователей, что использование mfc-контролов уменьшает переносимость программ на другие платформы. Отвечаю: прежде всего меня интересует переносимость между различными версиями Windows и Delphi. Когда я слышу о необходимости делать все совместимым с Kylix, я тихо радуюсь при мысли о том, что кого-то волнует совместимость со средой, установленной в качестве ОС на 1% PC-совместимых машин. Мое мнение: для Kylix/Linux надо просто делать другую версию. Может быть, синтаксически совместимую с этой. Но в любом случае переносимости достичь без изменения кода самой библиотеки нельзя. Просто потому, что API все равно чем-то отличается.

   Впрочем, дополняя ответ на вопрос, заданный выше, скажу, что аналоги VCL-ного TGraphicControl (и XCL-ного TCustomControl) все-таки планируются и в KOL. Вот только дублировать имеющиеся (т.е. реализованные на базе встроенных в Windows) визуальные объекты я буду избегать. Вместо этого попробую использовать их там, где они действительно могут оказаться полезны. Например, для реализации кнопки, которая могла бы менять свой цвет и другие визуальные характеристики, подобно TBitBtn в VCL (стандартная mfc-кнопка этого делать не позволяет).

   Не слишком ли KOL (да и предшествующая ей XCL) похожа на VCL? Может быть, я ощущаю излишнее влияние со стороны библиотеки, противоположность которой пытаюсь создать? Отвечаю. Во-первых, не слишком. Как раз в той мере похожа, в какой это удобно, чтобы ее использовать. Потому что на VCL разрабатывать программы мы продолжать будем и впредь. Когда речь пойдет о больших проектах, о работе с базами данных, и т.п. Посему библиотеку удобно сделать по схожим во многом принципам, чтобы в случае необходимости сделать небольшую программу не пришлось в корне менять свои привычки и стиль программирования. Влияние - да, ощущаю.  Но опять же, лишь в той степени, какая необходима. Единственное существенное отличие KOL от VCL - это максимальное использование возможностей компилятора по отбрасыванию ненужного кода. Все прочие отличия (в основном) диктуются именно этим требованием. И еще. Я не пытаюсь создать противоположность VCL. Я пытаюсь сделать KOL так, как могла бы быть сделана VCL, если бы к ее разработке подошли с моих позиций.

Кладов Владимир, 2000, октябрь.