Страницы

суббота, 24 сентября 2011 г.

Тёмная сторона обобщений (Generic'ов)

   Какое главное преимущество обобщений? Конечно же безопасность типов. Но иногда желание безопасного по типам кода превосходит пределы разумного. Главная мотивация - это попытка убрать дублирование с помощью единообразной обработки для иерархии классов + безопасность типов во время компляции. Вот один из примеров: пусть мы разрабатываем систему управления доставкой. У нас есть два вида доставки  - автомобилем или курьером.   Товар для доставки передаётся на склад доставки, откуда передаётся для доставки курьерам или машинам. Код может выглядеть так:
public abstract class DeliveryBase
{
  public string Name { get; set; }

  public int Goods { get; set; }
}

public class CarDelivery : DeliveryBase
{
  public int MaxPackages { get; set; }
}

public class CourierDelivery : DeliveryBase
{
  public Routine Routine { get; set; }

  public decimal MaxWeightInTons { get; set; }
}

public abstract class DeliveryStore<TDelivery>
  where TDelivery : DeliveryBase
{
  private IList<TDelivery> _deliveries;

  public void EnqueueDelivery(TDelivery delivery)
  {
    _deliveries.Add(delivery);
  }

  public TDelivery DequeueDelivery()
  {
    return _deliveries.FirstOrDefault();
  }
}

public class CourierDeliveryStore : DeliveryStore<CourierDelivery> 
{
}

public class CarDeliveryStore : DeliveryStore<CarDelivery> 
{
}
  Проблема. На первый взгляд кажется, что мы получили конкретных тип в наследниках DeliveryStore. И это вроде бы хорошо. Представим себе, что мы хотим сделать обработчик, который берёт товар со склада и распределяет его по свободным курьерам и машинам, и конечно же ему надо знать точный тип склада. Тогда мы получим что-то вроде этого:
ppublic abstract class DeliveryProcessorBase<TDelivery, TDeliveryStore> 
  where TDelivery : DeliveryBase
  where TDeliveryStore : DeliveryStore<TDelivery>
{
  public TDeliveryStore DeliveryStore { get; set; }

  protected DeliveryProcessorBase(TDeliveryStore deliveryStore)
  {
    DeliveryStore = deliveryStore;
  }

  public abstract void Process();
}

public class CourierDeliveryStoreProcessor : 
  DeliveryProcessorBase<CourierDelivery, CourierDeliveryStore>
{
  public CourierDeliveryStoreProcessor(CourierDeliveryStore deliveryStore) : 
    base(deliveryStore)
  {
  }

  public override void Process()
  {
    DispatchCourier(DeliveryStore.DequeueDelivery());
  }
}

public class CarDeliveryStoreProcessor : 
  DeliveryProcessorBase<CarDelivery, CarDeliveryStore>
{
  public CarDeliveryStoreProcessor(CarDeliveryStore deliveryStore)
    : base(deliveryStore)
  {
    
  }

  public override void Process()
  {
    DispatchCar(DeliveryStore.DequeueDelivery());
  }
}

  Что же выиграли? Только то, что знаем точный тип 1-го свойства у двух иерархий классов. Зато проблем больше, и они серьёзнее.   Во первых, мы не можем сделать единый обработчик для курьеров и машин, т.к. для обработки мы должны знать точный тип доставки. Это заставляет делать иерархию классов с обобщённым базовым классом, для каждой реализации обобщения. Если нам нужен один обработчик, то будет одна иерархия. Но если их будет 10, то уже придётся написать 30 классов. Еще один вид доставки даст 10 новых классов. Параллельные иерархии - скверный запах кода, Фаулер подтвердит :).
  Вторая проблема меньше, но лично мне она также неприятна. Ограничения where. Будучи один раз введёнными в классе Store, они как вирус расползаются по всем клиентам класса Store, всем клиентам клиентов Store и т.д., засоряя код малополезной информацией. Иногда ограничения на параметр-тип составляют большую часть кода класса.
