35

我有两个并行的继承链:

Vehicle <- Car
        <- Truck <- etc.

VehicleXMLFormatter <- CarXMLFormatter
                    <- TruckXMLFormatter <- etc.

我的经验是,随着并行继承层次结构的增长,它们可能会成为维护方面的难题。

即不向toXML(), toSoap(), toYAML()我的主要课程添加方法。

如何在不破坏关注点分离概念的情况下避免并行继承层次结构?

4

6 回答 6

13

我正在考虑使用访客模式。

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

这样,您就可以避免额外的继承树,并将格式化逻辑与您的车辆类分开。当然,当您创建新车辆时,您必须向 Formatter 接口添加另一个方法(并在 formatter 接口的所有实现中实现这个新方法)。
但是,我认为这比创建一个新的 Vehicle 类更好,并且对于您拥有的每个 IVehicleFormatter,创建一个可以处理这种新型车辆的新类。

于 2009-03-30T08:14:13.827 回答
9

另一种方法是采用推模型而不是拉模型。通常你需要不同的格式化程序,因为你打破了封装,并且有类似的东西:

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。它甚至可以在内部委托给不同的接收器策略。但是 sink 不一定需要关心车辆的类型,只关心如何以某种格式表示数据。使用内部全局 ID 而不是内联字符串有助于降低计算成本(仅在您编写 ASN.1 或其他紧凑格式时才重要)。

于 2009-03-30T09:28:08.377 回答
2

您可以尝试避免对格式化程序进行继承。只需制作一个VehicleXmlFormatter可以处理Cars、Trucks、... 的方法,通过分割方法之间的职责和找出一个好的调度策略,重用应该很容易实现。避免超载魔法;在格式化程序中尽可能具体地命名方法(例如formatTruck(Truck ...),而不是format(Truck ...))。

只有在需要双重分派时才使用访问者:当您有类型的对象Vehicle并且想要将它们格式化为 XML 而不知道实际的具体类型时。访问者本身并不能解决在格式化程序中实现重用的基本问题,并且可能会引入您可能不需要的额外复杂性。上面的方法重用规则(切碎和分派)也适用于您的访问者实现。

于 2009-03-30T08:33:53.310 回答
2

您可以使用Bridge_pattern

桥接模式将抽象与其实现分离,以便两者可以独立变化

在此处输入图像描述

两个正交的类层次结构(抽象层次结构和实现层次结构)使用组合(而不是继承)链接。这种组合有助于两个层次结构独立变化。

实现从不引用Abstraction。抽象包含实现接口作为成员(通过组合)。

回到你的例子:

Vehicle抽象的

Car并且TruckRefinedAbstraction

Formatter实施者

XMLFormatter,POJOFormatterConcreteImplementor

伪代码:

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

相关帖子:

什么时候使用桥接模式?它与适配器模式有何不同?

于 2016-09-18T19:08:08.977 回答
1

为什么不让 IXMLFormatter 成为与 toXML()、toSoap() 和 YAML() 方法的接口,并让 Vehicle、Car 和 Truck 都实现它?这种方法有什么问题?

于 2009-03-30T08:13:27.493 回答
0

我想将泛型添加到 Frederiks 的答案中。

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
}
于 2016-09-18T19:28:22.030 回答