Подтвердить что ты не робот

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

Я имею две параллельные цепи наследования:

Vehicle <- Car
        <- Truck <- etc.

VehicleXMLFormatter <- CarXMLFormatter
                    <- TruckXMLFormatter <- etc.

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

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

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

4b9b3361

Ответ 1

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

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
{
}

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

Ответ 2

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

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();
...

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

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

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 или других жестких форматов).

Ответ 3

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

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

Ответ 4

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

Ответ 5

Вы можете использовать Bridge_pattern

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

введите описание изображения здесь

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

В реализации никогда не упоминается Абстракция. Абстракция содержит интерфейс Реализация как член (через композицию).

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

Vehicle Абстракция

Car и Truck RefinedAbstraction

Formatter Исполнитель

XMLFormatter, POJOFormatter 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();

связанная почта:

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

Ответ 6

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

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
}