На этом месте скажите себе стоп. Есть хорошие новости: есть простые ООП-приёмы, которые с большим успехом решают проблему полиморфной обработки, не добавляя в код сомнительных ароматов.
  Решение
  Вариант 1. Старый добрый полиморфизм. Суть проблемы в том, что нам нужно знать конкретный тип доставки для ее обработки. А решение - сам класс лучше всех знает свой тип! Так почему бы не доверить ему и выполнять вызов зависящих от его типа методов? Тогда можно переписать код в таком виде:
public abstract class DeliveryBase
{
  public string Name { get; set; }

  public int Goods { get; set; }
  // Этот метод в классах наследниках будет вызывать "правильный" метод на deliveryProcessor
  public abstract void DispathUsing(DeliveryProcessor deliveryProcessor);
}

public class CarDelivery : DeliveryBase
{
  public int MaxPackages { get; set; }

  public override void DispathUsing(DeliveryProcessor deliveryProcessor)
  {
    deliveryProcessor.Dispatch(this);
  }
}

public class CourierDelivery : DeliveryBase
{
  public Routine Routine { get; set; }

  public decimal MaxWeightInTons { get; set; }

  public override void DispathUsing(DeliveryProcessor deliveryProcessor)
  {
    deliveryProcessor.Dispatch(this);
  }
}
//Иерархия классов склада больше не нужна - будем использовать базовый класс
public class DeliveryStore
{
  private IList<DeliveryBase> _deliveries;

  public void EnqueueDelivery(DeliveryBase delivery)
  {
    _deliveries.Add(delivery);
  }

  public DeliveryBase DequeueDelivery()
  {
    return _deliveries.FirstOrDefault();
  }
}
//Иерархия обработчиков также не нужна. 
public class DeliveryProcessor
{
  public DeliveryStore DeliveryStore { get; set; }

  protected DeliveryProcessor(DeliveryStore deliveryStore)
  {
    DeliveryStore = deliveryStore;
  }

  public void Process()
  {
    var delivery = DeliveryStore.DequeueDelivery();

    delivery.DispathUsing(this);
  }
  // Эти методы и будут вызывать наследники Delivery.
  public void Dispatch(CarDelivery carDelivery) {}

  public void Dispatch(CourierDelivery courierDelivery) {}
}
  Этот код намного лучше, и вот почему: мы избавились от двух иерархий: DeliveryStore и DeliveryProcessor, сохранив основные преимущества обобщений - безопасность типов. Правда, если обработчиков множество, то со временем классы DeliveryBase будут перегружены разными функциями, которые имеют малое отношение к самому классу. И здесь нам на помощь приходит второй способ.
  Вариант 2. Шаблон Visitor (Посетитель). По сути, это абстракция первого способа, позволяющая создавать неограниченное количество обработчиков без вмешательства в код класса. Основу шаблона составляет интерфейс IDeliveryVisitor:
public interface IDeliveryVisitor
{
  void Visit(CarDelivery carDelivery);

  void Visit(CourierDelivery courierDelivery);
}
А для его полноценного использования, надо дополнить классы Delivery кодом, который бы принимал посетителя:
public abstract class DeliveryBase
{
  public string Name { get; set; }

  public int Goods { get; set; }

  public abstract void Accept(IDeliveryVisitor deliveryVisitor);
}

public class CarDelivery : DeliveryBase
{
  public int MaxPackages { get; set; }

  public override void Accept(IDeliveryVisitor deliveryVisitor)
  {
    deliveryVisitor.Visit(this);
  }
}

public class CourierDelivery : DeliveryBase
{
  public Routine Routine { get; set; }

  public decimal MaxWeightInTons { get; set; }

  public override void Accept(IDeliveryVisitor deliveryVisitor)
  {
    deliveryVisitor.Visit(this);
  }
}
В результате мы снова можем избавить от иерархии классов для складов доставки и обработчиков:
public class DeliveryStore
{
  private IList<DeliveryBase> _deliveries;

