Страницы

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

NHibernate. Вникаем в hilo генератор.

  Для большинства пользователей NHibernate hilo генератор остаётся туманным, и они предпочитают старый недобрый identity. Давайте разберём их его по косточкам.
Сама аббревиатура hilo обозначает High Low, которые обозначают два числа. На примере его можно объяснить так: программа запрашивает в некоем хранилище уникальное High число. Допустим, это будет 5. При этом Low число равно 9. Тогда мы можем безопасно присваивать в качестве идентификатора числа 50,51,52,...59. После исчерпания чисел приложение запрашивает новое число High. Такой механизм позволяет безопасно присваивать идентификаторы на стороне клиента, даже для множества клиентов. Таков сам принцип генератора hilo, теперь посмотрим, как он реализован в nhibernate.
  Для начала образец нашего наш "домена":
public class Cat
{
  public virtual int Id { get; set; }

  public virtual string Name { get; set; }
}
и его маппинг:
<class name="Cat">
  <id name="Id">
    <generator class="hilo">
    <param name="table">HighNumbers</param>
    <param name="column">NextHigh</param>
    <param name="max_lo">4</param>
    </generator>
  </id>
  <property name="Name"/>
</class>
  • table- название таблице, содержащей информацию о текущем High числе.
  • column - название колонки, содержащей информацию о текущем High числе.
  • max_lo - максимально low число, которое захватывает nhibernate.
  Рассмотрим на живых примерах. Пустим в дело такой код:
[Test]
public void SaveManyCats()
{
  using(var session = _sessionFactory.OpenSession())
  using (var tx = session.BeginTransaction())
  {
    for (int i = 0; i < 15; i++)
    {
      session.Persist(new Cat{Name = "Cat number " + i});
    }
    tx.Commit();
  }
}
Для начала выполним его с identity-генератором. И вот что будет в логах:

2011-11-25 23:06:19,288 DEBUG [7] - (SQL) :
    INSERT 
    INTO 
        dbo.Cat 
        (Name) 
    VALUES 
        (@p0);     select SCOPE_IDENTITY();     @p0 = 'Cat number 0' [Type: String (4000)]

2011-11-25 23:06:19,384 DEBUG [7] - (SQL) :
    INSERT 
    INTO 
        dbo.Cat 
        (Name) 
    VALUES 
        (@p0);     select SCOPE_IDENTITY();     @p0 = 'Cat number 1' [Type: String (4000)]

... и еще шесть таких записей. 

 
  Nhibernate после сохранения объекта в бд, присваивает объекту в памяти идентификатор. И чтобы знать значение этого идентификатора, дописывает к запросу select SCOPE_IDENTITY(); Таким образом Nhibernate выполняет сохранение одного объекта за одно обращение к БД, расходуя машинные ресурсы, как олигархи деньги в Куршавеле! А чем же лучше hilo? Снова взглянем на логи. Во-первых, видны обращения для получения High числа, которые регистрируются в таблице HighNumbers .


2011-11-25 23:20:17,734 DEBUG [7] - (SQL) : Reading high value:
    select 
        NextHigh 
    from 
        [dbo].HighNumbers 
    with (updlock,rowlock)

2011-11-25 23:20:17,799 DEBUG [7] - (SQL) : Updating high value:
    update 
        [dbo].HighNumbers 
    set 
        NextHigh = @p0 
    where 
        NextHigh = @p1;     @p0 = 2 [Type: Int32 (0)], @p1 = 1 [Type: Int32 (0)]

2011-11-25 23:20:17,871 DEBUG [7] - (SQL) : Reading high value:
    select 
        NextHigh 
    from 
        [dbo].HighNumbers 
    with (updlock, rowlock)
 

2011-11-25 23:20:17,874 DEBUG [7] - (SQL) : Updating high value:     update 
        [dbo].HighNumbers 
    set 
        NextHigh = @p0 
    where 
        NextHigh = @p1;     @p0 = 3 [Type: Int32 (0)], @p1 = 2 [Type: Int32 (0)]

