[Перевод] Пример для иллюстрации принципов SOLID который я (кажется) понял
Это перевод вступления из книги: Dependency Injection with Unity.
Авторы которой утверждают что:
в этой (вводной) главе вы увидите, как можно удовлетворить некоторые из распространенных требований корпоративных приложений (приложений для бизнеса), таких как низкая стоимость (простота) сопровождения и тестируемость, применяя слабосвязанный дизайн для вашего приложения. Вы увидите очень простую иллюстрацию этого подхода в примерах кода, которые показывают два разных способа реализации зависимости между классами ManagementController и TenantStore. Вы также увидите, как принципы объектно-ориентированного программирования SOLID связаны с теми же проблемами (имеются ввиду проблемы стоимости сопровождения = исправления ошибок + возможности расширения функциональности, и тестируемости).
Вводная часть к вводной главе
Содержимое книги, главу из которой мы переводим посвящено понятиям dependency injection и Unity, и тому почему, когда и для решения каких типов проблем их полезно применять.
В этой вводной главе не будет много сказано о Unity или о dependency injection, но она предоставит некоторую необходимую справочную информацию, которая поможет вам оценить преимущества внедрения зависимостей как метода и почему Unity делает все именно так, как она это делает.
Мотивация <ред: общие слова – можно пропустить>
При проектировании и разработке программных систем необходимо учитывать множество требований. Некоторые из них будут специфичны для рассматриваемой системы, а некоторые будут иметь более общее применение. Вы можете классифицировать некоторые требования как функциональные, а некоторые — как нефункциональные (или требования к качеству). Полный набор требований будет отличаться для каждой отдельной системы. Набор требований, изложенный ниже, является общими требованиями, особенно для программных систем для бизнеса (LOB) с относительно длительным ожидаемым сроком эксплуатации.
Не все они обязательно будут важны для каждой разрабатываемой вами системы, но вы можете быть уверены, что некоторые из них будут постоянно дублироваться в списках требований для разных проектов, над которыми вы работаете или будете работать.
Обратите внимание:
В этой главе представлено множество требований и принципов. Не надо думать, что все они актуальны всегда. Однако большинство корпоративных систем используют некоторые требования из списка, и все принципы являются хорошими методами проектирования и кодирования.
Вопросы удобства и эффективности сопровождения (maintenance)
<ред.: нельзя перевести «maintenance» одним словом как «сопровождение», потому что далее это понятие рассматривается как качество системы допускать обслуживание в рамках сопровождения.>
По мере того, как системы становятся крупнее и ожидаемый срок службы систем увеличивается, обслуживание этих систем становится все более сложной задачей. Очень часто первоначальные члены команды, разработавшие систему, больше недоступны или больше не помнят детали системы. Документация может устареть или даже быть утеряна. В то же время бизнес может требовать быстрых действий для удовлетворения какой-либо новой насущной бизнес-потребности. Удобство и эффективность сопровождения — это качества программной системы, которые определяют, насколько легко и эффективно вы можете ее обновить. Обновление системы требуется, если обнаружен дефект, который необходимо устранить (другими словами, выполнить корректирующее обслуживание), если какие-либо изменения в операционной среде требуют от вас внесения изменений в систему или если вам нужно добавить новые функции в систему для удовлетворения бизнес-требований (perfective maintenance = техническое обслуживание с целью достижения совершенства с точки зрения пользователей и/или повышения эффективности внутренних процессов). Таким образом сформулированные задачи сопровождения системы повышают гибкость организации и снижают затраты.
Поэтому вам следует включить удобство и эффективность сопровождения в качестве одной из целей вашего проектирования наряду с другими, такими как надежность, безопасность и масштабируемость.
Обратите внимание:
Очень сложно сделать существующие системы более удобными в обслуживании. Гораздо лучше с самого начала проектировать систему с учетом удобства последующего обслуживания.
Тестируемость
Система, которая обеспечивает возможности собственного тестирования — это система, которая позволяет эффективно тестировать отдельные свои части. Разработка и написание эффективных тестов может быть такой же сложной задачей, как разработка и написание тестируемого кода приложения, особенно по мере того, как системы становятся больше и сложнее. Такие методологии, как разработка на основе тестирования (TDD) требуют от вас написания модульного теста перед написанием любого кода для реализации новой функции, и целью такого метода проектирования является повышение качества вашего приложения. Такие методы проектирования также помогают расширить охват ваших модульных тестов, снижают вероятность регрессий и упрощают рефакторинг. Однако не стоит забывать и о других типах тестов, таких как приемочные тесты, интеграционные тесты, тесты производительности и стресс-тесты.
Выполнение тестов также может стоить денег и отнимать много времени из-за необходимости тестирования в реалистичной среде. Например, для некоторых типов тестирования облачного приложения вам необходимо развернуть приложение в облачной среде и запустить тесты в облаке. Если вы используете TDD, то постоянно запускать все тесты в облаке может быть нецелесообразно из-за времени, необходимого для развертывания вашего приложения, даже в локальном эмуляторе. В сценариях такого типа вы можете решить использовать тестовых двойников (простые заглушки «stubs» или верифицируемые макеты «mocks») для программных компонент, которые заменяют реальные компоненты в облачной среде тестовыми реализациями, чтобы можно было изолированно запускать свой набор модульных тестов в течение стандартного цикла разработки TDD.
Тестируемость должна быть еще одной целью проектирования вашей системы наряду с легкостью сопровождения и гибкостью: тестируемая система, как правило, более удобна в обслуживании, и наоборот.
Обратите внимание:
Использование тестовых двойников отличный способ обеспечить непрерывное выполнение модульных тестов в процессе разработки. Однако вы все равно должны полностью протестировать свое приложение в реальных условиях.
Гибкость и расширяемость
Гибкость и расширяемость также часто входят в список желательных атрибутов корпоративных приложений. Учитывая, что бизнес-требования часто меняются, как во время разработки приложения, так и после его запуска в работу, вам следует попытаться спроектировать приложение таким образом, чтобы оно было гибким, чтобы его можно было адаптировать для работы различными способами, и расширяемым, чтобы вы могли добавлять новые функции. Например, вам может потребоваться преобразовать ваше приложение из локального в облачное.
Позднее связывание
В некоторых сценариях работы приложения может потребоваться поддержка концепции позднего связывания. Позднее связывание полезно, если вам требуется возможность заменить часть вашей системы без перекомпиляции. Например, ваше приложение может поддерживать несколько реляционных баз данных с отдельным модулем для каждого поддерживаемого типа базы данных. Вы можете использовать декларативную конфигурацию, чтобы указать приложению использовать определенный модуль во время выполнения. Другой сценарий, в котором может быть полезно позднее связывание, заключается в предоставлении пользователям системы возможности самостоятельно настраивать параметры с помощью какого-то плагина (а plug-in — внешнего подключаемого модуля). Опять же, вы можете дать системе указание использовать специфичную пользовательскую кастомизацию, используя параметры конфигурации или соглашение, согласно которому система сканирует определенное местоположение в файловой системе в поисках используемых модулей.
Обратите внимание:
Не во всех системах есть требования по реализации позднего связывания. Обычно это требуется для поддержки конкретной функции приложения, такой как настройка с использованием архитектуры плагинов.
Параллельная разработка
Когда вы разрабатываете крупномасштабные (или даже малые и среднемасштабные) системы, непрактично, чтобы вся команда разработчиков работала одновременно над одной и той же функцией или компонентом. На практике вы будете назначать различные функции и компоненты небольшим группам для параллельной работы. Хотя этот подход позволяет сократить общую продолжительность проекта, он создает дополнительные сложности: вам необходимо управлять несколькими группами и гарантировать, что вы сможете интегрировать части приложения, разработанные разными группами для правильной совместной работы.
Обратите внимание:
Обеспечение того, чтобы классы и компоненты, разработанные независимо, действительно работали вместе, может оказаться серьезной проблемой.
Проблема сквозных задач
Корпоративным приложениям обычно требуется решать целый ряд сквозных задач, таких как проверка, обработка исключений и логирование. Эти функции могут понадобиться вам в различных областях приложения, и вы захотите реализовать их стандартным, последовательным способом, чтобы улучшить удобство сопровождения системы. В идеале вам нужен механизм, который позволит вам эффективно и прозрачно добавлять поведение к вашим объектам как во время разработки, так и во время выполнения, не требуя внесения изменений в существующие классы. Часто вам требуется возможность настраивать эти функции во время выполнения, а в некоторых случаях добавлять функции для решения новой сквозной задачи в существующее приложение
Обратите внимание:
Для крупной корпоративной системы важно иметь возможность согласованно решать сквозные задачи, такие как логирование и валидация. Мне часто приходится изменять уровень логирования для определенного компонента во время выполнения, чтобы устранить проблему без перезапуска системы.
Низкая связанность
Вы можете удовлетворить большинство требований, перечисленных в предыдущих разделах, таким образом гарантируя, что по результатам вашего проектирования будет создано приложение, в котором разные его части (компоненты) будут слабо связаны. Низкая (слабая) связанность, в отличие от сильной связанности, означает сокращение числа зависимостей между компонентами, составляющими вашу систему. Это упрощает и делает более безопасным внесение изменений в одну область системы, поскольку каждая часть системы в значительной степени независима от другой (практически не связана с другой).
Обратите внимание:
Низкая связанность должна быть общей целью проектирования ваших корпоративных приложений.
Простой пример
Следующий пример иллюстрирует низкую (слабую) связанность, в которой класс ManagementController напрямую зависит от класса TenantStore. Эти классы могут находиться в разных проектах Visual Studio.
public class TenantStore
{
//...
public Tenant GetTenant(string tenant)
{
//...
}
public IEnumerable GetTenantNames()
{
//...
}
}
public class ManagementController
{
private readonly TenantStore tenantStore;
public ManagementController()
{
tenantStore = new TenantStore(...);
}
public ActionResult Index()
{
var model = new TenantPageViewData>
(this.tenantStore.GetTenantNames())
{
Title = "Subscribers"
};
return this.View(model);
}
public ActionResult Detail(string tenant)
{
var contentModel = this.tenantStore.GetTenant(tenant);
var model = new TenantPageViewData(contentModel)
{
Title = string.Format("{0} details", contentModel.Name)
};
return this.View(model);
}
//...
}
Хотя класс ManagementController является ASP.NET контроллером MVC, вам не обязательно знать о MVC, чтобы проверить соответствие примера изложенным выше принципам. Однако это пример демонстрирует классы, с которыми вы могли бы столкнуться в реальной системе.
В этом примере класс TenantStore реализует объект хранилища данных (представления репозитория). Этот объект обрабатывает доступ к базовому хранилищу данных, такому как реляционная база данных, а ManagementController — это класс контроллера MVC, который запрашивает данные из репозитория. Обратите внимание, что класс ManagementController должен либо создать экземпляр объекта TenantStore, либо получить ссылку на объект TenantStore откуда-либо еще, прежде чем он сможет вызвать методы GetTenant и GetTenantNames. Класс ManagementController зависит от конкретного класса TenantStore. Если вы вернетесь к списку общих желательных требований для корпоративных приложений в начале этой главы, вы сможете оценить, насколько хорошо подход, описанный в предыдущем примере кода, помогает вам их выполнить.
<ред.:клиентский класс для некоторого класса - это класс который использует этот некоторый класс внутри своей реализации. ManagementController класс является клиентским по отношению к TenantStore классу. у одного класса может быть много клиентских классов, классов которые его используют>
Хотя в этом простом примере показан только один клиентский класс для типа TenantStore, на практике в вашем приложении может быть много клиентских классов, использующих класс TenantStore. Если вы предполагаете, что каждый клиентский класс отвечает за создание экземпляра или определение местоположения объекта TenantStore во время выполнения, то все эти классы привязаны к определенному конструктору или методу инициализации в этом классе TenantStore, и, возможно, все они должны быть изменены, если изменится реализация класса TenantStore. Это потенциально делает обслуживание класса TenantStore более сложным, более подверженным ошибкам и отнимающим больше времени.
Чтобы запустить модульные тесты для методов Index и Detail в классе ManagementController, вам необходимо создать экземпляр объекта TenantStore и убедиться, что соответствующее тестовое хранилище данных содержит подходящие тестовые данные для теста. Это усложняет процесс тестирования и, в зависимости от используемого хранилища данных, может привести к увеличению времени выполнения теста, поскольку необходимо создать хранилище данных и заполнить его правильными данными. Это также делает тесты намного более хрупкими.
Можно изменить реализацию класса TenantStore, чтобы использовать другое хранилище данных, например хранилище таблиц Windows Azure вместо SQL Server. Однако это могло бы потребовать некоторых изменений в клиентских классах, использующих экземпляры TenantStore, если бы им было необходимо предоставить некоторые данные инициализации, такие как строки с адресами/параметрами для подключения.
Вы не cможете использовать позднее связывание при таком подходе, поскольку клиентские классы содержат тип TenantStore в качестве типа своего члена (или просто упоминание класса в теле функции), соответственно при изменении этого типа (класса) вам придется перекомпилировать все клиентские классы.
Если вам нужно добавить поддержку сквозной задачи, такой как логирование в нескольких классах хранилища, включая класс TenantStore, вам потребуется изменить и настроить каждый из ваших классов хранилища независимо
Следующий пример кода показывает небольшое изменение. Конструктор в клиентском классе ManagementController теперь получает объект, который реализует интерфейс ITenantStore, а класс TenantStore предоставляет реализацию того же интерфейса.
public interface ITenantStore
{
void Initialize();
Tenant GetTenant(string tenant);
IEnumerable GetTenantNames();
void SaveTenant(Tenant tenant);
void UploadLogo(string tenant, byte[] logo);
}
public class TenantStore : ITenantStore
{
//...
public TenantStore()
{
//...
}
//...
}
public class ManagementController : Controller
{
private readonly ITenantStore tenantStore;
public ManagementController(ITenantStore tenantStore)
{
this.tenantStore = tenantStore;
}
public ActionResult Index()
{
//...
}
public ActionResult Detail(string tenant)
{
//...
}
//...
}
Это изменение напрямую влияет на то, насколько легко вы сможете выполнить список требований.
Теперь ясно, что класс ManagementController и любые другие клиенты класса TenantStore больше не отвечают за создание экземпляров объектов TenantStore, хотя приведенный пример кода не показывает, какой класс или компонент отвечает за их создание. С точки зрения технического обслуживания, эта ответственность теперь может принадлежать одному классу, а не многим.
Теперь мы сделали явной зависимость при создании контроллера от наличия объекта типа ITenantStore, вместо того чтобы скрыть это внутри конструктора класса контроллера.
Теперь чтобы протестировать поведения клиентского класса (например класса ManagementController) связанного с ITenantStore, мы можем предоставить упрощенную реализацию интерфейса ITenantStore, которая возвращает некоторые тестовые данные. Это может быть гораздо проще чем создание настоящего объекта TenantStore, который запрашивает у базового хранилища настоящие данные.
Внедрение интерфейса ITenantStore упрощает замену реализации хранилища, не требуя изменений в клиентских классах, поскольку все, что они ожидают — это объект, реализующий интерфейс. Если интерфейс находится в отдельном проекте для реализации, то проекты, содержащие клиентские классы, должны содержать ссылку только на проект, содержащий определение интерфейса.
Теперь класс, ответственный за создание экземпляров классов хранилища (разных реализаций ITenantStore), мог бы предоставлять дополнительные возможности приложению. Он мог бы управлять временем жизни создаваемых им экземпляров ITenantStore, например, создавая новый объект каждый раз, когда классу ManagementController требуется экземпляр представления хранилища данных, или поддерживая один и тот же экземпляр (singleton-like), который он передает в качестве ссылки всякий раз, когда создается новый клиентский класс.
Теперь можно использовать позднее связывание, поскольку клиентские классы ссылаются только на тип интерфейса ITenantStore. Приложение может создать объект, который реализует интерфейс во время выполнения (загрузить из DLL-ки), возможно, на основе параметра конфигурации, и передать этот объект клиентским классам. Например, приложение может создать экземпляр SQLTenantStore или экземпляр BlobTenantStore в зависимости от настройки в каком-то web.config файле и передать его конструктору в классе ManagementController.
Если определение интерфейса задокументировано-согласовано между разработчиками, две разные команды могут параллельно работать над классом хранилища и классом контроллера который его использует.
Класс, ответственный за создание экземпляров класса хранилища (реализующих интерфейс ITenantStore), теперь может добавить поддержку сквозных задач перед передачей экземпляра store клиентам, например, используя шаблон Декоратор для передачи объекта, который реализует сквозные задачи. Вам не нужно изменять ни клиентские классы, ни класс хранилища, чтобы добавить поддержку сквозных задач, таких как логирование или обработка исключений.
Подход, показанный во втором примере кода, является примером слабосвязанного дизайна, использующего интерфейсы. Если мы сможем устранить прямую зависимость между классами, это снизит уровень взаимосвязи и поможет решать вопросы сопровождения в будущем, а также тестируемость, гибкость и расширяемость решения.
Чего не показывает второй пример кода, так это того, как внедрение зависимостей и контейнер Unity вписываются в общую картину, хотя вы, вероятно, можете догадаться, что они будут отвечать за создание экземпляров и передачу их клиентским классам. В главе 2 описывается роль внедрения зависимостей как метода поддержки слабо связанных проектов, а в главе 3 описывается, как Unity помогает вам включать внедрение зависимостей в ваши приложения.
Обратите внимание:
Слабая связанность не обязательно подразумевает внедрение зависимостей (dependency injection), хотя эти два понятия часто сочетаются.
Когда следует использовать слабосвязанный дизайн?
Прежде чем мы перейдем к внедрению зависимостей и Unity, вы должны начать понимать, где в вашем приложении вам следует рассмотреть возможность введения слабой связи, программирования интерфейсов и уменьшения зависимостей между классами. Первым требованием, которое мы описали в предыдущем разделе, было удобство сопровождения, и это часто дает хорошее представление о том, когда и где следует рассмотреть возможность уменьшения сцепления в приложении. Как правило, чем крупнее и сложнее приложение, тем сложнее его поддерживать, и, следовательно, тем больше вероятность того, что эти методы окажутся полезными. Это верно независимо от типа приложения: это может быть десктоп приложение, веб-приложение или облачное приложение.
На первый взгляд, это, наверно, кажется нелогичным. Во втором примере, показанном выше, представлен интерфейс, которого не было в первом примере, это также требует дополнительного кода, который мы еще не показывали, который отвечает за создание экземпляров объектов и управление ими от имени клиентских классов. На небольшом примере эти методы, по-видимому, усложняют решение, но по мере того, как приложение становится больше и сложнее, эти накладные расходы становятся все менее и менее значимыми.
Предыдущий пример также иллюстрирует еще один общий момент о том, где уместно использовать эти методы. Скорее всего, класс ManagementController существует на уровне пользовательского интерфейса в приложении, а класс TenantStore является частью уровня доступа к данным. Это распространенный подход к разработке приложения таким образом, чтобы в будущем можно было заменить один уровень, не нарушая работу других. Например, замена или добавление нового пользовательского интерфейса к приложению (например, создание приложения для мобильной платформы в дополнение к традиционному веб-интерфейсу) без изменения уровня данных или замены базового механизма хранения и без изменения уровня пользовательского интерфейса. Построение приложения с использованием уровней помогает отделить части приложения друг от друга. Вам следует попытаться определить части приложения, которые, вероятно, изменятся в будущем, а затем отделить их от остальной части приложения, чтобы минимизировать и локализовать влияние этих изменений.
Обратите внимание:
«Небольшие примеры слабосвязанного дизайна, программирования на интерфейсах и внедрения зависимостей часто усложняют решение. Вы должны помнить, что эти методы предназначены для того, чтобы помочь вам упростить большие и сложные приложения со множеством классов и зависимостей и управлять ими. Конечно, небольшие приложения часто могут вырасти в большие и сложные приложения.»
Список требований в предыдущем разделе также включает анализ проблемы сквозных задач, которые вам, возможно, потребуется последовательно применять к целому ряду классов в вашем приложении… <ред.:здесь пропущены некоторые рассуждения-ссылка на какие-то примеры>
Принципы объектно-ориентированного дизайна
Наконец, прежде чем перейти к внедрению зависимостей и Unity, мы хотим сопоставить пять принципов объектно-ориентированного программирования и проектирования SOLID с обсуждавшимися до сих пор требованиями. SOLID — это аббревиатура, обозначающая следующие принципы:
Принцип единой ответственности (Single responsibility principle)
Принцип «Открыто/закрыто» (Open/close principle)
Принцип подстановки Лисков (Liskov substitution principle)
Принцип разделения интерфейсов (Interface segregation principle)
Принцип инверсии зависимостей (Dependency inversion principle)
В следующих разделах описывается каждый из этих принципов и как они соотносятся с низкой связанностью между классами-объектами-частями системы и требованиями, перечисленными в начале этой главы.
Принцип единой ответственности
Принцип единой ответственности гласит, что у класса должна быть одна и только одна причина для изменения. Для получения дополнительной информации смотрите the article Principles of Object Oriented Design by Robert C. Martin статью Роберта К. Мартина <ред: ссылка для меня не работает к сожалению>.
В первом простом примере, показанном в этой главе, класс ManagementController выполнял две функции: выступать в качестве контроллера в пользовательском интерфейсе и создавать экземпляры объектов TenantStore и управлять временем жизни объектов TenantStore. Во втором примере ответственность за создание экземпляров объектов хранилища клиента и управление ими лежит на другом классе или компоненте системы.
Принцип «Открыто/закрыто»
Принцип «Открыто/закрыто» гласит, что «программные объекты (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для модификации» (Meyer, Bertrand (1988). Object-Oriented Software Construction.)
Хотя вы можете изменить код в классе, чтобы исправить дефект, вам следует расширить класс, если вы хотите добавить к нему какое-либо новое поведение. Это помогает сохранить код поддерживаемым и тестируемым, поскольку существующее поведение не должно изменяться, а любое новое поведение существует в новых классах. Требование иметь возможность добавлять поддержку сквозных задач в ваше приложение лучше всего может быть выполнено, следуя принципу открыто/закрыто. Например, когда вы добавляете логирование в набор классов в своем приложении, вам не следует вносить изменения в реализацию существующих классов.
Принцип подстановки Лисков
Принцип подстановки Лисков в объектно-ориентированном программировании гласит, что в компьютерной программе, если S является подтипом T, то объекты типа T могут быть заменены объектами типа S без изменения каких-либо значимых свойств, таких как корректность, этой программы.
Во втором примере кода, показанном в этой главе, класс ManagementController должен продолжать работать должным образом, если вы передадите ему любую реализацию интерфейса ITenantStore. В этом примере тип интерфейса используется в качестве типа для передачи конструктору класса ManagementController, но вы с таким же успехом могли бы использовать абстрактный тип.
Принцип разделения интерфейсов
Принцип разделения интерфейсов — это принцип разработки программного обеспечения, предназначенный для повышения надежности программного обеспечения. Принцип разделения интерфейсов поощряет слабую связанность и, следовательно, облегчает рефакторинг, изменение и повторное развертывание системы. Принцип гласит, что очень большие интерфейсы должны быть разделены на более мелкие и специфичные, чтобы клиентским классам нужно было знать только о методах, которые они используют: ни один клиентский класс не должен быть вынужден зависеть от методов, которые он не использует.
В определении интерфейса ITenantStore, показанном ранее в этой главе, если вы определили, что не все клиентские классы используют метод UploadLogo, вам следует рассмотреть возможность выделения этого метода в отдельный интерфейс, как показано в следующем примере кода:
public interface ITenantStore
{
void Initialize();
Tenant GetTenant(string tenant);
IEnumerable GetTenantNames();
void SaveTenant(Tenant tenant);
}
public interface ITenantStoreLogo
{
void UploadLogo(string tenant, byte[] logo);
}
public class TenantStore : ITenantStore, ITenantStoreLogo
{
//...
public TenantStore()
{
//...
}
//...
}
Принцип инверсии зависимостей
Принцип инверсии зависимостей гласит, что:
• Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
• Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Два примера кода в этой главе иллюстрируют, как применять этот принцип. В первом примере высокоуровневый класс ManagementController зависит от низкоуровневого класса TenantStore. Обычно это ограничивает возможности повторного использования высокоуровневого класса в другом контексте.
Во втором примере кода класс ManagementController теперь имеет зависимость от абстракции ITenantStore, как и класс TenantStore.
Резюме
В этой главе вы увидели, как можно удовлетворить некоторые из распространенных требований корпоративных приложений (приложений для бизнеса), таких как низкая стоимость (простота) сопровождения и тестируемость, применяя слабосвязанный дизайн для вашего приложения. Вы увидели очень простую иллюстрацию этого подхода в примерах кода, которые показывают два разных способа реализации зависимости между классами ManagementController и TenantStore. Вы также увидели, как принципы объектно-ориентированного программирования SOLID связаны с теми же проблемами (имеются ввиду проблемы стоимости сопровождения = исправления ошибок + возможности расширения функциональности, и тестируемости).
Однако обсуждение в этой главе оставило открытым вопрос о том, как создавать экземпляры объектов TenantStore и управлять ими, если ManagementController больше не отвечает за эту задачу. В следующей главе будет показано, как внедрение зависимостей связано с этим конкретным вопросом и как применение подхода внедрения зависимостей может помочь вам соответствовать требованиям и придерживаться принципов, изложенных в этой главе.
<ред.:конец введения, читайте оригинал. Удачи и успехов в построении самого лучшего дизайна приложений.>