避免并行继承层次结构

时间:2009-03-30 07:59:22

标签: java oop inheritance anti-patterns

我有两个并行的继承链:

Vehicle <- Car
        <- Truck <- etc.

VehicleXMLFormatter <- CarXMLFormatter
                    <- TruckXMLFormatter <- etc.

我的经验是,并行继承层次结构随着它们的增长会成为一种维护问题。

即。不要将toXML(), toSoap(), toYAML()方法添加到我的主要类中。

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

6 个答案:

答案 0 :(得分:12)

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

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-classes分开。 当然,当你创建一个新的车辆时,你将不得不向Formatter接口添加另一个方法(并在格式化程序界面的所有实现中实现这个新方法)。
但是,我认为这比创建一个新的Vehicle类更好,并且对于你拥有的每个IVehicleFormatter,创建一个可以处理这种新型车辆的新类。

答案 1 :(得分:8)

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

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

答案 2 :(得分:2)

您可以尝试避免格式化程序的继承。只需制作一个可以处理VehicleXmlFormatter s,Car s的Truck,......通过切断方法之间的责任并通过确定一个好的调度,可以很容易地实现重用 - 战略。避免超载魔法;在格式化程序的命名方法中尽可能具体(例如formatTruck(Truck ...)而不是format(Truck ...))。

如果您需要双重调度,则仅使用“访问者”:当您拥有类型为Vehicle的对象并且希望在不知道实际具体类型的情况下将其格式化为XML。访问者本身并不能解决在格式化程序中实现重用的基本问题,并且可能会引入您可能不需要的额外复杂性。上述通过方法重复使用的规则(切断和发送)也适用于您的访客实施。

答案 3 :(得分:1)

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

答案 4 :(得分:1)

您可以使用Bridge_pattern

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

enter image description here

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

实施永远不会引用 抽象 。抽象包含 实施 接口作为成员(通过合成)。

回到你的榜样:

Vehicle 抽象

CarTruck RefinedAbstraction

Formatter 执行者

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

相关帖子:

When do you use the Bridge Pattern? How is it different from Adapter pattern?

答案 5 :(得分: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
}