Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Простая объектная модель

Оригинал: A Simple Object Model
Автор: Carl Friedrich Bolz
Дата публикации: 07 October 2015
Перевод: Н.Ромоданов
Дата перевода: май 2016 г.

Глава 6 из предварительной версии книги "500 Lines or Less", которая входит в серию "Архитектура приложений с открытым исходным кодом", том 4.

Creative Commons

Перевод сделан в соответствие с лицензией Creative Commons. С русским вариантом лицензии можно ознакомиться здесь.

Сегодня мы представляем шестую главу предварительной публикации нашего нового сборника «500 строк или меньше». Глава написана Карлом Фредриком Больцем (Carl Friedrich Bolz).

Как и обычно, если вы обнаружите ошибки, о которых, как вы считаете, стоит сообщить, пожалуйста, сделайте запись на нашем треккере GitHub.

Приятного чтения!

Это предварительная публикация главы из сборника «500 строк или меньше», четвертой книги из серии книг Архитектура приложений с открытым исходным кодом. Пожалуйста, сообщайте в нашем треккере GitHub о любых проблемах, которые вы обнаружите при чтении этой главы. Следите в блоге AOSA или в Твиттере за объявлениями о предварительных публикациях новых глав и окончательной публикацией.

Карл Фридрих Больц является научным сотрудником Королевского колледжа в Лондоне и его интересуют вопросы реализации и оптимизации всех видов динамических языков. Он является одним из основных авторов проекта PyPy/RPython, принимал участие в разработке языков Prolog, Racket, Smalltalk, PHP и Ruby. Его можно найти в Твиттере по ссылке @cfbolz.

Введение

Объектно-ориентированное программирование является одной из основных используемых сегодня парадигм программирования и различные виды объектно-ориентированного подхода присутствуют в огромном количестве языков. Хотя внешне механизмы этих различных объектно-ориентированных языков программирования предоставляют программисту очень похожие возможности, их детали могут сильно различаться. Общим для большинства языков является наличие объектов и некоторого механизма наследования. Но что касается классов, то это та особенность, которая не в каждом языке поддерживается напрямую. Например, в языках на основе прототипов, например, Self или JavaScript, понятие класса не существует, а объекты наследуются непосредственно друг от друга.

Интересно разобраться в особенностях различных объектных моделях. Часто оказывается, что в различных языках есть общее сходство. Полезно поместить модель нового языка в контекст моделей других языков, быстро разобраться с этой новой моделью и почувствовать себя более комфортно в сфере разработки языков программирования.

В этой главе изучается реализация ряда очень простых объектных моделей. Мы начнем с простых экземпляров и классов и с возможности вызывать методы на конкретных экземплярах объектов. Это "классический" объектно-ориентированный подход, который был создан в самых первых объектно ориентированных языках, таких как Simula 67 и Smalltalk. Затем эта модель будет постепенно расширена и следующими двумя этапами будет изучение различных вариантов проектирования языков, а в завершении будут рассмотрены вопросы повышения эффективности объектной модели. Итоговая модель не будет моделью реального языка, но это будет идеализированная и упрощенная версия объектной модели языка Python.

Объектные модели, представленные в этой главе, реализованы на языке Python. Код работает как в версии Python 2.7, так и в версии 3.4. Для того, чтобы можно было лучше разобраться в свойства модели и ее структуре, для каждой объектной модели будут также приведены тесты. Тесты можно запустить с помощью py.test или nose.

Выбор языка Python в качестве языка реализации абсолютно нерационален. "Настоящая" виртуальная машина обычно реализуется на языке низкого уровня, например, на C/C++, а для того, чтобы виртуальная машина была эффективной, потребуется уделить очень много внимания всяким инженерным частностям. Но простой язык реализации позволит легче сосредоточиться на конкретных различиях свойств моделей и не погрязнуть в деталях реализации.

Модель, базирующаяся на методах

