0

Многие архитекторы и инженеры рекомендуют Dependency Injection и другие Inversion of Control узоры как способ до improve the testability of your code. Нельзя отрицать, что Dependency Injection делает код более подверженным тестированию, однако, разве это не завершающая цель Abstraction в целом?Где баланс между впрыском зависимостей и абстракцией?

Чувствую противоречие! Я написал пример, чтобы проиллюстрировать это; это не суперреалистично, и я бы не проектировал его таким образом, но мне нужен был быстрый и простой пример структуры класса с несколькими зависимостями. Первый пример - без Injection Dependency, а второй - Injected Dependencies.

Non-DI Пример

package com.stackoverflow.di; 


public class EmployeeInventoryAnswerer() 
{ 
    /* In reality, at least the store name and product name would be 
    * passed in, but this example can't be 8 pages long or the point 
    * may be lost. 
    */ 
    public void myEntryPoint() 
    { 
     Store oaklandStore = new Store('Oakland, CA'); 
     StoreInventoryManager inventoryManager = new StoreInventoryManager(oaklandStore); 
     Product fancyNewProduct = new Product('My Awesome Product'); 

     if (inventoryManager.isProductInStock(fancyNewProduct)) 
     { 
      System.out.println("Product is in stock."); 
     } 
    } 
} 


public class StoreInventoryManager 
{ 
    protected Store store; 
    protected InventoryCatalog catalog; 

    public StoreInventoryManager(Store store) 
    { 
     this.store = store; 
     this.catalog = new InventoryCatalog(); 
    } 

    public void addProduct(Product product, int quantity) 
    { 
     this.catalog.addProduct(this.store, product, quantity); 
    } 

    public boolean isProductInStock(Product product) 
    { 
     return this.catalog.isInStock(this.store, this.product); 
    } 
} 


public class InventoryCatalog 
{ 
    protected Database db; 

    public InventoryCatalog() 
    { 
     this.db = new Database('productReadWrite'); 
    } 


    public void addProduct(Store store, Product product, int initialQuantity) 
    { 
     this.db.query(
      'INSERT INTO store_inventory SET store_id = %d, product_id = %d, quantity = %d' 
     ).format(
      store.id, product.id, initialQuantity 
     ); 
    } 

    public boolean isInStock(Store store, Product product) 
    { 
     QueryResult qr; 

     qr = this.db.query(
      'SELECT quantity FROM store_inventory WHERE store_id = %d AND product_id = %d' 
     ).format(
      store.id, product.id 
     ); 

     if (qr.quantity.toInt() > 0) 
     { 
      return true; 
     } 

     return false; 
    } 
} 

Dependency-Введенный Пример

package com.stackoverflow.di; 


public class EmployeeInventoryAnswerer() 
{ 
    public void myEntryPoint() 
    { 
     Database db = new Database('productReadWrite'); 
     InventoryCatalog catalog = new InventoryCatalog(db); 

     Store oaklandStore = new Store('Oakland, CA'); 
     StoreInventoryManager inventoryManager = new StoreInventoryManager(oaklandStore, catalog); 
     Product fancyNewProduct = new Product('My Awesome Product'); 

     if (inventoryManager.isProductInStock(fancyNewProduct)) 
     { 
      System.out.println("Product is in stock."); 
     } 
    } 
} 

public class StoreInventoryManager 
{ 
    protected Store store; 
    protected InventoryCatalog catalog; 

    public StoreInventoryManager(Store store, InventoryCatalog catalog) 
    { 
     this.store = store; 
     this.catalog = catalog; 
    } 

    public void addProduct(Product product, int quantity) 
    { 
     this.catalog.addProduct(this.store, product, quantity); 
    } 

    public boolean isProductInStock(Product product) 
    { 
     return this.catalog.isInStock(this.store, this.product); 
    } 
} 


public class InventoryCatalog 
{ 
    protected Database db; 

    public InventoryCatalog(Database db) 
    { 
     this.db = db; 
    } 