  public void EnqueueDelivery(DeliveryBase delivery)
  {
    _deliveries.Add(delivery);
  }

  public DeliveryBase DequeueDelivery()
  {
    return _deliveries.FirstOrDefault();
  }
}

public class DeliveryProcessor : IDeliveryVisitor
{
  public DeliveryStore DeliveryStore { get; set; }

  protected DeliveryProcessor(DeliveryStore deliveryStore)
  {
    DeliveryStore = deliveryStore;
  }

  public void Process()
  {
    var delivery = DeliveryStore.DequeueDelivery();

    delivery.Accept(this);
  }
  
  public void Visit(CarDelivery carDelivery)
  {
    Dispatch(carDelivery);  
  }

  public void Visit(CourierDelivery courierDelivery)
  {
    Dispatch(courierDelivery);
  }

  public void Dispatch(CarDelivery carDelivery) { }

  public void Dispatch(CourierDelivery courierDelivery) { }
}
  Как видите, очень похоже. Но в отличии от первого варианта, мы можем добавлять любое количество новых обработчиков. Надеюсь, этот пример поможет многим избежать ошибок, на которые так соблазняют обобщения. :)

четверг, 8 сентября 2011 г.

Model-View-Presenter - два пути создания презентеров.

Проблема.

  Пусть у нас есть WinForms приложение, созданное с применением шаблона Model-View-Presenter. Логичным шагом будет наличие главного презентера, реализующим логику работу главной формы. И одна из главных обязанностей - создание и отображение дочерних презентеров. Звучит просто, но в реализации возникает главная сложность: как создавать дочерние презентеры? Я бы принял во внимание два возможных варианта действий. Оба из них предполагают использования шаблона Ioc/DI, и любого контейнера на ваш вкус.
  Рассмотрим нашего подопытного:
public class MainPresenter
{
  public void ShowClients()
  {
    //как создать и отобразить презентер?
  }

  public void ShowVendors()
  {
    //как создать и отобразить презентер?
  }

  // Много других аналогичных методов.

}

Вариант 1: Абстрактная фабрика.

  Название как-бы намекает нам на абстрактный класс, но лучше пропустить этот намёк и сделать интерфейс:
public interface IPresenterFactory
{
   IPresenter CreateClientsPresenter();

   IPresenter CreateVendorsPresenter();

   // Создание остальных видов презентеров
}
И тогда код презентера обретает вид:
public class MainPresenter
{
  public MainPresenter(IPresenterFactory presenterFactory)
  {
    _presenterFactory = presenterFactory;
  }

  public void ShowClients()
  {
    var presenter = _presenterFactory.CreateClientsPresenter();
    presenter.Start();
  }

  public void ShowVendors()
  {
    var presenter = _presenterFactory.CreateVendorsPresenter();
    presenter.Start();
  }

  // Много других аналогичных методов.
}
Еще одна вариация на тему - обобщённая версия абстрактной фабрики:
public interface IPresenterFactory
{
   IPresenter CreatePresenter<TPresenter>() where TPresenter : IPresenter;
}
Замечу, возвращаемое значение  IPresenter. Возвращать TPresenter крайне нежелательно, поскольку мы сильно затрудним тестирование. У конкретных реализаций презентеров могут быть зависимости, у которых в свою очередь могут быть зависимости... Создать модульный тест в таком случае будет почти невозможно.  Для обобщённой фабрики презентер будет выглядеть так:
public class MainPresenter
{
  public MainPresenter(IPresenterFactory presenterFactory)
  {
    _presenterFactory = presenterFactory;
  }

  public void ShowClients()
  {
    var presenter = _presenterFactory.Create<ClientsPresenter>();
    presenter.Start();
  }

  public void ShowVendors()
  {
    var presenter = _presenterFactory.Create<VendorsPresenter>();
    presenter.Start();
  }

