面向对象设计中的父子关系

7
这是一个关于创建对象之间父子关系的最佳做法问题。
假设我有车轮(Wheel)和汽车(Car)对象,并且我想要将一个车轮对象添加到汽车对象中。
public class Car{

    private List<Wheel> wheels = new List<Wheel>();

    void AddWheel ( Wheel WheelToAdd)
        {
            wheels.Add(WheelToAdd)
            //Some Other logic relating to adding wheels here
        }
    }
}

到目前为止还不错。但是如果我想在我的轮子上有一个车辆属性,以表示它所属的父级汽车,像这样:
 public class Wheel {

     private Car parentCar;
     public Car 
     {
        get
        {
         return parentCar
        }

  }

}

当将车轮添加到汽车上时,您在何时设置Wheel的父属性?您可以在Car.AddWheel方法中设置它,但是然后Wheel对象的Car属性必须是可读/可写的,然后您可以在AddWheel方法之外设置它,从而创建不一致性。
有什么想法,提前感谢。
8个回答

8
更好的设计方法(领域驱动设计)指定您应该首先确定这些实体的域模型要求是什么...并非所有实体都需要独立访问,如果Wheel属于此类别,则其每个实例始终将成为Car对象的子成员,您无需在其上放置父属性... Car成为所谓的根实体,访问Wheel的唯一方式是通过Car对象。

即使Wheel对象需要独立访问,域模型要求也应告诉您使用模式需要什么。任何Wheel是否会作为单独的对象传递,而没有其父对象?在这些情况下,Car父级是否相关?如果父Car的身份对某些功能很重要,为什么不直接将完整的组合Car对象传递给该方法或模块?包含的组合对象(如Wheel)必须单独传递,但需要父对象(它所属的对象)的身份和/或相关性的情况实际上并不常见,使用上述类型的分析方法来设计可以避免向系统中添加不必要的代码。


在我的具体场景中,我有乘客和队列,因此可以独立存在一个乘客。也许我应该用具体的例子-抱歉。 - Mark 909
我有一些乘客的属性,这些属性是根据他们在队列中的位置派生的。例如,Passenger.Position。为了返回这个属性,乘客需要知道它所属的父队列。当然,我可以在队列上定义一个方法,接受乘客作为参数,并返回其位置,但这样做似乎很混乱。 - Mark 909
当一个功能需要访问属于同一层级的两个不同对象时,决定将功能实现放入哪种类型中可以成为一门艺术...这个决定中两种类型相对位置是一个因素(但并非必要的决定因素)。 - Charles Bretana
1
无论你将实现放在哪里,当然可以将另一种替代的实现方法放在另一个中,它会委托给第一个实现中的实际实现。 - Charles Bretana

4

双向关系往往很难正确实现。这里的普遍建议应该是“不要这样做,很可能你实际上并不需要它,而且它会给你带来更多的伤害。”

如果经过仔细考虑后,您决定需要一个双向关系,您可以将 Car 属性的 setter 设置为 internal,这并不能完全防止恶意设置不想要的值,但显著减少了表面积。


我得出了相同的结论。查看 MS Word 和 MS Project 的 API,所有这些对象中都提供了父/子关系。当然,作为 API 用户,我无法更改其中许多属性,但在内部可能是可写的。 - Mark 909
3
认识一些原始MSProject的开发人员,我不会认为它的设计和对象模型代表了"最佳实践"。 ;) (意思是作者认为MSProject的设计并不一定是最好的范例) - mikemanne

2
你可能需要考虑添加一个setter,仅在设置为空时才设置Wheel.parentCar,但前提是您能够假定第一次设置是有效的,并因此能够忽略任何其他尝试。
编辑:但是,那将是添加Car对象的适当位置。您还可以进行检查,例如创建Wheel.validateCar(Car carInQuestion),其中强制执行仅在当前Wheel对象存在于Car中时才设置parentCar属性。这意味着您将拥有一个公共方法,用于基于特定轮子实例搜索Car.wheels的成员身份。但只有在您真正感觉需要严格要求时才需要这样做。