Объектная модели, с которой мы начнем, является чрезвычайно упрощенной версией объектной модели языка Smalltalk. Smalltalk это объектно-ориентированный язык программирования, разработанный в 70-х годах группой Алана Кея (Alan Kay) в Xerox PARC. В этом языке популяризировалось объектно-ориентированное программирование, и он является первоисточником многих возможностей, которые сейчас можно найти в сегодняшних языках программирования. Одним из основных принципов разработки языка в Smalltalk был принцип "все — это объекты". Сегодня самым непосредственным преемником языка Smalltalk является язык Руби, синтаксис которого больше похож на синтаксис языка C, но в нем сохранилась основная часть объектной модели языка Smalltalk.

Объектная модель, рассматриваемая в данном разделе, будет иметь классы и экземпляры объектов этих классов, средства чтения и записи атрибутов объектов, механизм вызова методов объектов, и возможность класса быть подклассом другого класса. С самого начала, классы будут абсолютно обычными объектами, в которых также могут быть атрибуты и методы.

Замечание: В этой главе используется термин "экземпляр", который означает "объект, не являющийся классом".

Хорошее правило начинать с написания теста, определяющего поведение объекта, который должен быть реализован. Все тесты, представленные в этой главе, состоят из двух частей. Во-первых, немного обычного кода на языке Python, определяющего и использующего несколько классов и дополнительные свойства объектной модели Python. А, во-вторых, соответствующие тесты, будут применяться не к обычным классам языка Python, а к объектным моделям, которые мы будем реализовывать в этой главе.

В этих тестах вручную прописываетс соотношение между обычными классами языка Python и нашей объектной модели. Например, вместо того, чтобы в языке Python писать obj.attribute, мы в объектной модели будем пользоваться методом obj.read_attr("attribute"). При реализации реального языка эта часть будет отрабатываться с помощью интерпретатора языка или компилятора.

С тем, чтобы еще упростить наше обсуждение, мы в данной главе мы не будем обращать внимание на существенную разницу между кодом, реализующим объектную модель, и кодом, используемым для записи методов в объектах. В реальной системе эти две части кода обычно реализуется на разных языках программирования.

Начнем с простого теста, который читает и записывает поля объекта.

def test_read_write_field():
    # Код на языке Python
    class A(object):
        pass
    obj = A()
    obj.a = 1
    assert obj.a == 1

    obj.b = 5
    assert obj.a == 1
    assert obj.b == 5

    obj.a = 2
    assert obj.a == 2
    assert obj.b == 5

    # Код объектной модели
    A = Class(name="A", base_class=OBJECT, fields={}, metaclass=TYPE)
    obj = Instance(A)
    obj.write_attr("a", 1)
    assert obj.read_attr("a") == 1

    obj.write_attr("b", 5)
    assert obj.read_attr("a") == 1
    assert obj.read_attr("b") == 5

    obj.write_attr("a", 2)
    assert obj.read_attr("a") == 2
    assert obj.read_attr("b") == 5

В тесте используются три вещи, которые мы должны реализовать. Классы Class и Instance представляют собой классы и экземпляры нашей объектной модели, соответственно. Есть два специальных экземпляра класса: OBJECT и TYPE. OBJECT соответствует objectв языке Python и является конечным базовым классом в иерархии наследования. TYPE соответствует type в языке Python и является типом всех классов.

Для того, чтобы можно было что-нибудь делать с экземплярами Class и Instance, в них необходимо реализовать разделяемый интерфейс, наследуемый из разделяемого базового класса Base,в котором предоставлено несколько методов:

class Base(object):
    """ Базовый класс, из которого наследуются все классы объектной модели. """

    def __init__(self, cls, fields):
        """ Каждый объект имеет класс. """
        self.cls = cls
        self._fields = fields

    def read_attr(self, fieldname):
        """ Чтение из объекта поля 'fieldname' (имя поля) """
        return self._read_dict(fieldname)

    def write_attr(self, fieldname, value):
        """ Запись в объект поля 'fieldname' (имя поля) """
        self._write_dict(fieldname, value)

    def isinstance(self, cls):
        """ Возвращает True, если объект является экземпляром класса cls """
        return self.cls.issubclass(cls)

    def callmethod(self, methname, *args):
        """ Вызов их объекта метода 'methname' (имя метода) с аргументами 'args' """
        meth = self.cls._read_from_class(methname)
        return meth(self, *args)

    def _read_dict(self, fieldname):
        """ Чтение поля 'fieldname' (имя поля) из словаря объектов """
        return self._fields.get(fieldname, MISSING)

    def _write_dict(self, fieldname, value):
        """  Запись поля 'fieldname' (имя поля) в словарь объектов """
        self._fields[fieldname] = value