  // Много других аналогичных методов.
}
Теперь окинем код взглядом еще раз, и оценим достоинства.
Необобщённая версия:
  • + Конкретная реализации презентеров скрыта.
  • + Возможные варианты создаваемых презентеров явно видны. 
  • + В создающие методы можно добавить параметры.
  • - Много однообразного кода.
Обобщённая версия:
  • + "Халявная" поддежка новых типов презентеров. 
  • + Экономия кода. 
  • - Нарушение принципов сокрытия информации и OCP
Возможен и гибридный вариант, помесь обобщённой и необобщённой фабрик. В таком случае их достоинства и недостатки так же объединятся, и к ним добавиться еще один недостаток: неоднозначность выбора метода создания презентера.

Вариант 2: Диспетчер событий.

Второй способ я уже описывал в прошлом посте. Но для ясности повторю идею. Если у нас есть Event Aggregator:
public interface IPublisher
{
  void Publish<TMessage>(TMessage message);
}
Чтобы использовать диспетчер событий, надо создать классы событий:
public class ShowClientsMessage{}

public class ShowVendorsMessage{}
И классы обработчики событий:
public class ShowClientsMessageListener: IListener<ShowClientsMessageListener>
{
  public ShowClientsMessageListener(ClientsPresenter presenter)
  {
    _presenter = presenter;
  }

  public void ListenTo(ShowClientsMessage message)
  {
     presenter.Start();
  }
}
// второй класс не показан для краткости. 
То с их помощью можно значительно упростить главный презетер:
public class MainPresenter
{
  public MainPresenter(IPublisher publisher)
  {
    _publisher = publisher;
  }

  public void ShowClients()
  {
    _publisher.Publish(new ShowClientsMessage());

  }

  public void ShowVendors()
  {
    _publisher.Publish(new ShowVendorsMessage());
  }

  // Много других аналогичных методов.
}
Детали реализации можно найти в предыдущих постах. А сейчас немного о преимуществах и недостатках:
  • + Скрыта не только конкретная реализация презентеров, но способы их создания и отображения! Это даёт огромную гибкостью 
  • + Через классы сообщений легко передавать параметры.
  • + Высокая тестируемость главного презентера и обработчиков событий.  
  • - Требуется больше намного больше кода.
  • - Менее очевидны зависимости

AbstractFactory vs EventAggregator. 

  Что же выбрать? Этот вопрос традиционно переходит в вопрос гибкость против сложности.
Абстрактная фабрика проста в реализации, ее легко отлаживать. В то же время сокрытие информации с диспетчером событий даёт фантастическую гибкость. Я предпочитаю гибкость диспетчера событий, т.к. набрать немного простого кода не так сложно, а поддерживать его намного проще.
  Стоит отметить, что грамотное использование современных контейнеров (и точек их расширения) позволяет существенно уменьшить объёмы ручного труда в обоих вариантах. Но об этом позже.

вторник, 6 сентября 2011 г.

Реализуем EventPublisher. Послесловие.

   Этот пост должен идти как предисловие к серии постов Реализуем EventPublisher. Но хорошая мысль, как это с ней часто бывает, пришла позже. :) Поэтому о назначении (читай решаемых проблемах) шаблона Event Aggerator расскажу только сейчас.
  Для начала понадобиться пример проблемы. Пусть есть некое приложение c архитектурой MVVM. Скажем, модель для главного меню может выглядеть примерно так:
public class MainMenuVewModel
{
  public MainMenuVewModel(
                                         IClientsViewModel clientsViewModel,  
                                         ICountriesViewModel countriesViewModel,
                                         ICitiesViewModel citiesViewModel,
                                         IWindowManager windowManager)
  {
    _clientViewModel = clientsViewModel;
    _countriesViewModel = countriesViewModel;
    _citiesViewModel = citiesViewModel;
    _windowManager = windowManager;
  }

