避免并行继承层次结构

Posted

技术标签:

【中文标题】避免并行继承层次结构【英文标题】:Avoiding parallel inheritance hierarchies 【发布时间】:2010-10-16 07:56:33 【问题描述】:

我有两条平行的继承链:

Vehicle <- Car
        <- Truck <- etc.

VehicleXMLFormatter <- CarXMLFormatter
                    <- TruckXMLFormatter <- etc.

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

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

如何在不破坏关注点分离概念的情况下?

【问题讨论】:

【参考方案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


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

【讨论】:

最好将 IVehicleFormatterVisitor 重命名为 IVehicleVisitor,因为它是一种比格式化更通用的机制。 这不是抢彼得付钱给保罗吗?所以现在你需要 IVehicleFormatter 中的一个方法以及 Vehicle 的每个子类的所有实现......那么这实现了什么? @Jordie 这可以使用 AbstractVehicleVisitor 解决,它将实现接口 IVehicleVisitor。所有方法都可以实现抛出不受支持的操作异常。具体的访问者将扩展抽象类并覆盖他们案例中使用的所需方法 但这只能解决编译器错误。它实际上并没有修复逻辑,并且可能会被认为更糟,因为现在编译器不会告诉您您忘记实现某些东西。 我想知道为什么Vehicle 需要AcceptIVehicleFormatter 并称之为Visit 方法而不是在外部创建IVehicleFormatter 并称之为Visit(vehicle) 方法直接?【参考方案2】:

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

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 或其他紧凑格式时才重要)。

【讨论】:

这个设计模式叫什么名字?管道模式? @haoli 我不知道,我称之为“与格式无关的数据接收器”【参考方案3】:

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

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

【讨论】:

【参考方案4】:

您可以使用Bridge_pattern

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

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

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

回到你的例子:

Vehicle抽象

CarTruckRefinedAbstraction

Formatter实施者

XMLFormatterPOJOFormatterConcreteImplementor

伪代码:

 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】:

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

【讨论】:

有时什么都没有。有时您不希望您的车辆类必须了解 XML/SOAP/YAML - 您希望它专注于对车辆进行建模,并保持标记表示分离。 它打破了一个类应该有单一职责的观念。 一切都与凝聚力有关。实体的多个方面或特征通常会将其设计拉向不同的方向,例如“一个事实在一个地方”与“单一责任原则”。作为设计师,你的工作是通过最大化凝聚力来决定哪个方面“获胜”。有时,toXML()、toSOAP() 等会有意义,但在更大的多层系统中,最好将表示逻辑与模型分开,即以特定格式(XML、SOAP、等)作为一个层次跨实体更具凝聚力,而不是与每个实体的具体细节联系在一起。【参考方案6】:

我想将泛型添加到 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

【讨论】:

以上是关于避免并行继承层次结构的主要内容,如果未能解决你的问题,请参考以下文章

使用Composition时如何避免子类回调?

访问类树层次结构中的对象时避免使用空指针

先发制人:如何避免 Node 中的深层回调层次结构? (使用快递)[重复]

SQL Server 基于集合的 while 循环避免万圣节保护 (LAHP) 用于层次结构透视将某些结果减半

桥接模式

软件工艺的话题