Service Locator нарушает инкапсуляцию в статически типизированных языках, поскольку шаблон не выражает четко предварительные условия.
Лошадь уже давно мертва, но некоторые люди все еще хотят на ней покататься, поэтому я пну эту лошадь еще раз.
В течение многих лет я пытался объяснить, почему Service Locator — это антипаттерн (например, он нарушает SOLID ), но недавно меня поразило, что большая часть моих аргументов сосредоточена на симптомы , упуская из виду фундаментальную проблему.
В качестве примера для рассмотрения симптомов, в моем исходная статья Я описал, как ухудшается работа IntelliSense из-за использования Service Locator. В 2010 году мне никогда не приходило в голову, что основной проблемой является нарушение инкапсуляция .
Рассмотрим мой оригинальный пример:
Пример написан на C#, но он будет похож на Java и любой другой сопоставимый статически типизированный язык.public class OrderProcessor : IOrderProcessor { public void Process(Order order) { var validator = Locator.Resolve<IOrderValidator>(); if (validator.Validate(order)) { var shipper = Locator.Resolve<IOrderShipper>(); shipper.Ship(order); } } }
Предусловия и постусловия Одним из основных преимуществ инкапсуляции является абстракция: освобождение вас от бремени понимания каждой детали реализации в каждом фрагменте исходного кода.
Правильно спроектированная инкапсуляция дает возможность использовать класс, не зная деталей реализации.
Это достигается путем заключения договора взаимодействия.
Как описано в книге Объектно-ориентированное создание программного обеспечения , контракт состоит из набора пред- и пост-условий взаимодействия.
Если клиент удовлетворяет предварительным условиям, то объект обещает удовлетворить постусловиям.
В статически типизированных языках, таких как C# или Java, многие предварительные условия могут быть выражены самой системой типов, как я продемонстрировано ранее .
Когда вы смотрите на общедоступный API класса OrderProcessor, каковы, по вашему мнению, его предварительные условия? public class OrderProcessor : IOrderProcessor
{
public void Process(Order order)
}
Как видите, предпосылок здесь не так много.
Единственное предварительное условие, видимое из API, — это то, что перед вызовом метода Process у вас должен быть объект типа Order. Да, если вы попытаетесь использовать OrderProcessor, учитывая только это предварительное условие, ваша попытка завершится неудачей во время выполнения.
var op = new OrderProcessor();
op.Process(order); // throws
Вот реальные предпосылки: требуется объект типа Order
экземпляр службы IOrderValidator требуется в каком-то глобальном каталоге локатора
требуется экземпляр службы IOrderShipper в каком-то глобальном каталоге локатора.
Два из трех предусловий невидимы во время компиляции.
Как видите, Service Locator нарушает инкапсуляцию, поскольку этот шаблон скрывает предварительные условия для правильного использования объекта.
Передача аргументов
Некоторые люди в шутку назвали «внедрение зависимостей» раскрученным термином."передача аргументов" , и возможно в этом есть доля истины.
Самый простой способ сделать предварительные условия очевидными — использовать систему типов для выражения требований.
В конце концов мы уже поняли, что нам нужен объект типа Order. Это было очевидно, поскольку Order — это тип аргумента метода Process.
Можем ли мы сделать необходимость в IOrderValidator и IOrderShipper столь же очевидной, как и необходимость в объекте Order, используя тот же метод? Может быть, следующий код является решением? public void Process(
Order order,
IOrderValidator validator,
IOrderShipper shipper)
В некоторых обстоятельствах это все, что может потребоваться сделать – теперь все три предварительных условия одинаково очевидны.
К сожалению, такое решение часто оказывается невозможным.
В этом случае OrderProcessor реализует интерфейс IOrderProcessor. public interface IOrderProcessor
{
void Process(Order order);
}
Поскольку сигнатура метода Process уже определена, вы не можете добавлять к ней аргументы.
Вы по-прежнему можете сделать предварительные условия видимыми через систему типов, потребовав от клиента передать требуемые объекты через аргументы, вам просто нужно передать их через какой-либо другой член класса.
Конструктор - самый безопасный способ: public class OrderProcessor : IOrderProcessor
{
private readonly IOrderValidator validator;
private readonly IOrderShipper shipper;
public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
{
if (validator == null)
throw new ArgumentNullException("validator");
if (shipper == null)
throw new ArgumentNullException("shipper");
this.validator = validator;
this.shipper = shipper;
}
public void Process(Order order)
{
if (this.validator.Validate(order))
this.shipper.Ship(order);
}
}
При таком дизайне публичный API стал выглядеть так: public class OrderProcessor : IOrderProcessor
{
public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
public void Process(Order order)
}
Теперь ясно, что для вызова метода Process необходимы все три объекта.
Последняя версия класса OrderProcessor продвигает свои предварительные условия через систему типов.
Вы даже не сможете скомпилировать клиентский код, пока не передадите аргументы конструктору и методу (здесь можно передать null, но это уже другая история).
Заключение
Service Locator — это антишаблон в статически типизированных объектно-ориентированных языках, поскольку он нарушает инкапсуляцию.Причина в том, что этот антипаттерн скрывает предпосылки правильного использования объекта.
Если вы ищете доступное введение в инкапсуляцию, вы можете просмотреть мой курс.
Инкапсуляция и SOLID на Pluralsight.com. Если вы хотите узнать больше о внедрении зависимостей, вы можете прочитать мою книгу ( получил награду ) Внедрение зависимостей в .
NET. Теги: #рефакторинг #локатор сервисов #архитектура #IoC #di #программирование #.
NET #проектирование и рефакторинг
-
Гомер – Новый Шифр Гомофонной Замены
19 Oct, 24 -
Таблица Дроидов. Выпуск 15
19 Oct, 24 -
Развитие Клиентов В Стартапе
19 Oct, 24