пятница, 10 октября 2014 г.

Реализация паттерна Service Locator на Python

Вряд ли стоит описывать важность знания паттернов проектирования в современной разработке  приложений и сервисов. Но вот все ли паттерны мы знаем? На слуху - разумеется синглтон, декоратор, адаптер, фасад, прокси, фабрика и прочие. А вот IoC-контейнер (инверсия контроля) и DI (внедрение зависимости) - многие ли слышали, многие ли применяли? Между тем, не смотря на споры вокруг ценности и применимости IoC, он вполне имеет право на существование и при правильном применении может существенно облегчить жизнь разработчику.


В данной статье я хочу рассмотреть простой пример реализации паттерна Service Locator (частного случая IoC) на замечательном языке Python. Этот паттерн представляет из себя дополнительный абстрактный слой, реализованный в виде контейнера, хранящего “таблицу переходов” и функционал для её управления: добавление, удаление и подмена параметров перехода.


Применение Service Locator’а помогает избегать развесистых цепочек условных операторов и усложнения алгоритмов: достаточно сохранить в контейнере класс и потом, по мере надобности, подменять его на другие классы, при том что интерфейс обращения к нему остаётся одним и тем же - по заранее заданному ключу (тут я назвал его алиасом).

Собственно: сам класс, реализующий паттерн Service Locator:

# coding: utf-8

"""Service Locator: a simple python implementation."""

from importlib import import_module


class ServiceLocatorError(Exception): pass


class ServiceLocator(object):

    u"""Класс для работы с контейнером названий объектов.

    Представляет собой реализацию паттерна Service Locator.
    """

    # словарь для хранения сервисных классов
    _objects = {}

    @classmethod
    def set_class(cls, alias, class_path):
        u"""Добавить новый сервисный класс в контейнер.

        :param alias: алиас сервисного класса
        :type alias: str
        :param class_path: полное имя класса
        :type object_name: str
        """
        assert isinstance(alias, basestring), type(alias)
        assert isinstance(class_path, basestring), type(class_path)

        cls._objects[alias] = class_path

    @classmethod
    def get_class(cls, alias, default=None):
        u"""Найти имя класса в сервис-локаторе по его алиасу.

        :param alias: Алиас класса
        :type alias: str
        :return: полное имя объекта или None
        """
        assert isinstance(alias, basestring), type(alias)

        return cls._objects.get(alias, default)

    @classmethod
    def fabricate(cls, alias, *args, **kwargs):
        u"""Импортировать и создать экземпляр класса по его алиасу.

        :param alias: Алиас сервисного класса
        :type alias: str
        :param args: аргументы для конструктора класса
        :param kwargs: именованные аргументы для конструктора класса
        :return: инстанс класса
        """
        assert isinstance(alias, basestring), type(alias)

        full_class_name = cls._objects.get(alias)
        if full_class_name:
            full_path, class_name = full_class_name.rsplit('.', 1)

            try:
                module = import_module(full_path)
                return getattr(module, class_name)(*args, **kwargs)
            except (ImportError, AttributeError) as err:
                raise ServiceLocatorError(err)

        raise ServiceLocatorError(u"Unknown alias: %s" % alias)

    @classmethod
    def remove(cls, alias):
        u"""Удаление алиаса из Service Locator’а.

        :param alias: алиас класса
        :type alias: str
        """
        assert isinstance(alias, basestring), type(alias)

        if cls._objects.get(alias):
            del cls._objects[alias]

Из приведённого кода видно, что о каждом классе хранится следующая информация:
- алиас - короткое название или ещё какой условный синоним,
- полное имя - имя сервисного класса и полный путь к нему, по которому он будет вызываться (пакеты, подпакеты и модуль),  например: “foo_package.bar_module.ProviderClazz”.
Пример использования:
...
# сохранять(добавлять в контейнер) можно так:
ServiceLocator.set_class(
    'test_provider',
    'legacy.provider.Provider'
)

# а можно даже так:
if some_flag:
    ServiceLocator.set_class(
        'test_provider',
        'foo.provider.FooProvider'
    )
else:
    ServiceLocator.set_class(
        'test_provider',
        'bar.provider.BarProvider'
    )

        ...

# получение экземпляра класса
provider = ServiceLocator.fabricate(
    'test_provider',
     a=10, b=20, c=30
)
        
print provider.func()

Реализация Service Locator на Java - здесь