【问题标题】:C# properly coupling Parent/Children objects without sacrificing scalabilityC# 在不牺牲可伸缩性的情况下正确耦合父/子对象
【发布时间】:2016-08-19 01:28:14
【问题描述】:

考虑两个类:

public class Parent { }
public class Child { }

假设 Parent 拥有一个 Child 元素的集合。 Parent 有自己的属性、方法等。Child 不能没有 Parent 实例而存在,因为它的某些属性依赖于 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...
}

现在这一切都很好而且很花哨,直到我们决定我们需要 Parent 和 Child 可以为未来的实现更改等进行扩展。让我们将它们抽象化,以便我们可以做到这一点。不过,现在还有另一个问题。我们的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_pattern 和这里 Is letting a class pass itself as a parameter to a generic base class evil?)。然而,我们在两个程度上有这个,因为我们正在耦合两个类。

这完全解决了这个问题,允许通过继承层次结构完全控制共享代码,同时保持一个通用接口。不过,我并不赞成它,因为它非常复杂,而且如果没有文档,其他开发人员要破译它会很痛苦。不卖这有多复杂?观看...

简单的场景。让我们派生一个自定义的 Parent 和 Child,而不关心它们的重用(它们将永远相互耦合,并且没有其他 Parent 派生或 Child 派生可以介入共享任何代码。)这里是:

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

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

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

为了派生一个可供多个派生子类使用的Parent(或可以被多个派生Parent类使用的Child),我们可以一次派生一个泛型变量...

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 整合所有 CoolKids 通用的代码,并允许每个 CoolKid 扩展他们对 CoolParent 的使用,同时将 CoolParent 对 CoolKid 的使用保持在一个地方。

我不了解你,但我正准备从桥上跳下来 :)

不管怎样,问题来了。 对于这样的事情,这是我唯一的好设计选择吗?这似乎过于复杂,但我确实倾向于认为复制大量代码是不行的,并转向抽象类和泛型类型来解决那个问题。

我能看到的唯一其他选择是使 Parent 通用和抽象,并包装对 Child 的所有访问。然后我可以将所有依赖于父子的属性保留在 Parent 而不是 Child 中,因此 Child 可以是通用的,不知道 Parent,所以没有 CRTP。然而这意味着,随着 Child 变得越来越复杂,Parent 也会变得越来越复杂。更不用说所有包装器都必须能够通过某种索引路由到正确的 Child,因为 Child 本身将被隐藏。这似乎会为 IMO 的运行时错误引入太多空间,并提醒我在非面向对象的环境中传递“句柄”,ick。

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

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

  • 它必须有一个通用接口。归根结底,该模型的实际实现通过串行端口与任意数量的设备通信。这种通信涉及按需命令/查询以及以每秒约 10 次的速率实时“流式传输”数据。
    • 它必须足够抽象,以便能够表示 100% 软件(在内存中计算)或简单地从设备本身(通常计算很多东西)查询的特征。在软件的生命周期中,这些事情可能会发生很大变化。
    • 可能需要使用锁实现线程安全。目前,我计划在构造函数中需要一个同步上下文,然后只从上下文中改变模型以避免线程安全问题。但是,我可能需要在所有属性周围添加锁定代码(大约 20 左右,但未来可能会增长)。
    • 它必须能够区分内部更改(来自设备的更改并随时间改变模型)与外部更改(源自用户请求的更改,如 UI、控制器、命令行等.)

上述要求导致了很多“保持简单”的问题。通常,对于这样的事情,我尝试完全避免使用泛型,而只是重复代码。然而,在这种情况下,即使没有锁,我也在寻找数百行 INotifyPropertyChanged 和其他变异方法(用于外部变异请求)的样板文件,以及随着时间的推移处理变异的异步代码。每次我需要稍微不同或完全不同的接口实现时重复所有这些代码,这不仅仅是一件令人讨厌的事情,这将是一个维护/开发的噩梦。该软件需要太多的可靠性和稳定性才能冒这种设计的风险。这就是为什么我什至考虑像二级 CRTP 这样的东西。

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

编辑 2:

我一直在进一步考虑替代方案,但没有遇到任何提供此设计所具有的最重要的好处的东西:给定父子组合的编译时受限功能集。我开始认为这是最好的设计路线,尽管非常复杂。所以我提出了一个更狭窄的问题:抛开复杂性不谈,这样的设计会引入什么问题?到目前为止,由于强类型化,似乎唯一可能产生的坏事是给未来的开发人员带来困惑,如果他们未能正确派生父/子,则会导致编译时错误。这就是文档足以填补空白的地方。

【问题讨论】:

  • 可重用性是一件好事,但如果你不得不牺牲代码的可读性,那就不值得了。尽量让事情尽可能简单,你真的需要创建所有这些结构来重用属性和东西吗?很难给出另一种解决方案,因为我根本不知道您要解决什么设计问题,这实际上取决于您使用的环境。
  • @Alisson 感谢您在介绍了如此奇怪的设计/问题后指出缺乏上下文,当我发布时应该写更多。我更新了帖子以反映导致我达到这一点的一般设计要求。
  • 据我所知,父/子关系不是 OOP。您正在尝试将 Parent 和 Child 绑定到具体类型的 Child/Parent 配对,不允许继承。你明确地试图打破 Liskov,这就是你发现困难的地方。因为 OOP 本质上是为与 LSP 一起工作而设计的。
  • 如果你通过使用接口/协方差来放松需求,你可以解决很多问题。鉴于您已经决定 Children 集合必须是不可变的,这似乎是一种隐含的可能性。
  • @Aron 感谢您提供有关 OOP 的信息,这对我来说是个新闻!我也在研究利用接口和协方差。

标签: c# generics inheritance


【解决方案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; }
}

【讨论】:

  • 协方差允许什么否则在本例中不起作用?另外,有了这样的东西,我什至会继承父/子吗?还是我只会为我的新实现实现 IParent/IChild?
  • @ckemper 它允许您不必每次都实现父子对。我认为....
【解决方案2】:

在考虑了 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
{
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-27
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多