MISSING = object()

В классе Base реализовано хранение класса объекта, а также словарь, в котором хранятся значения полей объекта. Теперь нам нужно реализовать Class и Instance. Конструктор Instance берет класс, для которого должен быть создан экземпляр, и инициализирует fields dict пустым словарем. В противном случае Instance будет просто очень тонким подклассом вокруг класса Base и не будет добавлять дополнительных функциональных возможностей.

В конструкторе Class используется имя класса, базовый класс, словарь классов и метаклассов. Что касается классов, то поля в конструктор передаются пользователем объектной модели. В конструкторе класса также используется базовый класс, тесты для которого еще не нужны и которым мы будем пользоваться в следующем разделе.

class Instance(Base):
    """Экземпляр класса, определяемого пользователем. """

    def __init__(self, cls):
        assert isinstance(cls, Class)
        Base.__init__(self, cls, {})


class Class(Base):
    """ Класс, определяемый пользователем. """

    def __init__(self, name, base_class, fields, metaclass):
        Base.__init__(self, metaclass, fields)
        self.name = name
        self.base_class = base_class

Поскольку классы такте являются своеобразным видом объектов, они (косвенно) наследуются от Base. Поэтому класс должен быть экземпляром другого класса: его метакласса.

Теперь наш первый тест почти прошел. Единственное, чего недостает, это определений базовых классов TYPE и OBJECT, которые оба являются экземплярами класса Class. Для них мы разрешим сильное отклонение от модели Smalltalk, в которая используется довольно сложная система метаклассов. Вместо этого мы будем пользоваться моделью, предложенной в ObjVlisp [1] и принятой в языке Python.

В модели ObjVlisp, OBJECT и TYPE тесно переплетены друг с другом. OBJECT является базовым классом всех классов, что означает, что у него нет базового класса. TYPE является подклассом класса OBJECT. По умолчанию, каждый класс является экземпляром класса TYPE. В частности, оба класса TYPE и OBJECT являются экземплярами класса TYPE. Тем не менее, программист также может создать подкласс класса TYPE с тем, чтобы построить новый метакласс:

# Настраиваем базовую иерархию точно также, как это реализовано в языке Python (модель ObjVLisp)
# Исключительно базовый класс — это OBJECT
OBJECT = Class(name="object", base_class=None, fields={}, metaclass=None)
# TYPE является подклассом класса  OBJECT
TYPE = Class(name="type", base_class=OBJECT, fields={}, metaclass=None)
# TYPE является экземпляром самого себя
TYPE.cls = TYPE
# OBJECT является экземпляром класса TYPE
OBJECT.cls = TYPE

Чтобы определить новые метаклассы, достаточно задать подкласс TYPE. Тем не менее, в оставшейся части этой главы мы этого делать не будем; мы просто в качестве метакласса каждого класса будем всегда использовать класс TYPE.

Рис.1. Наследование

Теперь первый тест проходит. Во втором тесте проверяется, что для классов также работает чтение и запись атрибутов. Такой тест легко написать, и он сразу проходит.

def test_read_write_field_class():
    # Классы также являются объектами
    # Код на языке Python
    class A(object):
        pass
    A.a = 1
    assert A.a == 1
    A.a = 6
    assert A.a == 6

    # Код объектной модели
    A = Class(name="A", base_class=OBJECT, fields={"a": 1}, metaclass=TYPE)
    assert A.read_attr("a") == 1
    A.write_attr("a", 5)
    assert A.read_attr("a") == 5

Проверяем isinstance

До сих пор мы не пользовались тем, что у объектов есть классы. В следующем тесте реализован механизм isinstance:

