C#正确耦合父/子对象而不牺牲可伸缩性

时间:2016-08-19 01:28:14

标签: c# generics inheritance

考虑两个类:

public class Parent { }
public class Child { }

让我们说Parent拥有一系列Child元素。 Parent有自己的属性,方法等。没有Parent实例,Child不能存在,因为它的一些属性依赖于Parent实例......

public class Parent
{
    // ImmutableList is used to simplify INotifyPropertyChanged which is used in actual code
    public ImmutableList<Child> Children { get; set; }

    // more ...
}

public class Child
{
    private Parent Parent { get; }

    // Other properties using Parent...

    public Child(Parent parent)
    {
        if (parent == null)
            throw new ArgumentNullException(nameof(parent));

        Parent = parent;
    }

    // more...
}

现在这一切都很好,直到我们决定我们需要父母和孩子可以扩展以便将来实施更改等等。让我们把它们抽象出来,这样我们就可以做到这一点。现在还有另一个问题。我们的ChildrenParent属性使用抽象类型。当我们稍后推导时,这不会削减它。如果我们向派生的Parent和派生的Child添加属性,我们希望以某种方式将这些派生相互关联,以便他们可以看到彼此的属性。好吧,没问题,让它们变得通用!

public abstract class Parent<C> where C : Child
{
    // ImmutableList is used to simplify INotifyPropertyChanged which is used in actual code
    public ImmutableList<C> Children { get; set; }

    // more ...
}

public abstract class Child<P> where P : Parent
{
    private P Parent { get; }

    // Other properties using Parent...

    public Child(P parent)
    {
        if (parent == null)
            throw new ArgumentNullException(nameof(parent));

        Parent = parent;
    }

    // more...
}
好吧,不能编译。我们的约束是在没有任何泛型的情况下引用Parent和Child。尝试自己解决这个问题。你会在这附近找到自己......

public abstract class Parent<P, C> where P : Parent<P, C> where C : Child<P, C>
{
    // ImmutableList is used to simplify INotifyPropertyChanged which is used in actual code
    public ImmutableList<C> Children { get; set; }

    // more ...
}

public abstract class Child<P, C> where P : Parent<P, C> where C : Child<P, C>
{
    private P Parent { get; }

    // Other properties using Parent...

    public Child(P parent)
    {
        if (parent == null)
            throw new ArgumentNullException(nameof(parent));

        Parent = parent;
    }

    // more...
}

这里我们有重复的泛型类型(又名CRTP - 奇怪的重复模板模式;在这里阅读更多相关信息https://en.wikipedia.org/wiki/Curiously_recurring_template_patternIs letting a class pass itself as a parameter to a generic base class evil?)。然而,我们有两个学位,因为我们耦合了两个类。

这完全解决了这个问题,它允许通过继承层次结构完全控制共享代码,同时保持对它的通用接口。虽然我没有卖掉它,因为它非常复杂,如果没有文档,对其他开发人员来说将是一个很大的痛苦。这个有多复杂,不卖?观看...

简单的场景。让我们得出一个自定义的父和子,而不是关心他们的重用(他们将永远相互耦合,没有其他父派生或子派生可以分享任何代码。)这里是:

public class ManParent : Parent<ManParent, ManChild> { }

public class ManChild : Child<ManParent, ManChild>
{
    public ManChild(ManParent parent) : base(parent)
    {
    }
}

这很棒。它允许我们派生父和子,从共享代码中获益,并且仍然可以在ManParent中对ManChild中的元素进行编译时访问,反之亦然。如果我们想要派生另一个使用同一个Parent的Child,怎么办?不能这样做,因为两个类都指定了另一个;它们是永远永远的,永久地耦合在一起。

为了派生可以由多个派生的Child类(或者可以由多个派生的Parent类使用的Child)使用的Parent,我们可以一次派生一个通用变量...

public class CoolParent<C> : Parent<CoolParent<C>, C> where C : CoolKid<C> { }

public abstract class CoolKid<C> : Child<CoolParent<C>, C> where C : CoolKid<C>
{
    public CoolKid(CoolParent<C> parent) : base(parent)
    {
    }
}

public class CoolKidOne : CoolKid<CoolKidOne>
{
    public CoolKidOne(CoolParent<CoolKidOne> parent) : base(parent)
    {
    }
}

public class CoolKidTwo : CoolKid<CoolKidTwo>
{
    public CoolKidTwo(CoolParent<CoolKidTwo> parent) : base(parent)
    {
    }
}

public class CoolKidThree : CoolKid<CoolKidThree>
{
    public CoolKidThree(CoolParent<CoolKidThree> parent) : base(parent)
    {
    }
}
// You could even go nuts and derive a CoolParent for each CoolKid, but I digress

这允许CoolKid使用CoolParent整合代码,CoolParent是所有CoolKids的通用代码,并允许每个CoolKid扩展CoolParent的使用,同时保持CoolParent在一个地方使用CoolKid。

我不了解你,但我准备跳下桥了:)

