Вряд ли стоит описывать важность знания паттернов проектирования в современной разработке приложений и сервисов. Но вот все ли паттерны мы знаем? На слуху - разумеется синглтон, декоратор, адаптер, фасад, прокси, фабрика и прочие. А вот 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 - здесь