从抽象基类返回规范表示子类是否可以接受?
编辑2: TL; DR :有没有办法不打破OO最佳实践,同时仍然满足一系列相同类型的东西必须转换为类似规范的东西的约束?
此外,请记住,我的问题是关于一般情况,而不是具体的例子。 这不是一个家庭作业问题。
假设您有以下内容:
- 实现通用function的抽象基类;
- 一个具体的派生类,用作规范表示。
现在假设您希望基类的任何inheritance者可以转换为规范表示。 实现此目的的一种方法是在基类中使用一个抽象方法,该方法旨在将inheritance者的转换作为规范派生类的实例返回。
但是,似乎普遍认为基类不应该知道它们的任何派生类,并且在一般情况下,我同意。 但是,在这种情况下,这似乎是最好的解决方案,因为它允许任意数量的派生类,每个派生类都有自己的实现,我们不需要知道任何事情,通过转换为每个派生的规范表示可以互操作class必须实施。
你会以不同的方式做吗? 为什么以及如何?
几何点的示例:
// an abstract point has no coordinate system information, so the values // of X and Y are meaningless public abstract class AbstractPoint { public int X; public int Y; public abstract ScreenPoint ToScreenPoint(); } // a point in the predefined screen coordinate system; the meaning of X // and Y is known public class ScreenPoint : AbstractPoint { public ScreenPoint(int x, int y) { X = x; Y = y; } public override ScreenPoint ToScreenPoint() => new ScreenPoint(X, Y); } // there can be any number of classes like this; we don't know anything // about their coordinate systems and we don't care as long as we can // convert them to `ScreenPoint`s public class ArbitraryPoint : AbstractPoint { private int arbitraryTransformation; public ArbitraryPoint(int x, int y) { X = x; Y = y; } public override ScreenPoint ToScreenPoint() => new ScreenPoint(X * arbitraryTransformation, Y * arbitraryTransformation); // (other code) }
编辑1: AbstractPoint
和ScreenPoint
不是同一个类的原因是语义。 AbstractPoint
没有已定义的坐标系,因此AbstractPoint
实例中的X和Y值无意义。 ScreenPoint
确实具有已定义的坐标系,因此ScreenPoint
实例中的X和Y值具有明确定义的含义。
如果ScreenPoint
是基类,则ArbitraryPoint
将是ScreenPoint
,但ScreenPoint
并非如此。 ArbitraryPoint
可以转换为ScreenPoint
,但这并不意味着它is-a
ScreenPoint
。
如果您仍然不相信,可以考虑将任意坐标系ACS1
定义为具有到屏幕坐标系SCS
的动态偏移。 这意味着两个坐标系之间的映射可以随时间变化,即点ACS1 (1, 1)
可以在一个时刻映射到SCS (10, 10)
,而在另一个时刻映射到SCS (10, 10)
SCS (42, 877)
。
这种设计通常是代码气味。 基类不应该知道它们的派生类,因为它创建了一个循环依赖。 循环依赖通常会导致复杂的设计,很难推断出应该在哪些类中进行。 在Java中,了解其派生类的基类甚至可能在极少数情况下导致死锁(我不知道C#)。
但是,如果您确切知道自己在做什么,特别是如果您想要实现的目标非常简单,那么您可以在特殊情况下违反一般规则。
你的情况似乎很简单。 将AbstractPoint
和ScreenPoint
作为不同的类是正确的。 但实际上它们“一起工作”:所有AbstractPoint
应该能够转换为ScreenPoint
(这可能是AbstractPoint
合同中最重要的function?)。 由于没有另一个不能存在,所以AbstractPoint
了解ScreenPoint
并没有错。
更新
在不同的设计中:创建一个名为CanonicalPoint
的界面。 AbstractPoint
有一个名为ToCanonicalPoint
的方法,它返回CanonicalPoint
。 所有派生类的AbstractPoint
都必须实现它并返回CanonicalPoint
。 ScreenPoint
是AbstractPoint
的派生类,它实现了CanonicalPoint
接口。 您甚至可以拥有多个实现CanonicalPoint
派生类。 注意:如果AbstractPoint
和CanonicalPoint
有共同的方法,两者都可以实现另一个名为Pointable
接口,它声明了所有这些方法。
我觉得SOLID中的单一责任原则被忽略了。 AbstractPoint
及其具体实现ScreenPoint
本质上是存储数据 ,例如X, Y
,对于类组是唯一的。 同样,基类AbstractPoint
试图强制执行内联工厂方法模式 (减去返回的接口)。 虽然我认为对AbstractPoint
和/或ScreenPoint
类中的数据进行逻辑操作是合适和必要的; 我觉得这里需要一个单独的ScreenPointFactory
类实现Factory Method模式来实例化ScreenPoint
。
如果您需要创建数千个ScreenPoint
类,则使用额外的虚拟方法调用(如ToScreenPoint()
和额外的对象大小)可能会产生负面的性能问题。 考虑在此场景中使用Flyweight模式可以帮助加载时间。 要使Flyweight模式成功,您需要实施Factory Method模式 ,因为在工厂中使用了Flyweight模式 。 由于将有工厂,您将需要一个IPoint
接口,并简单地从接口IPoint
派生AbstractPoint
。 IPoint
现在只需持有X, Y
. 其余的点类型通过inheritanceAbstractPoint
获得IPoint
接口。 由于会有一组相关的点类型,例如ArbitraryPoint
, ScreenPoint
,它们都可以通过抽象工厂实例化。
接口编程,而不是实现原则在这里很重要。 为了在我描述的内容中实现ToScreenPoint()
的function,我将完全从Point类中删除ToScreenPoint()
,而是创建一个在启动时配置的ArbitraryTransformation
对象。 在IOC配置期间,我会使用Dependency Injection将ArbitraryTransformation
对象放入适当的工厂。 然后,当调用抽象工厂方法来创建屏幕点的任何新变体时……它们是使用已经计算的任意变换创建的 ,因为AbstractFactory
每个独立工厂方法将使用适当配置的ArbitraryTransformation
来执行计算。
这样做可以减轻您的设计压力,并使您的物体更轻松,更松散地耦合。 我觉得你在这里处理复杂问题,你可以用GoF模式语言弄清楚我刚才所说的内容。 但是,如果您或任何人宁愿拥有编码样本,我可以回来并提供样本解决方案。 如果您不完全需要或想要我建议的内容,就好像要链接很多代码。
我继续在我的GitHub上开发了一个名为Point Example的示例解决方案。 让我知道你的想法。
此外,对于此处添加的每个设计模式,它将为您的应用程序引入额外的复杂层,因此虽然这本身可能是一个问题,但我认为它将有助于您所需要的。
更新
在点示例中,我决定对对象池模式进行非常简单的实现,而不是上面提到的Flyweight pattern
。 如果需要一个坚实的例子,我会在这样做时展示性能改进模式的位置。 虽然有更好的方法来实现,但ScreenPointFactory
使用的Object Pool pattern
仅仅是function性占位符。
我更新了Point Example中的代码,以反映将Flyweight pattern
命名为Object Pool pattern
。 对不起任何困惑。 另外,以下是评论部分中Miroslav Policki的问题的答案:
问题1:
1.进入工厂方法模式的动机似乎是使用Flyweight模式的能力。 但是,据我所知,Flyweight模式是一种性能优化,因此不应过早应用。 因此,在不需要Flyweight模式的情况下,您是否仍会继续进行其余设计,为什么? – 米罗斯拉夫波利奇
答:我做了一个更正,引用了Object Pool pattern
而不是Flyweight pattern
到代码。 无论我的命名错误如何, Object Pool pattern
和Flyweight pattern
都是性能优化。 但是,我创建工厂的动机是出于习惯,因为通常这样做是为了封装创造性逻辑。 是的,我从未要求在Object Pool pattern
的示例代码中实现我的实现,并且这是在这种情况下过早应用的性能优化。 然而,如果有人想知道在这个范例中将性能提升放在哪里,而不花太多时间,我觉得这更像是一种教育奖励。 我给了ArbitraryPointFactory
没有性能增强,以显示工厂的灵活性。
此外, 是的 ,我绝对会使用工厂创建任何点的变化,因为从给定的例子中可以看到多种具体类型。 保持工厂周围将在代码中的单个位置保持点ojbects的实例化,并且工厂将在代码中产生单个位置以修改创建逻辑。 工厂对于简单和复杂的对象很有用,并且允许我们编程到接口,而不是具体的实现。 但是,像大多数设计模式一样,如果只有一个具体的点对象或者代码中需要为对象调用new
运算符的一个地方,我不会建议任何人必须在其代码中生成工厂。 但是,如果您的代码中有两个实例,其中new
操作符用于创建具体点等,那么拥有一个工厂可能会很好,这样您就可以在工厂中的某个位置进行更改,而不必搜索如果对象需要创建的方式发生了变化,则代码可能会不适当地更新所有位置。
问题2:
2.在相应工厂方法中创建点时应用转换。 但是,这与我的示例中的语义不同,因为每个ArbitraryPoint
都有内部状态,可以随时更改,从而产生不同的ScreenPoint
。 您如何将其融入您的设计中? – 米罗斯拉夫波利奇
答:我的一部分想说ScreenPoint
的名字让我表现ScreenPoint
,我正在努力寻求相关的实施。 我的另一部分想说无论如何我会以这种方式设计它。 但是,我知道你建议ScreenPoint
与像素相关等等……事实并非如此,它只是另一个任意的例子。 我会先给你一个有效的例子。 我正在考虑在video游戏中创建数以千计的ScreenPoint
渲染,可能我们也可能存储每个屏幕点的每个像素的颜色,我们只是创建一个抛出ScreenPoint
对象的全屏渲染。 所以我们假设有一个屏幕点的双缓冲区,我们一次绘制两个屏幕渲染。 我们只计算游戏对象坐标到真实世界坐标的渲染,然后将它们映射到2D屏幕渲染,每秒30到60次。 在这种情况下,我不关心修改现有的渲染X,Y
数据,而不是仅为每个额外的渲染创建一组新的ScreenPoint
对象。 现在看起来很浪费, Flyweight pattern
可以帮助克服在运行时创建这么多新对象,因为只有一个ScreenPoint
对象,并且每个X,Y
的实现将被封装到内部Array
或Dictionary
。 然而,在考虑我的设计时,我还在考虑对象大小以及每个对象必须与数据一起维护的方法和虚拟方法指针的数量,以及在快速创建或处理数千个ScreenPoint
对象时。 该设计允许ScreenPoint
纯粹是一个数据对象,仅此而已。
回到你的语义,我通过ArbitraryTransformation
对象和相关接口通过对象组合实现了计算。 没有什么能阻止我们使用您的AbitraryTransformation
将AbitraryTransformation
对象与您的AbitraryTransformation
组成的AbitraryTransformation
组合用于对象属性或mutator方法,以分别从ArbitraryPoint
或ScreenPoint
对象中执行计算。 这是Has-A
比Is-A
更好地与对象组合发挥作用的地方。 工厂是与其他物体一起构建物体的好地方,例如Has-A
。 我们以后可能希望在unit testing中使用MochTransformation
对象和MochScreenPointFactory
来实际测试ScreenPoint
对象的function或数学。 因此,坚持这种范式允许在配置(IOC容器构建)中自然发生这种情况,保持可能是复杂算法或不可能的场景以在其自己的可替换转换对象中进行测试。 所以,如果你喜欢在那里使用接口而不是将责任交给工厂,那么Nothing什么都阻止你在ScreenPoint
对象上保留ToScreenPoint()
方法。 我重新组织计算发生的地方是为了保持ScreenPoint
简单和小巧。 它的唯一责任就是跟上数据。
我也看到在你的例子中,你在两个地方使用了ScreenPoint
的new
运算符,或者每个ToScreenPoint
方法。 通过将适当的注入工厂放入每个点,可以从每种类型的点的ToScreenPoint()
方法调用工厂方法。 也没有什么可以防止这种情况,并且还有一些其他方面代码将实例化第一个ScreenPoint
或AbstractPoint
,它们可以委托给工厂而不是在需要创建ScreenPoint
每个位置进行设置。 另外,我不喜欢AbstractPoint
类如何返回ScreenPoint
的单个具体实例而不是接口。 您只是为一系列对象嵌入了Abstract Factory pattern
的概念,但每个族都返回一种类型的具体实例,而不是接口。 因此,无论如何,您的调用代码都需要具有上下文感知function。 这就是为什么我在我的例子中突破了ScreenPoint
和ArbitraryPoint
派生自相同界面的概念,但是ArbitraryPoint
拥有它自己的额外/自定义数据。 如果我需要执行从任意点到屏幕点的转换,我必须完全依赖于上下文来使用正确的Factory方法来创建具有正确计算的新点,而这反过来利用了ArbitraryTransformation
计算,例如此刻对象创建。
我想我的例子可能与你的意图有所不同,就是你希望我在IOCConfig.cs
中创建ScreenPointFactory
如下new ScreenPointFactory(new ScreenTransformation())
而不是No-Op类型的操作它做的转换工作, ArbitraryPointFactory
有No-Op。 然后,我可以将X,Y
或者IPoint
接口传递给ScreenPointFactory
或正确的PointFactory
方法,然后从ArbitraryPoint
转换到可能是ScreenPoint
。 因此,在这种情况下,我将修改可能是video游戏模型上的点的ArbitraryPoint
数据,然后使用我的ScreenPoint
从ArbitraryPoint
创建一个ScreenPoint
,它执行所有坐标转换。 然后我不是直接修改ScreenPoint
计算,而是直接修改video游戏模型或原始的ArbitraryPoint
,然后扔掉我生成的ScreenPoint
并通过调用ScreenPointFactory
生成一个新的ScreenPointFactory
来更新我的ScreenPoint
位置。原始的ArbitraryPoint
。 处理这些复杂性肯定会影响我在工厂中使用的技术。 还简化了对象模型,使其以对使用它的应用程序有意义的方式使用。
如果我所说的任何内容都有问题,或者你希望我以任何方式更改示例代码以匹配我的最后一段,那么无论如何都会给我回复。 我可能坚持使用类似于您的示例中的其他方法的唯一方法是尝试实现适配器模式 。 然后我有能力从一个接口转到另一个接口,例如从ScreenPoint
到ArbitraryPoint
,反之亦然,而不强制它们从相同的接口或基类派生。 然而来回提供一些计算和转换。 我的实现代码总是在Adapter类中查找我想要操作的数据,然后来回转换。 无论哪种方式,我想我试图表达的主要设计原则是(在Head First Design Patterns一书中找到):
- 编程到接口,而不是实现。
- 赞成合成而不是inheritance。 (
Has-A
比Is-A
) - 依赖倒置原则(取决于抽象。不依赖于具体的类。)
- 没有变量应该包含对具体类的引用。 (如果你使用new,你将持有一个具体类的引用。使用工厂来解决这个问题!)
- 任何课程都不应来自具体课程。 (如果你派生自一个具体的类,你依赖于一个具体的类。从抽象派生,如接口或抽象类。)
- 没有方法应该覆盖其任何基类的已实现方法。 (如果重写已实现的方法,那么基类实际上并不是一个抽象的开头。)
有趣的是,本书前面提到
Dependency Inversion Principle
,如果您遵循所有这三个设计指南,没有例外,您将永远无法编写单个程序。 所以我会告诉你,试图瞄准这些原则,但只有在他们最有意义的地方。 对于任何设计模式或原则,都有利有弊。 但是,至少在设计模式中,它们更像是模板,可以从已经解决的问题中做到你想要的。 因此,请将它们插入并在适合您设计的地方进行调整。 其他人将熟悉您的设计,可以直接使用您的命名策略并添加到您的代码中。
你没有理由有复杂的事情。 如果类无法实现基类的规范,则违反了Liskovs Substitution原则。
另一种方法是声明任意对象可以表示为坐标的接口:
public interface IScreenPointProvider { ScreenPoint ToPoint(); }
那么对象是什么或者它的内部坐标处理是什么并不重要。
另外不要忘记inheritance是关于is-a
关系。 如果任何子类不支持基类提供的内容,那么它实际上不是is-a
关系。 这通常表明基类确实是一个实用程序类,或者定义的层次结构没有很好地设计。