【发布时间】: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 可以为未来的实现更改等进行扩展。让我们将它们抽象化,以便我们可以做到这一点。不过,现在还有另一个问题。我们的Children 和Parent 属性使用抽象类型。当我们稍后推导时,这不会削减它。如果我们将属性添加到派生的 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