避免并行继承层次结构
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
需要Accept
和IVehicleFormatter
并称之为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】:您可以尝试避免对格式化程序进行继承。只需创建一个可以处理Car
s、Truck
s、...的VehicleXmlFormatter
,通过分割方法之间的职责和找出一个好的调度策略,重用应该很容易实现。避免超载魔法;在格式化程序中尽可能具体地命名方法(例如 formatTruck(Truck ...)
而不是 format(Truck ...)
)。
仅当您需要双重分派时才使用访问者:当您有 Vehicle
类型的对象并且您想将它们格式化为 XML 而不知道实际的具体类型时。访问者本身并不能解决在格式化程序中实现重用的基本问题,并且可能会引入您可能不需要的额外复杂性。上面的方法重用规则(切分和分派)也适用于您的访问者实现。
【讨论】:
【参考方案4】:您可以使用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();
相关帖子:
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
【讨论】:
以上是关于避免并行继承层次结构的主要内容,如果未能解决你的问题,请参考以下文章
先发制人:如何避免 Node 中的深层回调层次结构? (使用快递)[重复]