вторник, 20 мая 2014 г.

Проектируем стандартное приложение (Ninject+WinForm)

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

Используемый инструментарий

Для примера, я буду использовать Ninject в качестве DI и Windows Form, в качестве UI. Понятное дело, что это совершенно не принципиально, так как тонкости различных DI систем я тут не рассматриваю, а принципиально они все решают одни и те же задачи.

Постановка задачи

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

Простейшая структура

Для примера, создадим структуру, состоящую всего из трех классов.
// Тип транспорта
public class TransportType
{
   public int Id { get; set; }
   public string Caption { get; set; }
}
// Модель транспорта
public class TransportModel
{
   public int Id { get; set; }
   public string Caption { get; set; }
   public string Description { get; set; }
}
// Конкретная единица - транспортное средство
public class Transport
{
   public int Id { get; set; }
   public string Caption { get; set; }
   public TransportType Type { get; set; }
   public TransportModel Model { get; set; }
}
Естественно, я предполагаю, что каждый класс будет отмапирован на табличку в некотором репозитории/базе данных с использованием какого-нибудь ORM fraimwork-а. Однако, для данной статьи это не принципиально.

Пишем первый контроль

Следующим шагом я предлагаю начать писать первый редактор - UserControl, редактирующий и добавляющий новый экземпляр класса Transport. Возможно, для кого-то покажется странным такой резкий скачек от структуры данных к UI. Однако, в этом есть свой резон - наметив крайние точки программы, вам будет гораздо легче писать середину. Итак, заготовка нашего первого редактора будет выглядеть примерно так:
public partial class TransportEditor : UserControl
{
   public TransportEditor()
   {
      InitializeComponent();
   }

   // Код, обслуживающий кнопочки, поля ввода, валидацию и т. д.
}
Очевидно, в нашем редакторе должно быть поле ввода, для редактирования свойства Caption и пару элементов управления, позволяющие выбрать из списков нужную модель и тип транспортного средства. Не имеет смысла описывать тут способ размещения указанных элементов управления и варианты валидации вводимых данных - это дело вкуса. Зато, весьма интересно подумать над тем, как наш редактор будет взаимодействовать с данными.

Структура взаимодействия

Итак, мы видим, что для правильной работы редактора нам необходимо получить два списка объектов TransportType и TransportModel. Как именно эти объекты будут доставаться из базы данных, мы подумаем потом. На стадии проектирования UI нам это совершенно не интересно и отвлекаться на это не стоит. Вместо этого, мы просто объявим два интерфейса (такие интерфейсы я рекомендую размещать в отдельной сборке - так будет удобнее потом):
public interface ITransportTypeManager
{
   IEnumerable<TransportType> FullList { get; }
}

public interface ITransportModelManager
{
   IEnumerable<TransportModel> FullList { get; }
}
Где будут находиться реализации этих интерфейсов и как они будут работать нас сейчас не интересует. Однако, без них наш контроль корректно работать не может. Поэтому, потребуем передать нам эти интерфейсы в конструкторе. И код нашего редактора преобразуется в:
public partial class TransportEditor : UserControl
{
   ITransportTypeManager typeManager;
   ITransportModelManager modelManager;

   public TransportEditor( ITransportTypeManager typeManager, ITransportModelManager modelManager )
   {
      InitializeComponent();

      this.typeManager = typeManager;
      this.modelManager = modelManager;
   }

   // Код, обслуживающий кнопочки, поля ввода, валидацию и т. д.
}
После чего, мы спокойно начинаем использовать полученные интерфейсы, запрашивая списки и формируя их визуальное представление. После того, как пользователь введет все необходимые данные, нам нужно будет создать по ним новый объект транспортного средства. Как это сделать, мы пока не знаем - ведь мы еще даже не определились окончательно с движком ORM (может, попробовать EntityFramework или написать на чистом SQL? Еще есть неплохое решение от Telerik.. Или старый-добрый Genome?). Однако, это ни сколько не мешает нам завершить работу над контролем. Просто добавим еще один интерфейс:
public interface ITransportManager
{
   Transport Create( string caption, TransportType type, TransportModel model );
}
Конечно же, его тоже надо потребовать в конструкторе:
public partial class TransportEditor : UserControl
{
   ITransportTypeManager typeManager;
   ITransportModelManager modelManager;
   ITransportManager transportManager;

