Страницы

суббота, 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) { }
}
  Как видите, очень похоже. Но в отличии от первого варианта, мы можем добавлять любое количество новых обработчиков. Надеюсь, этот пример поможет многим избежать ошибок, на которые так соблазняют обобщения. :)

Комментариев нет:

Отправить комментарий