[Перевод] Service Locator нарушает инкапсуляцию

Service Locator нарушает инкапсуляцию в статически типизированных языках, потому что этот паттерн нечётко выражает предусловия.

Лошадь уже давно мертва, но некоторые до сих пор хотят на ней поездить, так что я пну эту лошадь ещё раз. Годами я предпринимал попытки объяснить почему Service Locator это антипаттерн (например, он нарушает SOLID), но недавно меня осенила мысль, что большая часть моих аргументов фокусировалась на симптомах, упуская из внимания фундаментальную проблему.
В качестве примера для рассмотрения симптомов, в моей статье-первоисточнике, я описывал как ухудшаются возможности использования IntelliSense из-за применения Service Locator. В 2010 году мне и в голову не приходило, что проблема, лежащая в основе, заключается в нарушении инкапсуляции.

Рассмотрим мой исходный пример:

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 и на любом другом сравнимом статически типизированном языке.

Предусловия и постусловия
Одно из главных преимуществ инкапсуляции это абстрагирование: освобождение от бремени понимания каждой детали реализации в каждом кусочке кода в исходниках. Правильно спроектированная инкапсуляция даёт возможность использовать класс, не зная деталей реализации. Это достигается за счёт установления контракта взаимодействия.

Как объясняется в книге Object-Oriented Software Construction, контракт состоит из набора пред и пост-условий для взаимодействия. Если клиент удовлетворяет предусловиям, то объект обещает удовлетворять постусловиям.
В статически типизированных языках, таких как C# или Java, многие предусловия могут быть выражены самой системой типов, как я демонстрировал ранее.

Когда вы смотрите на публичный API класса OrderProcessor, как вы думаете, какие у него предусловия?

public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
}


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

Да, если вы попытаетесь использовать OrderProcessor, учитывая только это предусловие, то ваша попытка провалится в run-time.

var op = new OrderProcessor();
op.Process(order); // throws


Вот настоящие предусловия:

  • требуется объект типа Order
  • требуется экземпляр сервиса IOrderValidator в некой глобальной директории локатора
  • требуется экземпляр сервиса IOrderShipper в некой глобальной директории локатора


Два из трёх предусловий невидимы в compile-time.
Как видите, Service Locator нарушает инкапсуляцию, потому что этот паттерн скрывается предусловия для корректного использования объекта.

Передача аргументов


Несколько человек шутливо определяли Dependency Injection как разрекламированный термин вместо «передачи аргументов», и, возможно, в этом есть частичка правды.
Наипростейшим способом сделать предусловиями очевидными было бы использование системы типов для выражения требований. В конце концов, мы уже поняли, что нам требуется объект типа 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 — антипаттерн в статически типизированных, объектно-ориентированных языках, поскольку он нарушает инкапсуляцию. Причина заключается в том, что данный антипаттерн скрывает предусловия для корректного использования объекта.

Если вам требуется доступное введение в инкапсуляцию, вы можете посмотреть мой курс Encapsulation and SOLID на Pluralsight.com. Если вы хотите более подробно изучить Dependency Injection, вы можете почитать мою книгу (получившую награду) Dependency Injection in .NET.

© Habrahabr.ru