Обращений могло быть и меньше, но я специально выбрал max_lo = 5, чтобы была видна работа hilo генератора. Так происходит вставка:

2011-11-25 23:20:17,929 DEBUG [7] - (SQL) : Batch commands:
command 0:
    INSERT
    INTO
        dbo.Cat
        (Name, Id)
    VALUES
        (@p0, @p1);
    @p0 = 'Cat number 0' [Type: String (4000)], @p1 = 5 [Type: Int32 (0)]
command 1:
    INSERT
    INTO
        dbo.Cat
        (Name, Id)
    VALUES
        (@p0, @p1);
    @p0 = 'Cat number 1' [Type: String (4000)], @p1 = 6 [Type: Int32 (0)]
... и еще шесть команд.


  Всё внимание  выделенным словах: NHibernate вставил все 8 записей в одном пакете! Это удалось сделать, потому что hilo генератор присваивает значения на клиенте.
  Присвоенные идентификаторы - 5,6.. и т.д. Как они получились? Дело в том, что nhibernate  высчитывает диапазон по формуле high * (max_lo + 1)..high * (max_lo + 1) + max_lo. В нашем случае, для первого зарезервированного числа high = 1, диапазон был 5..9, и для втого high = 2  10..14.
  И еще несколько слов о hilo. Зарезервированный диапазон чисел хранится на уровне ISessionFactory,  и доступ к нему потокобезопасен. Если приложение закрылось, не выбрав весь диапазон чисел - он пропадает. Но не стоит об этом беспокоится (если конечно вы не установили слишком большое max_lo). А если всё-таки предел близок - берите Int64. Их хватит на все разумные размеры приложений. Еще можно использовать фильтр where. Только помните, что на одну иерархию классов должен быть один фильтр (об этом ниже).
  Identity генератор не позволяет замапить иерархию классов через union-subclass, т.к. в этом случае нет никакой ясности, как искать базовый класс по идентификатору, если он есть в нескольких таблицах наследников. В случае hilo такой проблемы нет - идентификаторы уникальны.

  Надеюсь, мне удалось приоткрыть завесу тайны над hilo. Если у вас есть nhibernate - забудьте identity как страшный сон! Hilo намного эффективней, и даёт больше возможностей.

четверг, 17 ноября 2011 г.

Конфликт имён. И как с ним бороться.

Проблема - конфликт имён. 
 
  Однажды, в обычный рабочий день писал unit-тест для проверки работы Windsor-interceptor'ов с помощью Moq. Код выглядел примерно так:
var system = new Mock<ISystem>();
var windsor = new WindsorContainer();

windsor.Register(
  Component.For<ISystem>()
    .Instance(system.Object)
    .Interceptors(typeof (SomeInterceptor)));

var resolvedSystem = windsor.Resolve<ISystem>();

var proxy = (IProxyTargetAccessor) resolvedSystem;
  В ответ компилятор вполне определённо намекает:

 Error    1    The type 'Castle.DynamicProxy.IProxyTargetAccessor' exists in both 'd:\Projects\Test\packages\Castle.Core.3.0.0.2001\lib\net40-client\Castle.Core.dll' and 'd:\Projects\Test\packages\Moq.4.0.10827\lib\NET40\Moq.dll' 
  
  Дело в том, что в Moq интегрирована сборка Castle.DynamicProxy. Дописать namespace не поможет - имена совпадают полностью. Аналогичная проблема возникает и в случае Rhino.Mocks. Огорчительно, но к счастью, решение есть.


Решение - Alias. 

  Если посмотреть свойства любого reference в проекте, можно заметить одно волшебное свойство:

  По-умолчанию значение этого свойства global. Что оно означает? В некотором смысле, это самый глобальный namespace, с которого компилятор начинает поиск зависимостей. Т.е. такой код вполне компилируется и рабоает:
