Эта статья, надеюсь, будет полезна не только тем, кто хочет освоить Smalltalk, но и тем, кто желает получше разобраться в проблемах построения объектных систем. В Smalltalk-е классы являются полноценными объектами, и то, как это реализовано, является отличным примером построения развитой системы на основе нескольких простых принципов без излишнего усложнения.
(Впрочем, и связь с недавно вышедшей статьей Метаклассы в Objective-C отрицать не буду.)
Чтобы разобраться с понятием метаклассов нам понадобится всего два принципа, из которых мы сделаем (почти очевидные) логические выводы.
Первый принцип является основополагающим для объектного программирования в целом и, по идее, должен быть таковым для любого объектного языка (для Smalltalk в любом случае это так):
Второй принцип не является основополагающим для объектного программирования, но в Smalltalk (как и в большинстве современных объектных языков), к сожалению, тоже был заложен:
Чтобы не усложнять систему, вводя какие-то новые сущности или правила, можно воспользоваться уже имеющимся: метод
Это обстоятельство также показывает, что у каждого класса есть свой собственный метакласс. Больше одного метакласса на класс нам точно не нужно, но и по одному на каждый класс — тоже многовато. Зачем нужно так много?
В Smalltalk-76 на все классы был всего один метакласс. Но если несколько классов имеют один и тот же объект в качестве своего метакласса, то и поведение они имеют одинаковое, так как оно определено именно в метаклассе. Например, сообщение
Поэтому, для каждого класса в системе нам нужен один и ровно один метакласс, который определяет поведение этого класса. В Smalltalk метакласс автоматически создается «за кадром» при создании нового класса.
Учитывая, что метаклассы (см. первый принцип) являются объектами и (см. второй принцип) являются экземплярами некоторого класса, делаем вывод, что вышесказанное применимо и к самим метаклассам. Но есть и особенность: в отличие от «обычных» классов, метаклассам не нужно специфичное поведение — они все одинаковым образом хранят поведение своих экземпляров, больше от ничего не требуется. Поэтому уже нет необходимости заводить отдельный (мета-)метакласс на каждый метакласс, достаточно одного объекта, который определит поведение всех метаклассов в системе. Его назвали
И еще раз повторяем пройденное: метаклассом
Цепочка замкнулась, вот что у нас получилось:
Мы не затрагивали вопрос наследования, но здесь все просто: поскольку поведение классов наследуется так же, как поведение объектов, суперклассом метакласса C является метакласс суперкласса C:
В заключении — пара небольших замечаний, не относящихся к сути дела, но интересных.
Метаклассы не являются полноценными классами, так как первым не требуется вся функциональность последних:
(Впрочем, и связь с недавно вышедшей статьей Метаклассы в Objective-C отрицать не буду.)
Чтобы разобраться с понятием метаклассов нам понадобится всего два принципа, из которых мы сделаем (почти очевидные) логические выводы.
Первый принцип является основополагающим для объектного программирования в целом и, по идее, должен быть таковым для любого объектного языка (для Smalltalk в любом случае это так):
Всё является объектом, то есть может получать сообщения и реагировать на них.Классы так же подпадают под это самое «всё». Например, мы порождаем объекты, посылая сообщения классу, экземпляр которого мы хотим создать:
point := Point x: 1 y: 2.
Никаких сверхъестественных спец-операций по чудесному созданию объектов, только посылка сообщений. В данном случае мы посылаем сообщение #x:y:
классу Point
в надежде получить точку по двум координатам.Второй принцип не является основополагающим для объектного программирования, но в Smalltalk (как и в большинстве современных объектных языков), к сожалению, тоже был заложен:
Поведение объекта определяется его классом: в классе задается набор методов, описывающих реакцию экземпляров на то или иное сообщение.Так, все методы для точек определены в классе
Point
— это понятно. Мы можем узнать класс объекта, послав ему сообщение #class
.point class."-> Point"
Но где определен метод #x:y:
, который описывает обработку одноименного сообщения классом Point
?Чтобы не усложнять систему, вводя какие-то новые сущности или правила, можно воспользоваться уже имеющимся: метод
#x:y:
должен быть определен в классе, экземпляром которого является класс Point
. Конструкцию «класс класса» заменим термином «метакласс». Само собой, метакласс Point
можно получить послав сообщение #class
классу Point
:point class class."-> Point class"
Point class."-> Point class"
Как видим, метаклассу Point
не присвоено в системе собственное имя. Метаклассы обозначаются через Smalltalk-выражения, с помощью которых их можно получить.Это обстоятельство также показывает, что у каждого класса есть свой собственный метакласс. Больше одного метакласса на класс нам точно не нужно, но и по одному на каждый класс — тоже многовато. Зачем нужно так много?
В Smalltalk-76 на все классы был всего один метакласс. Но если несколько классов имеют один и тот же объект в качестве своего метакласса, то и поведение они имеют одинаковое, так как оно определено именно в метаклассе. Например, сообщение
#x:y:
в этом случае можно было бы послать любому классу: String
, Integer
и т.д. Очевидно, это не очень хорошо. Альтернативой является полное отсутствие кастомного поведения на стороне класса (что ничем не лучше), или (еще хуже) ставящая программиста в зависимость от потусторонних сил черная магия на уровне языка — как в большинстве «современных» мейнстримовых творений.Поэтому, для каждого класса в системе нам нужен один и ровно один метакласс, который определяет поведение этого класса. В Smalltalk метакласс автоматически создается «за кадром» при создании нового класса.
Учитывая, что метаклассы (см. первый принцип) являются объектами и (см. второй принцип) являются экземплярами некоторого класса, делаем вывод, что вышесказанное применимо и к самим метаклассам. Но есть и особенность: в отличие от «обычных» классов, метаклассам не нужно специфичное поведение — они все одинаковым образом хранят поведение своих экземпляров, больше от ничего не требуется. Поэтому уже нет необходимости заводить отдельный (мета-)метакласс на каждый метакласс, достаточно одного объекта, который определит поведение всех метаклассов в системе. Его назвали
Metaclass
:Point class class."-> Metaclass"
Основываясь на тех же принципах, и применяя ту же логику, делаем вывод о том, что Metaclass
так же должен иметь метакласс:Metaclass class."-> Metaclass class"
Обратите внимание, собственного имени метакласс Metaclass
-а (за ненадобностью) не удостоился.И еще раз повторяем пройденное: метаклассом
Metaclass
(как и всех метаклассов) является Metaclass
:Metaclass class class."-> Metaclass"
Цепочка замкнулась, вот что у нас получилось:
Мы не затрагивали вопрос наследования, но здесь все просто: поскольку поведение классов наследуется так же, как поведение объектов, суперклассом метакласса C является метакласс суперкласса C:
Point superclass."-> Object"
Point class superclass."-> Object class"
Object superclass."-> ProtoObject"
Object superclass class."-> ProtoObject class"
Данное правило (вынуждено) нарушается только там, где обрывается цепочка наследования: ProtoObject не имеет суперкласса, но его метакласс должен быть классом:ProtoObject superclass."-> nil"
ProtoObject class superclass."-> Class"
В заключении — пара небольших замечаний, не относящихся к сути дела, но интересных.
Метаклассы не являются полноценными классами, так как первым не требуется вся функциональность последних:
Metaclass superclass."-> ClassDescription"
И для тех, кто захочет полностью закончить картину, но не хочет запускать Smalltalk:Class superclass."-> ClassDescription"
ClassDescription superclass."-> Behavior"
Behavior superclass."-> Object"