    public void addProduct(Store store, Product product, int initialQuantity) 
    { 
     this.db.query(
      'INSERT INTO store_inventory SET store_id = %d, product_id = %d, quantity = %d' 
     ).format(
      store.id, product.id, initialQuantity 
     ); 
    } 

    public boolean isInStock(Store store, Product product) 
    { 
     QueryResult qr; 

     qr = this.db.query(
      'SELECT quantity FROM store_inventory WHERE store_id = %d AND product_id = %d' 
     ).format(
      store.id, product.id 
     ); 

     if (qr.quantity.toInt() > 0) 
     { 
      return true; 
     } 

     return false; 
    } 
} 

(Пожалуйста, чтобы мой пример лучше, если у вас есть какие-либо идеи! Это не может это лучший пример.)

В моем примере, я чувствую, что Абстракция полностью нарушена EmployeeInventoryAnswerer, обладающей знаниями об основных деталях реализации StoreInventoryManager.

не EmployeeInventoryAnswerer должны иметь перспективу, «Хорошо, я просто захватить StoreInventoryManager, дать ему название продукта клиент ищет, и какой магазин я хочу, чтобы проверить, и он расскажет если товар находится на складе ».? Разве он не должен знать ничего о Database s или InventoryCatalog, так как с его точки зрения, это часть реализации, о которой она не должна касаться?

Итак, где баланс между проверяемым кодом с инъецированными зависимостями и скрытие информации в качестве принципа абстракции? Даже если средние классы являются просто сквозными зависимостями, одна только подпись конструктора обнаруживает нерелевантные детали, верно?

Более реалистично, скажем, это долговременное фоновое приложение, обрабатывающее данные из СУБД; на каком «слое» графа вызовов целесообразно создавать и обходить коннектор базы данных, сохраняя при этом свой код без использования СУБД?

Мне очень интересно узнать о теории и практичности ООП здесь, а также прояснить, что кажется парадоксальным между ДИ и Скрытием/Абстракцией информации.

+0

Может ли кто-нибудь проголосовать или закрыть голосование, прокомментируйте, как улучшить этот вопрос? – Will

ответ

1

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

Независимо от того, что вы делаете, и независимо от того, сколько абстракций вы вводите, где-то в вашем приложении вам нужно будет зависеть от конкретного типа. Таким образом, вы не можете полностью избавиться от этой связи, но это не проблема. Приложение, которое является 100% абстрактным, также на 100% бесполезно.

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

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

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

В соответствии с Stable-Dependencies Principle (2) зависимости должны указывать в направлении стабильности, и поскольку эта часть приложения, где вы составляете графические объекты, будет самой изменчивой частью, ничто не должно зависеть от нее. Вот почему это место, где вы составляете графы объектов, должно быть в вашей точке входа.

Эта точка входа в приложение, где вы составляете графические объекты, обычно называется Composition Root.

Если вы чувствуете, что EmployeeInventoryAnswerer не должно ничего о базах данных и InventoryCatalogs знает, это может быть так, что EmployeeInventoryAnswerer перемешивает инфраструктурные логики (для создания объектных диаграмм) и логики приложения (других слов, нарушая Single Responsibility Principle). В этом случае ваш EmployeeInventoryAnswerer не должен быть точкой входа. Вместо этого у вас должна быть другая точка входа, и EmployeeInventoryAnswerer должен получить только StoreInventoryManager. Ваша новая точка входа может создать граф объектов, начиная с EmployeeInventoryAnswerer и вызвать его метод AnswerInventoryQuestion (или как вы его назовете).

где находится баланс между тестируемым кодом с вложенными зависимостями, и скрытие информации в качестве принципа абстракции?

Конструктор представляет собой деталь реализации. Только корневой состав знает о конкретных типах, и поэтому он единственный, кто называет эти конструкторы. Поскольку зависимость, которая вводится потребителю, должна быть абстрактной, потребитель ничего не знает о реализации, и поэтому невозможно реализовать утечку какой-либо информации потребителю. Если сама абстракция будет утечка деталей реализации, это нарушит Dependency Inversion Principle, и если потребитель вернет зависимость к реализации, это нарушит Liskov Substitition Principle.