var windsor = new global::Castle.Windsor.WindsorContainer();
  Так вот, в свойствах reference можно присвоить зависимости другой alias.  Поменяем для Moq название на Moq.
  Теперь в коде мы можем ссылаться на этот псевдоним. Для этого служит ключевое слово extern alias. Для этого служит ключевое слово extern alias. Добавить его следует в самое начало файла:
extern alias Moq;
using Moq::Moq;
  Вторая строка выглядит несколько странно, но я объясню: это обычный using, но с указанием alias'a библиотеки. (другие using мы можем писать global::System.Text и т.д., но компилятор разрешает нам опустить global).
  Таким же образом можно использовать несколько версий одной библиотеки. Хотя и не рекомендовал бы этого делать. 

  Пожалуй, стоит упомянуть о самом главном - недостатках. Если для reference указан alias - то в каждом файле, где он используется придётся писать extern alias. Поэтому подумайте хорошенько, что какую именно библиотеку надо переименовать.

вторник, 25 октября 2011 г.

NHibernate vs Entity Framework 4.

  Недавно пришлось делать сравнение NHiberante и EF4. Стоит отметить, что мой личный опыт включает в себя только использование NHibernate, и остальные изыскание проводились исключительно с позиции стоит ли EF4 усилий по своему изучению. Вкратце преимущество каждого перечислены ниже.


Преимущества NHibernate
  1. Пакетное чтение (MulityQuery, Future)
  2. Пакетная запись, настраиваемая в конфиге. 
  3. Пакетная ленивая загрузка коллекций, сокращающая проблему N +1 
  4. Сверхленивая загрузка коллекций (Order.OrderLines.Count приводит к select count(*) )
  5. Возможность постраничной выборки и фильтрации коллекций. 
  6. Разнообразные способы выборки: HQL, ICriteria, QueryOver, LINQ, SQL(включая хранимые процедуры) и трансформации результатов с использованием IResultTransformer.
  7. Кеш второго уровня. 
  8. Большое количество точек расширения, открывающих широкие возможности по модификации поведения 
  9. Как следствие из предыдущего пункта, существует большое большое количество расширяющих фреймворков. Примеры:
    • NHibernate.Envers - аудит 
    • NHibernate.Validator - думаю понятно :)
    • NHiberante.Search - Полнотекстовый поиск с использованием Lucene.Net
    • NHibernate.Shards - горизонтальное масштабирование приложений. 
    • Rhino.Security - библиотека для разграничения прав доступа. 
  10. Разнообразные способы маппинга объектов на бд:
    • xml
    • аттрибуты
    • маппинг кодом (2 отдельных фреймворка + встроенная поддержка)
      • по классам
      • по соглашениям.
  11. Более мощный язык объектных запросов (HQL vs Entity SQL), позволяющий выполнять объектные DML-команды без загрузки объектов в память. 
  12. Встроенная поддержка для логгирования генерируемых sql-команд. 
  13. Более 10 разнообразных id-генераторов, включая наиболее эффективные для ORM HiLo и guid.comb.
  14. Возможность маппинга разнообразных пользовательских типов (шифрованные строки, локализованные свойства, запись Enum как строки, и многое другое)
  15. Поддержка readonly свойств. 
  16. Поддеркжа коллекций элементов (например IList<int>)
  17. Поддержка маппинга словарей (Dictionary).
  18. Сильное сообщество программистов. 



Преимущества EF
  1. Лучшая поддержка LINQ.
  2. Свойства объектов не обязательно должны быть виртуальными.  
  3. Microsoft.
  4. Наличие визуального редактора от Microsoft.
Мой выбор очевиден, NHibernate. Остальное решать вам. 

четверг, 13 октября 2011 г.

