Избежание параллельных иерархий наследования

у меня есть две параллельные цепочки наследования:

Vehicle <- Car
        <- Truck <- etc.

VehicleXMLFormatter <- CarXMLFormatter
                    <- TruckXMLFormatter <- etc.

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

не добавлять toXML(), toSoap(), toYAML() методы для моих основных классов.

Как избежать параллельной иерархии наследования, не нарушая концепции разделения?

6 ответов


Я думаю об использовании шаблона посетителя.

public class Car : Vehicle
{
   public void Accept( IVehicleFormatter v )
   {
       v.Visit (this);
   }
}

public class Truck : Vehicle
{
   public void Accept( IVehicleFormatter v )
   {
       v.Visit (this);
   }
}

public interface IVehicleFormatter
{
   public void Visit( Car c );
   public void Visit( Truck t );
}

public class VehicleXmlFormatter : IVehicleFormatter
{
}

public class VehicleSoapFormatter : IVehicleFormatter
{
}

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


другой подход состоит в том, чтобы принять модель, а не модель. Обычно вам нужны разные форматеры, потому что вы нарушаете инкапсуляцию и имеете что-то вроде:

class TruckXMLFormatter implements VehicleXMLFormatter {
   public void format (XMLStream xml, Vehicle vehicle) {
      Truck truck = (Truck)vehicle;

      xml.beginElement("truck", NS).
          attribute("name", truck.getName()).
          attribute("cost", truck.getCost()).
          endElement();
...

где вы вытягиваете данные из определенного типа в форматер.

вместо этого создайте приемник данных format-agnostic и инвертируйте поток, чтобы конкретный тип выталкивал данные в приемник

class Truck  implements Vehicle  {
   public DataSink inspect ( DataSink out ) {
      if ( out.begin("truck", this) ) {
          // begin returns boolean to let the sink ignore this object
          // allowing for cyclic graphs.
          out.property("name", name).
              property("cost", cost).
              end(this);
      }

      return out;
   }
...

это означает, что у вас все еще есть инкапсулированные данные, и вы просто подача помеченных данных в раковину. Затем приемник XML может игнорировать определенные части данных, возможно, переупорядочить некоторые из них и записать XML. Он может даже делегировать различные стратегии поглощения внутри страны. Но раковина не обязательно должна заботиться о типе транспортного средства, только о том, как представлять данные в некотором формате. Использование интернированных глобальных идентификаторов, а не встроенных строк помогает снизить стоимость вычислений (имеет значение, только если вы пишете ASN.1 или другие сжатые форматы).


вы можете попытаться избежать наследования для ваших форматтеров. Просто сделайте VehicleXmlFormatter Это может иметь дело с Cars,Trucks,... Повторное использование должно быть легко достигнуто путем разделения обязанностей между методами и путем определения хорошей стратегии отправки. Избегайте перегрузки магии; будьте как можно более конкретными в именовании методов в вашем форматере (например,formatTruck(Truck ...) вместо format(Truck ...)).

используйте только посетителя, если вам нужна двойная отправка: когда у вас есть объекты типа Vehicle и вы хотите отформатировать их в XML, не зная фактического конкретного типа. Сам посетитель не решает базовую проблему достижения повторного использования в вашем форматере и может ввести дополнительную сложность, которая вам может не понадобиться. Правила выше для повторного использования методами (измельчение и отправка) будут применяться и к вашей реализации посетителя.


Почему бы не сделать IXMLFormatter интерфейсом с методами toXML(), toSoap(), YAML() и не сделать автомобиль, автомобиль и грузовик все это реализовать? Что плохого в таком подходе?


можно использовать Bridge_pattern

шаблон моста отделяет абстракцию от ее реализации, чтобы они могли варьироваться независимо.

enter image description here

две ортогональные иерархии классов (абстрагирование иерархии и реализация иерархия) связаны с использованием композиции (а не наследство).Этот состав помогает иерархии варьировать самостоятельно.

реализация никогда не ссылается абстрагирование. Абстракция содержит реализация интерфейс как член (через состав).

возвращаясь к вашему примеру:

Vehicle is абстрагирование

Car и Truck are RefinedAbstraction

Formatter is конструктор

XMLFormatter, POJOFormatter are ConcreteImplementor

псевдо код:

 Formatter formatter  = new XMLFormatter();
 Vehicle vehicle = new Car(formatter);
 vehicle.applyFormat();

 formatter  = new XMLFormatter();
 vehicle = new Truck(formatter);
 vehicle.applyFormat();

 formatter  = new POJOFormatter();
 vehicle = new Truck(formatter);
 vehicle.applyFormat();

похожие темы:

когда вы используете шаблон мост? Чем он отличается от шаблона адаптера?


Я хочу добавить дженерики к ответу Фредерикса.

public class Car extends Vehicle
{
   public void Accept( VehicleFormatter v )
   {
       v.Visit (this);
   }
}

public class Truck extends Vehicle
{
   public void Accept( VehicleFormatter v )
   {
       v.Visit (this);
   }
}

public interface VehicleFormatter<T extends Vehicle>
{
   public void Visit( T v );
}

public class CarXmlFormatter implements VehicleFormatter<Car>
{
    //TODO: implementation
}

public class TruckXmlFormatter implements VehicleFormatter<Truck>
{
    //TODO: implementation
}