  public void ShowClients()
  {
    _windowManager.Show(_clientsViewModel);
  }

  public void ShowCounrties()
  {
    _windowManager.Show(_clientsViewModel);
  }

  public void ShowCities()
  {
    _windowManager.Show(_clientsViewModel);
  }
}
  Проблема: Этот код не так уже плох, но это может быть далеко не полным перечнем всех зависимостей, которые понадобятся нашему меню. Это затрудняет его понимание, и не очень хорошо для тестирования. Ведь для модульного теста придётся написать как минимум 4 мок-объекта. Хотелось бы более элегантный способ, который бы не требовал каждый раз добавлять новую зависимость для нового пункта меню, что включает в себя как минимум три действия (объявление члена класса, объявление параметра конструктора, создание нового метода). 
  Решение: Необходим единый способ диспетчеризации событий главного меню, т.е. Event Aggregator. Напомню основные его интерфейсы:
public interface IPublisher
{
  void Publish<TMessage>(TMessage message);
}

public interface IListener<TMessage>
{
  void ListenTo(TMessage message);
}
  Что же с ними делать? Давайте по порядку:
  1. Определить необходимые сообщения:

public class ShowClientsMessage {}

public class ShowCountriesMessage {}

public class ShowCitiesMessage {}
  2. Переписать модель главного меню с помощью IPublisher.  
public class MainMenuVewModel
{
  public MainMenuVewModel(IPublisher publisher)  
  {
    _publisher = publisher;
  }

  public void ShowClients()
  {
     _publisher.Publish(new ShowClientsMessage());
  }

  public void ShowCounrties()
  {
     _publisher.Publish(new ShowCountriesMessage());
  }

  public void ShowCities()
  {
     _publisher.Publish(new ShowCitiesMessage());
  }
}
  3. Написать обработчики событий:
public class ShowViewModelListener : TransientListenter<ShowClientsMessage>
{
  public MainMenuVewModel(IWindowManager windowManager, IClientsViewModel clientsViewModel)  
  {
    _windowManager = windowManager;
    _clientsViewModel = clientsViewModel; 
  }

  public void Handle(ShowClientsMessage message)
  {
     _windowManager.Show(_clientsViewModel);
  }
}
// обработчики двух других событий выглядят идентично, для краткости их не привожу. 
  Что в итоге? Мы получили более простую модель главного меню за счёт вынесения сложности на другие классы. Сложность системы несколько увеличилась, но вместе с тем увеличилась и ее гибкость. Стоит ли игра свеч решать вам.

понедельник, 5 сентября 2011 г.

Создание nuget-пакета с помощью UppercuT

  В прошлый раз я рассказывал, как настроить сборку проекта с помощью UppercuT. Продолжая тему, расскажу как настроить создание nuget-пакетов с его помощью.
Для начала надо определиться, какие задачи надо решить при создании пакетов:
  1. Создание пакета Solyutor.EventPublisher
  2. Создание пакета Solyutor.EventPublisher.Windsor, зависящего от Solyutor.EventPublisher и Castle.Windsor.
  Итак, создание пакета начинается с создание nuspec-файла. Согласно соглашениям UppercuT, nuspec файлы должны быть в папке SolutionFolder\nuget. Там же есть и nuspec-шаблон, заполнить который не составляет труда:
<?xml version="1.0"?>
<package xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <metadata>
    <id>Solyutor.EventPublisher</id>
    <version>DO_NOT_EDIT</version>
    <authors>Jury Soldatenkov</authors>
    <owners>Jury Soldatenkov</owners>
    <summary>Simple extensible event aggregator for net.</summary>
    <description>Simple extensible event aggregator for net.</description>
    <projectUrl>https://github.com/solyutor/Solyutor.EventPublisher</projectUrl>
  </metadata>
  </package>
  И схожий файл для Solyutor.EventPublisher.Windsor, с прописанными зависимостями на Solyutor.EventPublisher.