Оседлать AssemblyVersion.

   Наверно перед каждым разработчиком встаёт задача управлением версиями сборок. На заре своей карьеры я делал это самым простым способом - переписывал руками. Потом этот процесс был автоматизирован с nant'ом: он сканировал AssemblyInfo.cs по всему решению, и переписывал версию. Потом через подключение через ссылку общего для всех проектов файла SolutionVersion.cs.
  Но в этом решении по прежнему оставался один минус: версия сборки имеет значение только в момент компиляции проекта, и ее нахождение в системе контроля версий бессмысленно. Но если не класть SolutionVersion.cs в контроль версий, то в новой рабочей копии сначала придётся запустить сборку приложения, чтобы его сгенерировать. Иначе студия будет сообщать нам об ошибке. К счастью, решение есть. И оно не такое уж сложное. Давайте по шагам.
  1. Создаём в корневой папке решения пустой файл SolutionVersion.cs
  2. Подключаем его во все проекты как ссылку.

  3. Добавляем его в список файлов, которые не будут отслеживаться системой контроля версий (файлы .gitignore; .hgignore).
  4. Откройте любым редактором файл проекта .csproj, и ищите в самом конце такие строки:
     
      <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
           Other similar extension points exist, see Microsoft.Common.targets.
      <Target Name="BeforeBuild">
      </Target>
      <Target Name="AfterBuild">
      </Target>
      -->
    
    Если есть - удаляем комментарии, и пишем свою задачу. Если их нет, то просто допишите pre-build задачу перед закрывающим тегом </Project>
     
      <Target Name="BeforeBuild">
        <Exec Command="echo //DO NOT EDIT &gt; $(SolutionDir)\SolutionVersion.cs"  condition="!Exists('$(SolutionDir)\SolutionVersion.cs')">
        </Exec>
      </Target>
    
  5. И настроим в своём любимом build-tool генерацию этого же файла.
  Собственно, идеальный результат достигнут: версия сборки не хранится в контроле версий, и нам не надо предварительно запускать build, чтобы visual studio не выдавала ошибок.

суббота, 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!

воскресенье, 28 августа 2011 г.

Настройка сборки проекта за 5 минут - UppercuT.

  Кому приходилось настраивать сборку проекта с помощью msbuild или nant понимает нудность этой задачи. А кроме того, для особо сложных сборок этот путь громоздок и тернист. Если кто не верит, может ознакомится с огромными build-файлами NHibernate тут и тут.
  И вот, настал очередной раз возникла нужда настраивать сборку проекта... Писать очередную xml портянку не хотелось, и я полез в гугл. На самом деле, инструментов для сборки оказалось довольно много: MsBuild, nant, NuBuild, Psake, FAKE (F# MAKE)Rake, UppercuT.
  С msbuild, nant и nubuild были отвергнуты сразу: старая добрая xml-конфигурация не казалась мне такой уж доброй, и разные ее вариации не делают ее добрее.
  Psake - сборка проекта на основана на powershell скриптах. Уже лучше, но чтобы выполнять скрипты пришлось на каждом компьютере настраивать разрешения для их выполнения. Не сложно, но не хотелось бы.
  Fake & Rake - конфигурация сборки на языке F#. Серьёзно к ним присматривался, но видимо потребуют некоторого времени на изучение.
  В итоге остановился на UppercuT - автор обещал возможность настроить сборку проекта за пять минут. И не обманул. Настроить сборку оказалось в разы быстрее, чем написать этот пост :)