   public TransportEditor( ITransportTypeManager typeManager, ITransportModelManager modelManager, ITransportManager transportManager )
   {
      InitializeComponent();

      this.typeManager = typeManager;
      this.modelManager = modelManager;
      this.transportManager = transportManager;
   }

   // Код, обслуживающий кнопочки, поля ввода, валидацию и т. д.
}
Обращаясь к transportManager, завершим создание нового транспортного средства и отложим контроль в сторону.

Ninject

Теперь пришло время задуматься, как контроль будет создаваться. Предположим, в нашем главном окне есть специальная кнопочка, по клику на которую мы предполагали сделать примерно следующее:
public void OnCreateNewClick()
{
   var editor = new TransportEditor();
   editor.Dock = DockStyle.Fill;
   Controls.Add( editor );
}
Однако, сейчас такой код работать не будет - ведь TransportEditor больше не имеет конструктора без параметров. Вот тут-то на сцене и появляется Ninject. Там же, в главном окне, заведем переменную ядра Ninject и проинициализируем ее:
IKernel kernel;

void InitInjections()
{
   kernel = new StandardKernel();
}
После чего, слегка модифицируем код по созданию редактора:
public void OnCreateNewClick()
{
   var editor = kernel.Get<TransportEditor>();
   editor.Dock = DockStyle.Fill;
   Controls.Add( editor );
}
Такой код уже скомпилируется и даже запустится. И, лишь в момент создания редактора, Ninject сообщит нам, что реализации интерфейсов отсутствуют. Однако, это все еще не повод писать полноценные реализации. Что бы отладить уже написанный код, нам достаточно написать реализации-заглушки.

Ninject модули

Конечно, мы понимаем, что заглушки - это решение временное и, конечно же, нам хочется затратить минимум усилий, что бы перейти на полноценные реализации. Я, к примеру, не стал бы писать никаких заглушек, если бы это потребовало от меня более 10-ти минут моего драгоценного времени. К счастью в Ninject есть очень удобный механизм модулей. Итак, сами заглушки будут выглядеть так:
class DummyTransportTypeManager : ITransportTypeManager
{
   public IEnumerable<TransportType> FullList
   {
      get { return Enumerable.Empty<TransportType>(); }
   }
}

class DummyTransportModelManager : ITransportModelManager
{
   public IEnumerable<TransportModel> FullList
   {
      get { return Enumerable.Empty<TransportModel>(); }
   }
}

class DummyTransportManager : ITransportManager
{
   public Transport Create( string caption, TransportType type, TransportModel model )
   {
      return null;
   }
}
Далее пишем наш первый модуль, в котором эти заглушки будут подключаться:
class TestNinjectModule : NinjectModule
{
   public override void Load()
   {
      Bind<ITransportTypeManager>().To<DummyTransportTypeManager>();
      Bind<ITransportModelManager>().To<DummyTransportModelManager>();
      Bind<ITransportManager>().To<DummyTransportManager>();
   }
}
Как мы видим, модуль - это класс наследник NinjectModule и реализующий абстрактный метод Load. Итак, нам осталось сделать всего один шаг - подключить написанный модуль к нашей программе. Для этого, слегка подкорректируем код инициализации Ninject:
void InitInjections()
{
   kernel = new StandardKernel( new TestNinjectModule() );
}
Поле этого наша программа становится целиком и полностью рабочей. Аналогично, мы можем добавить и другие элементы управления, полностью отладив весь UI. И лишь после этого преступить к написанию настоящих реализаций. Конечно, я не утверждаю, что этот последний этап будет совсем простым. Однако, к этому моменту наша система будет полностью спроектирована. У нас уже не будут возникать философские вопросы - мы будем точно знать, что надо делать.
Остались вопросы? Пишите в комментарии. :)

2 комментария: