Методология SOLID 2

advertisement
Методология SOLID
Задание: Разработка собственного IoC-контейнера
Напишите консольную программу «Мастер отчетов», реализующую логику последовательной отправки отчетов через e-mail или sms. Вместо сервисов формирования и отправки отчета используйте заглушки Mock-объекты.
При разработке приложения используйте принцип инверсии зависимостей. Разработайте класс ServiceLocator, выполняющий регистрацию и получение реализации интерфейса.
Система будет консольным приложением, которое занимается рассылкой отчетов.
public class Program
{
public static void Main()
{
var reporter = new Reporter();
reporter.SendReports();
}
}
Главный объект в нашей бизнес-логике – Reporter.
public class Reporter
{
public void SendReports()
{
var reportBuilder = new ReportBuilder();
IList<Report> reports = reportBuilder.CreateReports();
if (reports.Count == 0)
throw new NoReportsException();
?
var reportSender = new EmailReportSender();
foreach (Report report in reports)
{
reportSender.Send(report);
}
}
}
Устроен Reporter очень просто. Он просит ReportBuilder создать список
отчетов, а потом один за другим отсылает их с помощью объекта EmailReportSender.
Тестируемость
Как протестировать функцию SendReports? Давайте проверим поведение
функции, когда ReportBuilder не создал ни одного отчета. В этом случае она
должна создать исключение NoReportsException:
public class ReporterTests
{
[Fact]
public void IfNotReportsThenThrowException()
{
var reporter = new Reporter();
reporter.SendReports();
// ???
}
}
Как в этом случае задать поведение объектов, которые использует Reporter? Мы же должны «сказать» ReportBuilder'у вернуть пустой список,
и тогда функцияSendReports выбросит исключение. Но в текущей реализации Reporter'а сделать мы этого не можем. Получается, мы не можем задать
такие входные данные, при которых SendReports выкинет исключение. Значит в данной реализации объект Reporter очень плохо поддается тестированию.
Связанность
Дело в том, что функция SendReports, кроме своей прямой обязанности,
слишком много знает и умеет:
 знает, что именно ReportBuilder будет создавать отчеты
 знает, что все отчеты надо отсылать через email с помощью EmailReportSender
 умеет создавать объект ReportBuilder
 умеет создавать объект EmailReportSender