Но даже если у вас будет потребитель, который зависит от конкретного компонента, этот компонент все еще может скрывать информацию; он не должен раскрывать свои собственные зависимости (или другие значения) через общедоступные свойства. И тот факт, что этот компонент имеет конструктор, который принимает зависимостей компонента, не нарушает скрытие информации, потому что невозможно получить зависимости компонента от его конструктора (вы можете вставлять зависимости через конструктор, а не получать их). И вы не можете изменять зависимостей компонента, потому что сам этот компонент будет инжектироваться в потребитель, и вы не можете вызвать конструктор на уже созданном экземпляре.

Как я вижу, здесь нет баланса. Это просто вопрос правильного применения принципов SOLID, потому что без применения принципов SOLID вы будете в полном дерьме (с точки зрения ремонтопригодности) в любом случае. И применение принципов SOLID, несомненно, приводит к инъекции зависимостей.

на какой «слое» колл-граф является целесообразным создать и передать вокруг соединителя базы данных

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

Но должна ли точка входа отвечать за создание соединения с базой данных, что может зависеть от множества факторов. У меня обычно есть какая-то ConnectionFactory абстракция для этого, но YMMV.

UPDATE

Я не хочу, чтобы пройти вокруг контекста или AppConfig ко всему, и в конечном итоге прохождение зависимостей классов не нужно

Passing зависимостей класса Безразлично» Мне нужна плохая практика, и может указывать на то, что вы нарушаете принцип инверсии зависимостей и применяете анти-шаблон Control Freak. Вот пример такой задачи:

public class Service : IService 
{ 
    private IOtherService otherService; 

    public Service(IDep1 dep1, IDep2 dep2, IDep3 dep3) { 
     this.otherService = new OtherService(dep1, dep2, dep3); 
    } 
} 

Здесь мы видим класс Service, который принимает в 3 зависимостей, но не использовать их вообще. Он только перенаправляет их в конструктор OtherService, который он создает. Это является нарушением принципа инверсии зависимостей, поскольку Service тесно связан с OtherService. Вместо этого, как Service должен выглядеть следующим образом:

public class Service : IService 
{ 
    private IOtherService otherService; 

    public Service(IOtherService otherService) { 
     this.otherService = otherService; 
    } 
} 

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

, но я также не хочу, чтобы передать те же 4 вещи на несколько различных классов

Если у вас есть группа зависимостей, которые часто все вместе впрыскивается в потребителя, изменения высоки, что вы нарушаете принцип единой ответственности: потребитель может сделать слишком много; Знайте слишком много.

Существует несколько решений для этого, в зависимости от конструкции. Одна вещь, которая приходит на ум, - refactoring to Aggregate Services.

Возможно, также, что эти вложенные зависимости являются сквозными проблемами. Часто гораздо лучше применять сквозные решения прозрачно, вместо того, чтобы вводить его в десятки или сотни потребителей (что является нарушением принципа Open/Closed). Для этого вы можете использовать декораторы или перехватчики.

+0

Спасибо за подробный и продуманный ответ и предоставленные вами ресурсы. Моя проблема почти похоже на баланс между «I» и «D» - я не хочу передавать «Context» или «AppConfig» ко всему и в конечном итоге передавать классы зависимостей не нужно, но я также не хотят передавать одни и те же 4 вещи нескольким различным классам. Я думаю, что эти общие абстракции - вот где баланс ... для моего примера БД я поселился в контейнере «DbConfig», созданном около точки входа, и чувствую, что это хороший баланс. Это замечательно, другие должны прочитать его, но я 3x downvoted :(Спасибо! – Will

+0

Принимая этот ответ, просто хотел дать ему некоторое время, чтобы другие могли добавить. Еще раз спасибо @Steven! – Will

+1

@Will, я обновил мой вопрос основан на вашем ответе. – Steven