经过10年以上的Java / C#编程,我发现自己创建了以下任一文件:
- 抽象类:合同,并非原样实例化。
- 期末/密封班:实现不打算用作其他任何东西的基类。
我想不出任何简单的“类”(即既不是抽象的也不是最终的/密封的)是“明智的编程”的情况。
为什么一个班级应该不是“抽象”或“最终/密封”之外的任何东西?
编辑
这篇很棒的文章比我能更好地解释我的担忧。
经过10年以上的Java / C#编程,我发现自己创建了以下任一文件:
我想不出任何简单的“类”(即既不是抽象的也不是最终的/密封的)是“明智的编程”的情况。
为什么一个班级应该不是“抽象”或“最终/密封”之外的任何东西?
编辑
这篇很棒的文章比我能更好地解释我的担忧。
Answers:
具有讽刺意味的是,我发现相反的情况:使用抽象类是一个例外,而不是规则,并且我不喜欢最终/密封类。
接口是一种更典型的按合同设计的机制,因为您没有指定任何内部结构–您不必担心它们。它允许该合同的每个实施都是独立的。这是许多领域的关键。例如,如果您正在构建ORM,则可以以统一的方式将查询传递给数据库非常重要,但是实现可能会大不相同。如果为此目的使用抽象类,则最终会在可能适用于所有实现的组件中进行硬接线。
对于最终/密封类,我能看到的唯一借口是允许覆盖实际上是危险的时候-可能是加密算法或其他东西。除此之外,您永远都不知道出于本地原因何时希望扩展功能。封闭类会限制您在大多数情况下都不存在的收益选择。编写类的方式要灵活得多,以便以后可以扩展它们。
通过使用密封类的第三方组件,我的后一种观点得到了巩固,从而防止了某些使生活变得更加轻松的集成。
这是埃里克·利珀特(Eric Lippert )的精彩文章,但我认为它不支持您的观点。
他认为,所有公开供他人使用的类都应被密封,或以其他方式使其不可扩展。
您的前提是所有类都应该是抽象的或密封的。
巨大差距。
EL的文章并未提及您和我一无所知的他的团队制作的(大概有很多)课程。通常,框架中公开的类仅是实现该框架所涉及的所有类的子集。
new List<Foo>()
为List<Foo>.CreateMutable()
。
从Java的角度来看,我认为最终类并不像看起来那样聪明。
许多工具(尤其是AOP,JPA等)都可以处理加载时间,因此必须扩展您的专长。另一种方法是创建委托(而不是.NET委托),将所有内容都委托给原始类,这比扩展用户类要麻烦得多。
需要使用普通的非密封类的两种常见情况:
技术的:如果您的层次结构深度超过两个层次,并且希望能够在中间实例化某些内容。
原理:有时最好编写明确设计为安全扩展的类。(在编写像Eric Lippert这样的API时,或者在团队中处理大型项目时,这种情况经常发生。)有时您想编写一个本身可以正常工作的类,但在设计时要考虑到可扩展性。
埃里克·利珀特(Eric Lippert)关于密封的想法是有道理的,但他也承认,通过将类保持“开放”状态,它们确实为可扩展性而设计。
是的,BCL中密封了许多类,但是没有很多类,并且可以通过各种奇妙的方式进行扩展。我想到的一个例子是Windows窗体,您可以在其中Control
通过继承将数据或行为添加到几乎所有窗体或窗体中。当然,这可以通过其他方式(装饰器模式,各种类型的合成等)完成,但是继承也可以很好地工作。
.NET的两个特定说明:
virtual
除了显式接口实现之外,继承者都不会破坏您的非功能性。internal
而不是密封类,从而使它可以在代码库内部而不是在代码库外部进行继承。我相信许多人认为课程应该final
/ 的真正原因sealed
是,大多数非抽象的可扩展课程没有得到适当的记录。
让我详细说明。从远处开始,一些程序员认为,作为OOP中的一种工具,继承被广泛使用和滥用。我们都读过Liskov替代原则,尽管这并没有阻止我们违反它数百次(甚至数千次)。
问题是程序员喜欢重用代码。即使这不是一个好主意。继承是这种“重新编码”的关键工具。返回到最后/密封的问题。
最终/密封类的适当文档相对较少:您描述每种方法的功能,参数,返回值等。所有常用的东西。
但是,在正确记录可扩展类时,必须至少包括以下内容:
super
实现吗?是在方法的开始还是结束时调用它?考虑构造函数与析构函数)这些只是我的头上。我可以为您提供一个示例,说明为什么每个元素都很重要,而跳过这些元素将使扩展类搞砸。
现在,考虑应该为正确记录每件事做多少记录工作。我相信具有7-8个方法的类(在理想化的世界中可能太多了,但实际上却太少了)可能还包含约5页的纯文本文档。因此,取而代之的是,我们半途而废,不给全班同学上课,以便其他人可以使用它,但也不能正确记录下来,因为这将花费大量时间(而且,您可能会无论如何都永远不会扩展,那么为什么要打扰呢?)。
如果您正在设计一门课程,您可能会感到有一种将其密封的诱惑,以使人们无法以您未曾预见(和准备)的方式使用它。另一方面,当您使用别人的代码时,有时从公共API使用该类,则没有明显的理由使该类最终定下来,您可能会认为“该死,这只花了我30分钟的时间来寻找解决方法”。
我认为解决方案的一些要素是:
还值得一提下面的“哲学”问题:是一类是sealed
/ final
合同的类,或者实现细节的一部分?现在我不想踩到那里,但是对此的答案也将影响您决定是否上课的决定。
如果出现以下情况,则该类既不应是最终的/封闭的,也不应该是抽象的:
例如,使用ObservableCollection<T>
C#中的类。它只需要将事件的引发添加到a的常规操作中Collection<T>
,这就是它子类化的原因Collection<T>
。Collection<T>
它本身就是一个可行的类,因此ObservableCollection<T>
。
CollectionBase
(抽象),Collection : CollectionBase
(密封),ObservableCollection : CollectionBase
(密封)。如果仔细查看Collection <T>,您会发现它是一个半确定的抽象类。
Collection<T>
“半辅助抽象类”又如何呢?
Collection<T>
通过受保护的方法和属性公开了许多内脏,并且显然打算成为专门收集的基类。但是尚不清楚应该将代码放在哪里,因为没有抽象方法可以实现。ObservableCollection
继承自Collection
并且未密封,因此您可以再次继承它。而且您可以访问Items
受保护的属性,从而可以在不引发事件的情况下添加集合中的项目。很好。
最终/密封类的问题在于,他们正在尝试解决尚未发生的问题。仅当问题存在时它才有用,但由于第三方施加了限制,这令人沮丧。密封等级很少解决当前的问题,这使得很难说出它的有用性。
在某些情况下,应该密封一堂课。例如 该类以无法预测将来的更改如何改变该管理的方式来管理分配的资源/内存。
多年来,我发现封装,回调和事件比抽象类更加灵活/有用。我看到了很多具有大型类层次结构的代码,其中封装和事件会使开发人员的生活变得更简单。
测试类似乎浮现在脑海。这些是基于程序员/测试人员试图完成的工作而以自动化方式或“随意”调用的类。我不确定我是否曾见过或听说过私人完成的测试。
当您需要为将来做一些实际的计划时,需要考虑打算扩展的课程。让我给你一个现实生活中的例子。
我花了大量时间在主要产品和外部系统之间编写接口工具。当我们进行新的销售时,一个重要组成部分是一组出口商,这些出口商被设计为定期运行,它们生成详细描述当天发生的事件的数据文件。这些数据文件然后由客户的系统使用。
这是扩展课程的绝佳机会。
我有一个Export类,它是每个出口商的基础类。它知道如何连接到数据库,找出上次运行该数据库的位置,并为其生成的数据文件创建存档。它还提供属性文件管理,日志记录和一些简单的异常处理。
最重要的是,我有一个不同的导出器来处理每种类型的数据,也许有用户活动,交易数据,现金管理数据等。
在此堆栈的顶部,我放置了一个特定于客户的层,该层实现了客户所需的数据文件结构。
这样,基本出口商很少更改。核心数据类型导出器有时会更改,但很少更改,通常仅用于处理数据库架构更改,无论如何应将这些更改传播给所有客户。我为每个客户要做的唯一工作就是特定于该客户的那部分代码。完美的世界!
所以结构看起来像:
Base
Function1
Customer1
Customer2
Function2
...
我的主要观点是,通过以这种方式设计代码,我可以将继承主要用于代码重用。
我不得不说,我想不出任何理由要超过三层。
我已经使用了两层,例如,有一个通用Table
类用于实现数据库表查询,而子类则Table
通过使用enum
定义字段来实现每个表的特定细节。让enum
实现器在Table
类中定义一个接口具有各种意义。
我同意你的观点。我认为在Java中,默认情况下,类应声明为“最终”。如果您没有将其定稿,请专门进行准备并记录以进行扩展。
这样做的主要原因是要确保您的类的任何实例都将遵守您最初设计和记录的接口。否则,使用您的类的开发人员可能会创建脆弱且不一致的代码,进而将其传递给其他开发人员/项目,从而使类的对象不可信。
从更实际的角度来看,这确实有一个缺点,因为库界面的客户端将无法进行任何调整,而无法以您最初想到的更灵活的方式使用您的类。
就个人而言,对于存在的所有质量差的代码(并且由于我们在更实际的水平上进行讨论,因此我认为Java开发更容易这样做),我认为这种刚性对于我们寻求更容易实现的目标来说是一个很小的代价。维护代码。
Open/Closed principle
,而不是Closed Principle.