Здесь нарушается принцип единственности ответственности. Проблема
заключается в том, что в данный момент внутри функции SendReports объект ReportBuilderсоздается оператором new. А если у него появятся обязательные параметры в конструкторе? Нам придется менять
код в классе Reporter да и во всех других классах, которые использовали оператор new для ReportBuilder'а.
К тому же, первые пункты нарушают принцип открытости/закрытости.
Дело в том, что если мы захотим с помощью нашей утилиты отсылать сообщения через SMS, то придется изменять код класса Reporter. Вместо EmailReportSender мы должны будем написать SmsReportSender. Еще
сложнее ситуация, когда одна часть пользователей класса Reporter захочет
отправлять сообщения через emal, а вторая через SMS.
Обратите внимание, что наш объект Reporter зависит не от абстракций, а
от конкретных объектов ReportBuilder и EmailReportSender. Можно сказать,
что он "сцеплен" с этими классами. Это и объясняет его хрупкость при изменениях в системе. Может оказаться, что Reporter жестко зависит от двух
классов, эти два класса зависят еще от 4х других. Получится, что вся система
– это клубок из стальных ниток, который нельзя ни изменить, ни протестировать. Этот пример наглядно показывает нарушение принципа инверсии зависимостей.
Применяем принцип инверсии зависимостей
Для
начала
вынесем
интерфейсы IReportSender из EmailReportSender и IReportBuilder из ReportBuilder.
public interface IReportBuilder
{
IList<Report> CreateReports();
}
public interface IReportSender
{
void Send(Report report);
}
Теперь вместо того, чтобы создавать объекты в функции SendReports,
мы передами их объекту Reporter в конструктор:
public class Reporter : IReporter
{
private readonly IReportBuilder reportBuilder;
private readonly IReportSender reportSender;
public Reporter(IReportBuilder reportBuilder,
IReportSender reportSender)
{
this.reportBuilder = reportBuilder;
this.reportSender = reportSender;
}
public void SendReports()
{
IList<Report> reports = reportBuilder.CreateReports();
if (reports.Count == 0)
throw new NoReportsException();
foreach (Report report in reports)
{
reportSender.Send(report);
}
}
}
Во время создания объекта Reporter в самом начале программы мы будем задавать конкретные IReportBuilder и IReportSender и передавать их в
конструктор:
public static void Main()
{
var builder = new ReportBuilder();
var sender = new SmsReportSender();
var reporter = new Reporter(builder, sender);
reporter.SendReports();
}
Посмотрим, какие проблемы мы смогли решить.
Тестируемость
Теперь у нас есть возможность передавать в конструктор Reporter'а объекты, которые реализуют нужные интерфейсы. Давайте подставим mockобъекты и зададим нужное нам поведение:
public class ReporterTests
{
[Fact]
public void IfNotReportsThenThrowException()
{
var builder = new Mock<IReportBuilder>();
builder.Setup(m => m.CreateReports()).
Returns(new List<Report>());
var sender = new Mock<IReportSender>();
var reporter = new Reporter(builder.Object, sender.Object);
Assert.Throws<NoReportsException>(() => reporter.SendReports());
}
}
Тест прошел! Теперь есть возможность задавать поведение объектов, с
которыми работает наш Reporter. И в данном случае нам не важно, что где-то
есть EmailReportSender, SmsReportSender или еще какой-то *ReportSender.
Тесты Reporter'а не зависят от других реализаций, мы используем только интерфейсы. Это делает тесты более устойчивыми к изменениям в системе.
Связанность
Мы реализовали на практике главный принцип инверсии зависимостей.
Наш Reporter зависит только от абстракций (интерфейсов).
Как быть, если мы хотим отсылать отчеты не через email, а через SMS?
Теперь сделать это проще простого . Надо передать в конструктор Reporter'а
неEmailReportSender, а SmsReportSender. Код самого Reporter'а мы изменять
уже не будем.
Тем не менее меня такое решение еще не полностью устраивает. Мне не
нравится, что где-то в клиентах моей библиотеки будет куча строк типа:?
var builder = new ReportBuilder();
var sender = new SmsReportSender();
var reporter = new Reporter(builder, sender);
// ...
Первым
решением
может
стать
использование фабрики объектов Reporter. В принципе это рабочее решение, но мы пойдем еще дальше. Я хочу, чтобы конфигурирование объектов моей программы
происходило один раз и находилось в одном месте.
Шаг 3. Используем ServiceLocator
Наша цель - задавать соответствие интерфейсов и их реализаций. Сделаем наше приложение конфигурируемым на клиенте!
Нам нужен объект, который будет хранить информацию о том, что интерфейсу IReportSender соответствует реализация EmailReportSender. Назовем этот объектServiceLocator. Связь интерфейса и реализации он будет хранить во внутреннем словаре:
public static class ServiceLocator
{
private static readonly Dictionary<Type, Type> services =
new Dictionary<Type, Type>();
public static void RegisterService<T>(Type service)
{
services[typeof (T)] = service;
}
public static T Resolve<T>()
{
return (T) Activator.CreateInstance(services[typeof (T)]);
}
}
Теперь рассмотрим, как мы будем его использовать. Для начала зарегистрируем связи:
public static void Main()
{
ServiceLocator.RegisterService<IReportBuilder>
(typeof(ReportBuilder))
ServiceLocator.RegisterService<IReportSender>
(typeof(SmsReportSender));
Теперь у класса Reporter создадим конструктор, который пользуется
этими настройками:
public class Reporter : IReporter
{
private readonly IReportBuilder reportBuilder;
private readonly IReportSender reportSender;
public Reporter() : this(ServiceLocator.Resolve<IReportBuilder>(), Ser
tor.Resolve<IReportSender>())
{
}
public Reporter(IReportBuilder reportBuilder, IReportSender reportSend
{
this.reportBuilder = reportBuilder;
this.reportSender = reportSender;
// ...
После инициализации ServiceLocator'а вызываем в любом месте программы пустой конструктор:
var reporter = new Reporter();
reporter.SendReports();
С таким подходом мы можем задать соответствие интерфейсов и их реализаций один раз и использовать его. Чтобы во всем приложении вместо SmsReportSenderиспользовать EmailReportSender, надо в начале выполнения программы (сайта, сервиса и т.д.) изменить:
ServiceLocator.RegisterService<IReportSender>(typeof(SmsReportSender));
на другую реализацию IReportSender'а:
ServiceLocator.RegisterService<IReportSender>(typeof(EmailReportSender));
В чем же отличие? Дело в том, что раньше классы сами знали от каких
объектов они зависят и могли напрямую использовать друг друга:
Теперь объекты знают только про интерфейсы классов, с которыми взаимодействуют, а реализации просят у сервиса:
Download