def test_isinstance():
    # Код на языке Python
    class A(object):
        pass
    class B(A):
        pass
    b = B()
    assert isinstance(b, B)
    assert isinstance(b, A)
    assert isinstance(b, object)
    assert not isinstance(b, type)

    # Код объектной модели
    A = Class(name="A", base_class=OBJECT, fields={}, metaclass=TYPE)
    B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)
    b = Instance(B)
    assert b.isinstance(B)
    assert b.isinstance(A)
    assert b.isinstance(OBJECT)
    assert not b.isinstance(TYPE)

Чтобы проверить, является ли объект obj экземпляром определенного класса cls, достаточно проверить является ли cls суперклассом класса obj или самим классом. Чтобы проверить, является ли класс суперклассом другого класса, нужно пройти по цепочке суперклассов этого класса. Если и только если в этой цепочке будет найден другой класс, то он будет суперклассом. Цепочка суперклассов класса, в том числе и сам класс, называются "последовательностью поиска методов" этого класса ("method resolution order"). Это можно легко сделать рекурсивно:

class Class(Base):
    ...

    def method_resolution_order(self):
        """ найти последовательность поиска методов класса """
        if self.base_class is None:
            return [self]
        else:
            return [self] + self.base_class.method_resolution_order()

    def issubclass(self, cls):
        """ сам подкласс класса cls? """
        return cls in self.method_resolution_order()

Тестирование этого кода проходит.

Вызов методов

Все, что нам осталось реализовать в этой первой версии объектной модели, это возможность из объектов вызывать методы. В данной главе мы реализуем простую единую модель наследования.

def test_callmethod_simple():
    # Код на языке Python
    class A(object):
        def f(self):
            return self.x + 1
    obj = A()
    obj.x = 1
    assert obj.f() == 2

    class B(A):
        pass
    obj = B()
    obj.x = 1
    assert obj.f() == 2 # works on subclass too

    # Код объектной модели
    def f_A(self):
        return self.read_attr("x") + 1
    A = Class(name="A", base_class=OBJECT, fields={"f": f_A}, metaclass=TYPE)
    obj = Instance(A)
    obj.write_attr("x", 1)
    assert obj.callmethod("f") == 2

    B = Class(name="B", base_class=A, fields={}, metaclass=TYPE)
    obj = Instance(B)
    obj.write_attr("x", 2)
    assert obj.callmethod("f") == 3

Чтобы найти правильную реализацию метода, который перенаправляется в объект, мы проходим по последовательности поиска методов в классе этого объекта. Вызывается первый метод, найденный в словаре одного из классов в последовательности поиска методов:

class Class(Base):
    ...

    def _read_from_class(self, methname):
        for cls in self.method_resolution_order():
            if methname in cls._fields:
                return cls._fields[methname]
        return MISSING

Этот тест проходит вместе с кодом для callmethod из реализации Base.

Чтобы убедиться, что методы работают также хорошо с аргументами, а также чтобы убедиться, что правильно реализовано переопределение методов, мы можем воспользоваться следующим чуть более сложным тестом, который также проходит:

def test_callmethod_subclassing_and_arguments():
    # Код на языке Python
    class A(object):
        def g(/self, arg):
            return self.x + arg
    obj = A()
    obj.x = 1
    assert obj.g(4) == 5

    class B(A):
        def g(/self, arg):
            return self.x + arg * 2
    obj = B()
    obj.x = 4
    assert obj.g(4) == 12

    # Код объектной модели
    def g_A(/self, arg):
        return self.read_attr("x") + arg
    A = Class(name="A", base_class=OBJECT, fields={"g": g_A}, metaclass=TYPE)
    obj = Instance(A)
    obj.write_attr("x", 1)
    assert obj.callmethod("g", 4) == 5

    def g_B(/self, arg):
        return self.read_attr("x") + arg * 2
    B = Class(name="B", base_class=A, fields={"g": g_B}, metaclass=TYPE)
    obj = Instance(B)
    obj.write_attr("x", 4)
    assert obj.callmethod("g", 4) == 12

Продолжение статьи