Итак, пять простых шагов, как настроить сборку с помощью:
  1. Uppercut формирует версии сборок  проекта используя файл SolutionVersion.cs. Вот этот файл нам и надо созадать в корневой папке Solution'а. И далее подключить этот файл как ссылку(!) ко всем проектам. 
  2. Далее идём на страницу UppercuT, и качаем архив с последней версией (или не последней, кому как нравится). Находим в архиве папку Uppercut, и копируем всё ее содержимое в корневую папку solution'a. 
  3. Теперь всё-таки придётся заняться xml. Среди скопированных папок найти папку settings,  а в ней открываем файл Uppercut.config. Заполняем четыре свойства, которые понадобится uppercut'у чтобы сгенерировать SolutionVersion.cs:
    <property name="project.name" value="__SOLUTION_NAME_WITHOUT_SLN_EXTENSION__" overwrite="false" />
      <property name="path_to_solution" value="." overwrite="false" />
      <property name="repository.path" value="__REPOSITORY_PATH__" overwrite="false" />
      <property name="company.name" value="__COMPANY_NAME__" overwrite="false" />
    Всё достаточно очевидно. Отмечу только, что сейчас поддерживаются git и svn репозитории. Соответствующий url и надо вписать в конфигурацию. Поскольку я собирался билдить проект прямо из папки решения - поставил в repository.path точку. И это сработало. :)
  4. Прописываем в переменную среды PATH путь к утилите командной строки контроля версий. В моем случае git - C:\Program Files\Git\bin
  5. Запускаем build.bat из корневой папки решения. Наслаждаемся результатом.
  Мне понравилось. Почти не приложив усилий - получаем настроенную сборку проекта. Не только комплияцию, но и прогон тестов, и проверку покрытия с помощью NCover. Не уверен, что для больших и сложных проектов такой способ подойдет, но для простых очень даже хорош.
  Немного изучив содержимое папки lib, удалил оттуда лишнее: xUnit, MbUnit, Eazfuscator и MoMA. Чуть позже попробую настроить создание nuget-пакета с помощью UppercuT. А дальше в планах изучить fake и rake.
PS. Проект о котором идёт речь всё тот же - EventPublisher.

четверг, 25 августа 2011 г.

Реализуем EventPublisher. Шаг четвёртый. Переходим в замок Windsor.

  Изначально EventPublisher прездназначался для отправки сообщений экземплярам классов, которые уже существуют, и явным образом подписаны. Но часто полезно, когда получатель сообщения не живёт постоянно, а создаваётся во время его отправки, и время жизни ограничено временем обработки сообщения. Такой вот одноразовый обработчик: создался, обработал, испарился. Такую вещь можно реализовать как рефлексивный IListenerSource. Но этот способ не лишён недостатков, главный из которых - сложности с управлением зависимостями.

  К счастью, в этом мире проблема управления зависимостями уже решена с помощью DI контейнеров.  Осталось только выбрать контейнер, и написать основанный на контейнере IListenerSource. Не буду скрывать, мой любимый контейнер - Castle.Windsor. Его и буду использовать.

  Перед написанием собственно IListenerSource, стоит подумать об еще одной вещи: как регистрировать наших подписчиков в контейнере? Windsor широко использует принцип convention over configuration, и имеет множество встроенных средств для регистрации компонентов по соглашением. Значит надо такое соглашение придумать. Вот оно:
public interface ITransientListener<TMessage> : IListener<TMessage>
{
} 
  Как можно догадаться из названия - регистрировать подписчиков будем с Transient Lifestyle, чтобы обеспечить желаемый стиль работы: создался, обработал, испарился.

  Следующий логичный шаг - это регистрация обработчиков событий в контейнере. Конечно, в каждом проекте это придётся делать по-новому, но было бы неплохо написать помощника в этом нудном деле. Я не буду приодить его здесь, дабы не загромождать блог кодом, но можно посмотреть на github

  Теперь у нас есть всё для создания IListenersSource: соглашение о подписчиках и инструмент их регистрации. С Windsor'ом это совсем не сложно:
public class TransientSource : IListenerSource
{
	private readonly IKernel _kernel;

	public TransientSource(IKernel kernel)
	{
		_kernel = kernel;
	}

