Статические = динамизм
Объектно-ориентированное программирование давно утратило прежний задор и превратилось в один их возможных стилей разработки. В моду вошли новые штучки, которые с тем же азартом обещают осчастливить мир единственно правильным и неповторимым образом; ладно, пусть немного остынут — и займут свое место среди всего прочего. А пока — все современные языки программирования имеют встроенные средства ООП, необходимые хотя бы для того, чтобы завлекать новых приверженцев из рядов бывших конкурентов.
В действительности объектно-ориентированный стиль — это эклектическая смесь двух основных парадигм: объект и класс. Объекты — в общих чертах представляют собой набор данных (свойства) и кусков кода (методы); классы — это, по сути дела, библиотеки подпрограмм для доступа к свойствам и методам объекта, рассматриваемого как экземпляр класса (хотя тот же объект может, в принципе, быть экземпляром другого класса или синглетоном, или еще чем-нибудь). На уровне языка объекты и классы организуют для своих компонент относительно замкнутые пространства имен — однако в статически компилируемом коде эта возможность практически обессмысливается.
Принято считать, что традиционный императивный стиль противоположен объектно-ориентированному. Императивно построенная программа представляет собой, грубо говоря, последовательность (возможно, распараллеленную) вызовов функций (операторов), работающих в контексте соответствующих входных данных и системного окружения, а иногда имеющих ряд параметров, определяющих индивидуальный контекст (замыкания). Задать раздельные пространства имен в императивном программировании не представляет особого труда: можно использовать специальные соглашения об именовании, блоки и модули, препроцессоры и т. д. Степень инкапсуляции не зависит от предпочитаемого стиля: существуют объектно-ориентированные языки без малейших следов инкапсуляции — тогда как императивный код может всегда быть разбит на полностью изолированные фрагменты, взаимодействующие только через специальный интерфейс
Иерархичность — общая черта как объектно-ориентированного, так и императивного стиля. Объекты состоят из других объектов, классы наследуют что-то от других классов, функции могут вызывать другие функции.
Никакой принципиальной разницы между императивным и объектно-ориентированным программированием нет. И однако в некоторых языках со строгой типизацией (вроде Java или C#), идея класса раздута до абсурда — и нельзя определить данные или код иначе как через специально придуманный класс. Как и везде, излишняя строгость превращается в полную бессмыслицу, делает язык громоздким и уродливым, невыразительным и непрозрачным.
Один из типичных способов скрытого ухода от объектно-ориентированной идеологии — статические свойства и методы. Это не что иное как возможность скормить системе что-то необъектное через задний проход. Нет абсолютно никакой разницы между следующими фрагментами кода (не привязанного к какому-либо конкретному языку):
class QQ { static public DWORD Zero = 0; }
DWORD x = QQ.Zero;
|
и
#define QQZero 0;
DWORD x = QQZero;
|
кроме того, что второй вариант проще и логически прозрачнее. Аналогично, статические методы класса — это просто обходной путь для определения самостоятельных функций, не связанных ни с каким классом вообще. Единственным оправданием могла бы послужить потребность разделения областей видимости — но для этого не нужны классы, достаточно определить пространства имен, которые представляют собой лишь контексты трансляции и не имеют отношения к типизации (для чего и служат классы). Оставаясь своего рода расширением инструкций препроцессора, пространства имен сродни интерфейсам, шаблонам и прочим языковым конструкциям, призванным преодолеть врожденную заскорузлость классов. В реальной жизни, хорошее соглашение о стандартах именования оказывается куда полезнее и удобней, чем лингвистические извращения.
Могут возразить, что, помещая статические компоненты в класс, мы задает тем самым специфические права доступа к ним. Лично мне по жизни ни разу не приходилось сталкиваться с ситуациями, в которых подобные ограничения были бы действительно нужны — большей частью, они лишь запутывают логику взаимодействия и становятся головной болью для программиста, когда приходится приспосабливать чужой код к изменившимся требованиям заказчика. В таких делах минимальный корпоративный стандарт намного эффективнее и удобнее, в смысле гибкости и логической прозрачности.
К тому же, когда статические свойства и методы уникальны в пределах проекта, даже соображения разделения доступов и областей видимости не оправдывают складирования их в класс. Это бросается в глаза, например, в отношении метода main() в программах на Java и C#, который не имеет ничего общего с объектным поведением, а просто ссылается на операционное окружение, на способ создания управления процессами в операционной системе. В терминах классов, можно было бы представить себе класс Task, который описывает всевозможные программы (вычислительные операции) — и произвести от него класс MyTask, каким-то образом определяя для него индивидуальный конструктор, эквивалент main(). Потом экземпляр класса TaskManager (созданный экземпляром класса OperationSystem, созданным экземпляром класса BIOS, созданным экземпляром класса Computer, созданным экземпляром класса PowerButtonPresser и так далее) вызывает встроенный метод StartTask(Task* pointer_to_task_definition) принимающий в качестве параметра определение класса, содержащее что-то вроде
Task instance_of_mytask = pointer_to_task_definition –> new();
|
Это наглядно показывает отличие программного материализма от объектно-ориентированного идеализма — то есть, определение класса трактуется как объект, а не как искусственная конструкция этапа компиляции. Практические реализации таких объектов могут быть разным, от чистых интерпретаторов (в духе JavaScript) до динамически подгружаемых библиотек в машинном коде (подобно технологии ActiveX) — с промежуточными вариантами вроде JVM, интерпретирующих частично оттранслированный код. И однако можно легко понять, что у всех этих реализаций есть нечто общее — протокол. Именно наличие таких стандартных соглашений позволяет работать с классами, совместимыми с данным протоколом, — в этом смысле протокол можно было бы назвать классом (или, скорее, категорией) определений класса. В частности, протокол задает способ построения класса по его определению. Так, можно потребовать, чтобы всякое определение класса (трактуемое как объект) имело стандартную точку входа, соответствующую методу new(). Но можно сделать иначе — динамически вызывать этот метод из определения класса. Разумеется, никто не мешает придумать еще тысячу возможных протоколов.
В каком-то смысле, протокол можно уподобить виртуальной машине, которую можно запустить для обработки соответствующих классов в любом окружении. Если подходящей виртуальной машины не найдется — операционная среда просто не сможет запускать задачи такого типа и вернет стандартный код ошибки, означающий что-то вроде UNKNOWN_PROTOCOL.
Возвращаясь к статическим компонентам классов (свойствам или методам), отметим, что они представляют собой часть описания особой задачи — реализуют стандартный метод new() для создания среды выполнения. А потому совершенно не нужно включать их в определение вызываемого класса. Функция main() в такой трактовке понимается как реализация (экземпляр) некоторой виртуальной машины (протокола) для обработки динамически определяемых классов. В целом, вычислительный процесс образует иерархию протоколов, реализованных как виртуальные машины разных уровней, — и можно сделать следующий шаг, рассматривая не только фиксированные иерархические структуры, но также возможные обращения иерархии и ее развитие. Но это уже другая история.
|