<?xml version="1.0"?>
<package xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <metadata>
    <id>Solyutor.EventPublisher.Windsor</id>
    <version>DO_NOT_EDIT</version>
    <authors>Jury Soldatenkov</authors>
    <owners>Jury Soldatenkov</owners>
    <summary>Castle Windsor extensions for Solyutor.EventPublisher</summary>
    <description>Provides facility that allows transient listeners to handle messages.</description>
    <projectUrl>https://github.com/solyutor/Solyutor.EventPublisher</projectUrl>
    <dependencies>
      <dependency id="Castle.Windsor" version="2.5.3" />
      <dependency id="Solyutor.EventPublisher" version="DO_NOT_EDIT"/>
    </dependencies>
  </metadata>
  </package>
  Версия пакета заполнять необязательно, ее проставит UppercuT. Но на этом этапе поджидала небольшая засада: версия проекта подставлялась правильно, но аттрибут version для зависимости Solyutor.EventPublisher не обновился. Посмотрев файл nugetPrepare.step в папке build обнаружил ответственный за версии nant-код:
<xmlpoke
file="${spec.file}"
xpath="/package/metadata/version"
value="${assembly.version.full}" />
  Чуть поразмыслив над проблемой, дописал в той же задаче простановку версии зависимости из проекта:
<xmlpoke
file="${spec.file}"
xpath="/package/metadata/dependencies/dependency[@id = 'Solyutor.EventPublisher']/@version"
value="${assembly.version.full}" />
  Это скорее хак, чем решение правильное решение проблемы. Но это единственное место, где версия проекта уже есть в виде установленного nant-свойства.
  С версиями разобрались, но в текущем виде Uppercut создаст два nuget-пакета с одинаковым содержанием: все сборки проекта и все сборки сборки Castle.Windsor. А надо бы только необходимое для пакета файлы...
Решение: надо создать две папки для разных проектов, и скопировать в них только необходимые сборки, и соответствующие nuspec-файлы. И здесь на сцену выходят точки расширения UppercuT. Воспользовавшись руководством, создадим файла для nugetPrepare.post.step. Полную версию можно посмотреть на github, а здесь представлю только значимую выдержку:

<property name="dirs.publisher" value="${dirs.drop.nuget}\Publisher"/>
<property name="dirs.publisher.windsor" value="${dirs.drop.nuget}\Publisher.Windsor"/>

<target name="go">
	
  <delete dir="${dirs.publisher}" failonerror="false"/>
  <delete dir="${dirs.publisher.windsor}" failonerror="false"/>
	
  <mkdir dir="${dirs.publisher}" />
  <mkdir dir="${dirs.publisher.windsor}"/>
	
  <copy todir="${dirs.publisher}\lib">
    <fileset basedir="${dirs.drop.nuget}\lib">
      <include name="Solyutor.EventPublisher.dll"/>
      <include name="Solyutor.EventPublisher.pdb"/>
    </fileset>
  </copy>
	
  <copy todir="${dirs.publisher.windsor}\lib">
    <fileset basedir="${dirs.drop.nuget}\lib">
      <include name="Solyutor.EventPublisher.Windsor.dll"/>
      <include name="Solyutor.EventPublisher.Windsor.pdb"/>
    </fileset>
  </copy>
	
  <copy file="${dirs.drop.nuget}\lib\License.txt" todir="${dirs.publisher}"/>
  <copy file="${dirs.drop.nuget}\lib\License.txt" todir="${dirs.publisher.windsor}"/>
	
  <move file="${dirs.drop.nuget}\Solyutor.EventPublisher.nuspec" todir="${dirs.publisher}"/>
  <move file="${dirs.drop.nuget}\Solyutor.EventPublisher.Windsor.nuspec" todir="${dirs.publisher.windsor}"/>

</target>
На этом настройка заканчивается. Можно запускать build.bat, и в папке code_drop/nuget будут два пакета nuget.
  Спасибо UppercuT!