	public virtual IEnumerable<IListener<TMessage>> ResolveListenersFor<TMessage>()
	{
		return _kernel.ResolveAll<ITransientListener<TMessage>>();
	}
}
  Теперь мы можем сконфигурировать EventPublisher, чтобы он перенаправлял сообщения одноразовым обработчикам из контейнера. Но как же быть со старыми добрыми долгоживущими экземплярами подписчиков? Ответ прост: нужен источник подпичиков, который бы аггрегировал подписчиков из разных источников - CompositeListenerSourсe.

  Остался последний штрих. Windsor предлагает еще одну концепцию для упрощения рутинных действий при регистрации компонентов: Facility. Почему бы не сделать EventPublisherFacility?
Основые действия, которые на неё возлагаюстся:
  1. Конфигурирует EventPublisher используя IPublishWay зарегистрированный в контейнере или переданный в конструкторе, и CompositeListenerSource, в который добавлены TransientListnerSource, и все зарегистрировные в контейнере IListenerSource.
  2. Регистрирует в контейнере IAssignee, для возможности добавлять подписчиков. 
  Реализация это не так уж сложно. Теперь у нас есть зарегистированный в контейнере EventPublisher  с возможностью подписывать/отписывать и вытаскивать из контейнера одноразовые обработчики.

  Надеюсь, это материал будет кому-нибудь полезен. Код проекта на github.

вторник, 23 августа 2011 г.

Реализуем EventPublisher. Шаг третий. Слово о тестировании.

 Еще одна важная часть процесса разработки - модульное тестирование. Всё описанное ниже можно заменить на использование мок-объектов. Но лично меня часто раздражает их многословный и не всегда легко читаемый лямбда-синтаксис. Собственно, как помочь в деле деле тестирования? Мне видятся две вещи:
  1. Реализация IPublisher в простом и надёжном варианте. В первую очередь - простое подписывание слушателей,  и однопоточность. 
  2. Удобный класс-помощник для Assert выражений.

Для первого пункта можно сделать простой наследник от Publisher, заранее инициализировав все свойства. Например так:

public class TestPublisher : Publisher
{
public IAssignee Assignee 
{ 
  get { return (IAssignee) ListenerSource; } 
}

public TestPublisher() : base(new SimpleAssignee(), new SimplePublishWay())
{
}
}