这不是一个坏主意。但这也会暗示你可以改变父级,即使你不能这样做。 - Mark 909
不一定;你可以抛出错误或以任何方式处理它。实现和语义真的是由你决定,以及你想让外部对象如何与其接口 - 但使用设置器有条件地分配属性是合法的。 - mway
是的,那样会保护它。但是如果车轮被拆下来,它就没有办法取消设置了。 - Mark 909
按照同样的逻辑,你可以使用 Wheel.validateCar(Car carInQuestion) 来有效地处理设置和可能的取消设置;这显然是一个具有访问内部属性的方法,因此通过将 Wheel.parentCar 作为参数调用它,你可以根据上下文处理两种操作。 - mway
我承认这可能不是最好的设计选择,但如果你必须限制访问/可见性,那么没有太多__好的__选择,而不是牺牲简单性或安全性。 - mway

0
问题:Wheel实例是否可以在以后分配给不同的Car实例?
1)如果是,那么公开一个方法来设置parentCar。您可以将其设置为流畅方法,以便于编码:
public class Wheel 
{
    private Car parentCar;

    public ParentCar 
    {
        get
        {
            return parentCar;
        }
    }

    public void SetParentCar(Car _parentCar)
    {
        parentCar = _parentCar;
        return this;
    }
}

在你分配添加轮子的位置,进行赋值:

void AddWheel ( Wheel WheelToAdd)
{
    wheels.Add(WheelToAdd.SetParentCar(this));
    //Some Other logic relating to adding wheels here
}



2) 如果没有,只需在构造函数中设置父级。

public class Wheel 
{
    private Car parentCar;

    public ParentCar 
    {
        get
        {
            return parentCar;
        }
    }

    public Wheel(Car _parentCar)
    {
        parentCar = _parentCar;
    }
}

0

在Wheel对象的Car属性中进行写入操作对我来说是有意义的,因为没有什么能阻止你将车轮从一个ToyotaCamry(Car子类)移动到另一个ToyotaCamry。不过,在允许Car.Drive()被调用之前,你可能希望在原始汽车上有一个Drivable属性来检查List.Count是否仍然为4。

你是否使用Car创建Wheels?如果是,则在那个时候设置属性。否则,在Wheel附加到Car时设置它。


每个汽车对象都需要一些容器来计算连接的车轮数量。请查看我的编辑关于检查可驾性。可能更复杂,因为前后轮可能不同。例如具有10个车轮的汽车等。 - Steve Townsend
@Mark 909 - 别忘了备用的。 - Steve Townsend
是的,我需要在Car中有一个集合,但问题是要保持该集合和车辆的父属性同步。 - Mark 909
@Mark 909 - 在你的Car的AddPartRemovePart方法中,应该封装集合的管理和任何所有权属性。我认为这是必要的。 - Steve Townsend
同意。但它防止了Car类外的其他代码更改此属性。 - Mark 909
显示剩余2条评论

0
我会选择“不要”。车轮不仅仅是Car的属性。车轮可以用于TrailerCartFerrisWheel(好吧,也许这有点牵强)。关键是,通过将使用车轮的汽车作为车轮本身的属性,您将车轮的设计与在汽车上使用相耦合,在那一点上,您失去了类的任何可重用性。
在这种情况下,让车轮知道它实际上用于什么似乎有很多损失而很少收益。


0
可能有点偏离中心,但从概念上讲,车轮是汽车的“部分”。你考虑过使用部分类吗?乘客是队列的“部分”。我发现在某些情况下使用部分类和良好定义的接口非常有用。你可以将整个对象的方面抽象成接口,并将每个接口实现为部分类。结果是一个完整的对象,能够通过其部分接口进行抽象,以满足一系列合约情况。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接