无论如何,这就是问题所在。 这是我唯一的这样的好设计选项吗?这看起来过于复杂,但我真的倾向于看到重复大量的代码作为禁忌而转向抽象类和泛型类型来解决那个问题。

我能看到的唯一其他选项是使Parent为generic和abstract,并包含对Child的所有访问权限。然后我可以将所有与Parent-Child相关的属性保留在Parent而不是Child中,因此Child可以是通用的,不知道Parent,所以没有CRTP。然而,这意味着,随着孩子变得越来越复杂,父母将变得越来越复杂。更不用说所有包装器必须能够通过某种索引路由到正确的Child,因为Child本身将被隐藏。这似乎会给运行时错误IMO带来太大的空间,并让我想起四处走动&#39;处理&#39;在非面向对象的环境中,ick。

编辑,有关实际设计的更多信息:

我正在构建的实际应用程序是一个MVVM(C)应用程序,它包含视图,视图模型,然后是一些控制器(将视图模型连接到更大的模型)。较大的模型有几个重要方面:

  • 必须具有通用接口。在一天结束时,此模型的实际实现通过串行端口与任意数量的设备进行通信。此通信涉及按需命令/查询以及实时流媒体&#39;数据速率约为每秒10次。
    • 它必须足够抽象,能够表示100%软件(在内存中计算)的功能,或者只是从设备本身查询(通常会计算很多东西)。在软件的生命周期中,这些事情可能会发生很大变化。
    • 可能需要使用锁是线程安全的。目前,我计划在构造函数中要求同步上下文,然后仅从上下文中改变模型以避免线程安全问题。但是,我不太可能需要在所有属性周围放置锁定代码(约20ish,但将来会增长)。
    • 必须能够辨别内部更改(来自设备的更改并随着时间的推移而改变模型)VS外部更改(源自用户请求的更改,如UI,控制器,命令行等) 。)

上述要求导致很多问题,并且保持简单&#34;。通常,有了类似的东西,我尝试完全避免泛型,只是重复代码。但是,在这种情况下,即使没有锁定,我也会为INotifyPropertyChanged和其他变异方法(用于外部变异请求)以及随时间处理变异的异步代码寻找数百行样板。每次我需要稍微不同或完全不同的界面实现时重复所有这些代码将不仅仅是一个麻烦,它将是一个维护/开发的噩梦。该软件需要太多的可靠性和稳定性才能冒这种设计风险。这就是我甚至考虑像二级CRTP这样的原因。

考虑到这一点,希望人们在这个问题上给出两分钱更容易。

编辑2:

我一直在进一步思考这方面的替代方案,并且没有发现任何能够提供此设计最重要的好处的东西:给定父子组合的编译时限制功能集。我开始认为这虽然非常复杂,但却是最好的设计路线。所以我提出了一个更狭隘的问题:除了复杂性之外,这样的设计会引入什么问题呢?到目前为止,由于强类型,似乎唯一可能产生的坏处是对未来的开发人员造成混淆,如果他们无法正确派生父/子,则会导致编译时错误。这就是文档绰绰有余的地方。

2 个答案:

答案 0 :(得分:1)

据我所知,父/子关系不是OOP。您试图将Parent和Child绑定到具体类型的Child / Parent配对,而不允许继承。你明确地试图打破Liskov,这是你发现困难的地方。因为OOP固有地设计为与LSP一起工作。

如果通过使用接口/协方差来放松要求,则可以解决许多问题。鉴于您已经确定Children集合必须是不可变的,这似乎是隐含的可能性。

public class Parent<TChild> : IParent<TChild>
   where TChild : IChild<IParent<TChild>>
{
    public IEnumerable<TChild> Children { get { return null ;} }
}

public class Child<TParent> : IChild<TParent>
   where TParent: class, IParent<IChild<TParent>>
{
    public TParent Parent{ get { return null ;} }
}

public interface IChild<out TParent>
{
    TParent Parent { get;}
}

public interface IParent<out TChild>
{
    IEnumerable<TChild> Children { get; }
}

答案 1 :(得分:1)

在考虑到Aron所说的父/子关系不是真正的OOP之后,我已经摆脱了强烈的双向耦合。相反,我已经隔离了孩子,只有父母知道孩子。 Child需要访问Parent的任何实例都已移至Parent类(作为需要Child实例的方法)。这使得Parent只在一个变量(C:Child)中使用,而Child根本不是通用的,所以没有CRTP。虽然这会使Parent类更加复杂,但我们仍然保留派生Parent和派生Child之间的编译时关系的优点。

这样的事情:

public abstract class Parent<C> where C : Child
{
    public ImmutableList<C> Children { get; set; }
}

public abstract class Child
{
}