С тестовым Publisher`ом всё. Теперь перейдём к assert-помощнику. Какие функции он должен нести? Неким образом фиксировать количество вызовов, и полученные сообщения. И еще лучше - лёгко читаемый синтаксис. И вот что в итоге у меня получилось:
public class TestListener<TMessage> : IListener<TMessage>
{
  private readonly List<TMessage> _messages;

  public TestListener()
  {
    _messages = new List<TMessage>();
  }

  public int WasCalled { get; private set; }

  public bool WasCalledAtLeastOnce
  {
    get { return WasCalled > 0; }
  }

  public TMessage LastMessage
  {
    get { return _messages.Last(); }
  }

  public IList<TMessage> Messages
  {
    get { return _messages.AsReadOnly(); }
  }

  public bool WasCalledOnce
  {
    get { return WasCalled == 1; }
  }

  public void ListenTo(TMessage message)
  {
    WasCalled++;
    _messages.Add(message);
  }
}
И как бы это выглядело в тесте:

[Test]
public void SomeTest()
{
   Assert.That(listener.WasCalledOnce);
}
Выглядит достаточно читаемо. По крайней мере для меня :)

суббота, 20 августа 2011 г.

Реализуем EventPublisher. Шаг второй.

В прошлой части я наметил основу будущего диспетчера событий. Пора заняться реализацией.
Для начала определим стоящие перед нами проблемы. К счастью их всего две:
  1. Получение списка подписчиков сообщений. 
  2. Отправка сообщений подписчикам. 
Решение обоих задач крайне желательно сделать легко модифицируемым и расширяемым. Поэтому их реализация начинается... с написания новых интерфейсов. Для каждой из задач оответсвенно:

public interface IListenerSource
{
  IEnumerable<IListener<TMessage>> ResolveListenersFor<TMessage>();
}

public interface IPublishWay
{
  void Publish<TMessage>(TMessage message, IListener<TMessage> listener);
}
Эти интерфейсы открывают путь к простейшей реализации диспетчера событий:
public class Publisher : IPublisher
{
  protected readonly IListenerSource ListenerSource;
  protected readonly IPublishWay PublishWay;

  public Publisher(IListenerSource listenerSource, IPublishWay publishWay)
  {
    if(listenerSource == null)
      throw new ArgumentNullException("listenerSource");
    if(publishWay == null)
      throw new ArgumentNullException("publishWay");

    ListenerSource = listenerSource;
    PublishWay = publishWay;
  }

  public virtual void Publish<TMessage>(TMessage message)
  {
    foreach (var listener in ListenerSource.ResolveListenersFor<TMessage>())
    {
      PublishWay.Publish(message, listener);
    }
  }
}

Теперь можно перейти к реализации IPublishWay. В простейшем случае можно сделать так:
public class SimplePublishWay : IPublishWay
{
  public virtual void Publish<TMessage>(TMessage message, IListener<TMessage> listener)
  {
    listener.ListenTo(message);
  }
}
Для работы в реальных условиях такая реализация вряд ли годится. Но пригодится для тестировочных конфигураций Publisher'a. Но об этом в следующей части. А сейчас посмотрим на более полезную реализацию, с использованием SynchronizationContext. От просто SynchronizationContext пользы немного, но при использовании его наследника WindowsFormsSynchronizationContext мы получим маршаллинг событий в поток пользовательского интерфейса. Подробнее об это можно почитать например здесь. Опять же, простейшая реализация:
public class SynchronizationContextPublishWay : IPublishWay
{
  private readonly SynchronizationContext _synchronizationContext;

  public SynchronizationContextPublishWay(SynchronizationContext synchronizationContext)
  {
    _synchronizationContext = synchronizationContext;
  }

  public void Publish<TMessage>(TMessage message, IListener<TMessage> listener)
  {
    _synchronizationContext.Post(x => listener.ListenTo(message), null);
  }
}
Метод Post позволит реализовать асинхронную передачу сообщений, освободив вызывающий поток для другой работы.
Остался последний штрих - реализовать IListenerSource. Для первой реализации можно взять обыкновенный список. Но лучше воспользоваться новой коллекцией из .net 4 - ISet. Она не позволяет дважды добавить один и тот же объект, что избавит от необходимости предварительной проверки.
public class SimpleAssignee : IAssignee
{
  private readonly ISet<object> _listeners;
  private readonly object _latch;

  public SimpleAssignee()
  {
    _listeners = new HashSet<object>();
    _latch = new object();
  }

  public virtual void Subscribe(object listener)
  {
    lock(_latch)
    {
      _listeners.Add(listener);
    }
  }

  public virtual void Unsubscribe(object listener)
  {
    lock(_latch)
    {
      _listeners.Remove(listener);
    )
  }

  public virtual IEnumerable<IListener<TMessage>> ResolveListenersFor<TMessage>()
  {
    lock(_latch)
    {
      return _listeners.OfType<IListener<TMessage>>().ToList();
    }
  }
}

На этом простая реализация диспетчера сообщений завершена, можно конфигурировать и использовать. Но это еще не все... В следующей части рассмотрим вопросы тестирования.
Код проекта по-прежнему можно найти на github

вторник, 16 августа 2011 г.

Реализуем EventPublisher. Шаг первый.

 Шаблон EventAggregator (он же EventPublisher, он же EventBroker) известен давно, и описан Фаулером еще в 2004 году. Обобщённую реализацию для .NET можно найти в серии Build your own CAB  под пунктом 13а.  Основная идея проста: за счёт единообразной диспетчеризации сообщений упрощается код клиентского класса, уменьшается число его зависимостей.
  В интернете можно найти много и других реализаций. Так собственно, зачем городить еще одну? Ответ прост: хочется получить более мощные возможности по модификации поведения, и естественно, получить полный контроль над проектом (как первый этап плана по захвату мира).

public interface IPublisher
{
    void Publish<TMessage>(TMessage "message"); 
}

 Обычно IPublisher включает методы подписки/отписки получателей сообщений. Но это неправильно: многим классам надо только отправлять сообщение. Поэтому вооружимся ISP, и выделим подписку в отдельный интерфейс:
public interface IAssignee : IListenerSource
{
    void Subscribe(object listener);

    void Unsubscribe(object listener);
}

И конечно же, осталось ввести интерфейс для подписчиков:
public interface IListener<TMessage>
{
    void ListenTo(TMessage message);
}
Обобщённая версия интерфейса подписчика даёт большое преимущество: статическую типизацию получателей сообщений по типу сообщения.
Скелет будущего диспетчера событий готов. В следующих частях серии займемся его реализацией.
Код проекта на есть Github.

понедельник, 14 февраля 2011 г.

Как разделить репозиторий Subversion

Иногда необходимо разделить хранилище Subversion, и перенести его на другой сервер. Задача решаема, хотя и с некоторыми трудностями. Решить ее удалось на основании этой статьи.
Но как всегда, возникли нюансы.
Но давайте по порядку. Пускай у нас был проект по адресу http://server/svn/Root/Projects/XYProject, а физически хранилище располагалось на F:\Repositories\Root. Задача был заключалась в том, чтобы вырезать проект XYProject со всей историей, и перенести в новое хранилище.
1. Создать дамп хранилища:   

   svnadmin dump F:\Repositories\Root > fullrepo.dump 
Команда отработала без ошибок, через пару минут дамп был готов для дальнейших опытов.


2.Отфильтровать дамп

Дальше нужно отфильтровать дамп по нужному нам пути, что делается командой svndumpfilter. Команда замечательно работала, но отфильтровывала вообще всё. Поковырявшись в дамп (он в простом текстовом формате), удалось написать правильную строку для фильтра.

   svndumpfilter include /Projects/XYProject/trunk < f:\fullrepo.dump > f:\XYProject.dump

Итак, есть готовый отфильтрованный дамп. Дело за малым, выполнить 3-ю рекомендацию статьи: исправить пути, и удалить запись о создании корневого каталога.

3. Исправить пути, и удалить запись о создании корневой папки

Не буду описывать свои попытки заставить работать sed на Windows, но у меня это так и не получилось. От 100Мб файла он оставлял 10-15Кб. В итоге сделал тоже самое через старый добрый notepad++.
Итак, что надо сделать: 

найти все строки пути вида

Node-path: Projects/XYProject/trunk/XYProject.Presentation
Node-copyfrom-path: Projects/XYProject/trunk/XYProject.Presentation/Class1.cs

заменить соответственно на:

Node-path: trunk/XYProject.Presentation
Node-copyfrom-path: trunk/XYProject.Presentation/Class1.cs

а также найти строки вида (создание папки для вашего проекта), и удалить их.

Node-path: Projects/XYProject/trunk
Node-action: add
Node-kind: dir
Prop-content-length: 10
Content-length: 10

PROPS-END

4. Импорт исправленного дампа на сервер. 

svnadmin load D:\Repositories\XYProject < f:\XYProject.dump


Если вы всё правильно сделали на предыдущих шагах, то проблем возникнуть не должно, и вся история ваших изменений ляжет на новый сервер.

среда, 9 февраля 2011 г.

Как посчитать количество строк кода.

Иногда для себя был интересно узнать количество строк кода в проекте. Но не обладая дорогими редакциями Visual Studio это сложно сделать в одну кнопку. Но недавно нагуглил простой способ посчитать количество строк кода в проекте использая встроенный поиск и регулярные выражения. Подробнее в блоге по ссылке.