似乎Java拥有声明类不可继承的能力,而现在C ++也拥有。但是,根据SOLID中的“打开/关闭”原理,为什么这样做有用?对我来说,final
关键字听起来像friend
-是合法的,但是如果您使用它,则很可能是设计错误。请提供一些示例,其中不可派生的类将成为优秀的体系结构或设计模式的一部分。
final
。
似乎Java拥有声明类不可继承的能力,而现在C ++也拥有。但是,根据SOLID中的“打开/关闭”原理,为什么这样做有用?对我来说,final
关键字听起来像friend
-是合法的,但是如果您使用它,则很可能是设计错误。请提供一些示例,其中不可派生的类将成为优秀的体系结构或设计模式的一部分。
final
。
Answers:
final
表达意向。它告诉用户类,方法或变量“不应更改此元素,并且如果要更改它,则说明您不了解现有设计。”
这很重要,因为如果必须预料到您编写的每个类和方法都可能会被子类更改为完全不同的操作,则程序体系结构将非常非常困难。最好预先决定哪些元素应该是可更改的,哪些元素是不可更改的,并通过强制执行不可更改性final
。
您也可以通过注释和体系结构文档来做到这一点,但是总比让希望将来的用户阅读并遵守文档更好。
它避免了脆弱的基类问题。每个类都带有一组隐式或显式保证和不变式。Liskov替代原则要求该类的所有子类型还必须提供所有这些保证。但是,如果不使用,这很容易违反final
。例如,让我们有一个密码检查器:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
如果我们允许重写该类,则一个实现可以将所有人锁定,另一个实现可以使所有人访问:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
这通常不行,因为子类现在的行为与原始行为非常不兼容。如果我们确实打算将班级与其他行为一起扩展,那么责任链会更好:
PasswordChecker passwordChecker =
new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
new DefaultPasswordChecker(
new OpenDoor(null)
);
public interface PasswordChecker {
boolean passwordIsOk(String password);
}
public final class DefaultPasswordChecker implements PasswordChecker {
private PasswordChecker next;
public DefaultPasswordChecker(PasswordChecker next) {
this.next = next;
}
@Override
public boolean passwordIsOk(String password) {
if ("s3cret".equals(password)) return true;
if (next != null) return next.passwordIsOk(password);
return false;
}
}
public final class OpenDoor implements PasswordChecker {
private PasswordChecker next;
public OpenDoor(PasswordChecker next) {
this.next = next;
}
@Override
public boolean passwordIsOk(String password) {
return true;
}
}
当更多复杂的类调用其自己的方法时,问题将变得更加明显,并且这些方法可以被覆盖。在漂亮地打印数据结构或编写HTML时,有时会遇到这种情况。每个方法负责一些小部件。
public class Page {
...;
@Override
public String toString() {
PrintWriter out = ...;
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("</head>");
out.print("<body>");
writeHeader(out);
writeMainContent(out);
writeMainFooter(out);
out.print("</body>");
out.print("</html>");
...
}
void writeMainContent(PrintWriter out) {
out.print("<div class='article'>");
out.print(htmlEscapedContent);
out.print("</div>");
}
...
}
现在,我创建一个子类,添加更多样式:
class SpiffyPage extends Page {
...;
@Override
void writeMainContent(PrintWriter out) {
out.print("<div class='row'>");
out.print("<div class='col-md-8'>");
super.writeMainContent(out);
out.print("</div>");
out.print("<div class='col-md-4'>");
out.print("<h4>About the Author</h4>");
out.print(htmlEscapedAuthorInfo);
out.print("</div>");
out.print("</div>");
}
}
现在暂时忽略这不是生成HTML页面的好方法,如果我想再次更改布局会怎样?我必须创建一个SpiffyPage
以某种方式包装该内容的子类。我们在这里看到的是模板方法模式的偶然应用。模板方法是基类中定义明确的扩展点,将被覆盖。
如果基类发生变化会怎样?如果HTML内容更改太多,则可能会破坏子类提供的布局。因此,事后更改基类并不真正安全。如果您所有的类都在同一个项目中,则这并不明显,但是如果基类是其他人所构建的某些已发布软件的一部分,则这一点将非常明显。
如果打算采用这种扩展策略,我们可以允许用户交换每个零件的生成方式。无论哪种情况,都可以从外部提供每个块的策略。或者,我们可以嵌套装饰器。这将等同于上面的代码,但是更加明确和灵活得多:
Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());
public interface PageLayout {
void writePage(PrintWriter out, PageLayout top);
void writeMainContent(PrintWriter out, PageLayout top);
...
}
public final class Page {
private PageLayout layout = new DefaultPageLayout();
public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
layout = wrapper.apply(layout);
}
...
@Override public String toString() {
PrintWriter out = ...;
layout.writePage(out, layout);
...
}
}
public final class DefaultPageLayout implements PageLayout {
@Override public void writeLayout(PrintWriter out, PageLayout top) {
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("</head>");
out.print("<body>");
top.writeHeader(out, top);
top.writeMainContent(out, top);
top.writeMainFooter(out, top);
out.print("</body>");
out.print("</html>");
}
@Override public void writeMainContent(PrintWriter out, PageLayout top) {
... /* as above*/
}
}
public final class SpiffyPageDecorator implements PageLayout {
private PageLayout inner;
public SpiffyPageDecorator(PageLayout inner) {
this.inner = inner;
}
@Override
void writePage(PrintWriter out, PageLayout top) {
inner.writePage(out, top);
}
@Override
void writeMainContent(PrintWriter out, PageLayout top) {
...
inner.writeMainContent(out, top);
...
}
}
(必须使用附加top
参数来确保调用writeMainContent
通过装饰器链的顶部。这模拟了称为open recursion的子类功能。)
如果我们有多个装饰器,我们现在可以更自由地混合它们。
重用现有类的某些部分的需求远比稍微修改现有功能的需求更多。我见过这样的情况:有人想要一堂课,您可以在其中添加项目并遍历所有项目。正确的解决方案是:
final class Thingies implements Iterable<Thing> {
private ArrayList<Thing> thingList = new ArrayList<>();
@Override public Iterator<Thing> iterator() {
return thingList.iterator();
}
public void add(Thing thing) {
thingList.add(thing);
}
... // custom methods
}
相反,他们创建了一个子类:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
这突然意味着的整个界面ArrayList
已成为我们界面的一部分。用户可以remove()
事物或get()
特定索引处的事物。打算这样吗?好。但通常,我们不会仔细考虑所有后果。
因此建议
extend
没有认真思考过的课程。final
除非您打算覆盖任何方法。有许多示例必须打破此“规则”,但它通常会指导您进行良好而灵活的设计,并避免由于基类的意外更改(或子类作为基类的实例的意外使用)而导致的错误。 )。
一些语言具有更严格的执行机制:
virtual
令我惊讶的是,没有人提到Joshua Bloch撰写的有效Java,第二版(至少每个Java开发人员都需要阅读该书)。书中的第17项对此进行了详细讨论,其标题为:“ 设计和记录继承文件,否则禁止继承 ”。
我不会在本书中重复所有好的建议,但是这些特殊的段落似乎很相关:
但是普通的具体课程呢?传统上,它们既不是最终的,也不是为子类设计和记录的,但是这种情况很危险。每次在此类中进行更改时,扩展该类的客户端类都有可能会中断。这不仅仅是一个理论问题。在修改非最终具体类的内部结构之后,通常会收到与子类相关的错误报告,该非最终具体类的设计和文档未为继承而设计和记录。
解决此问题的最佳方法是禁止在未设计和记录为可安全子类化的类中进行子类化。有两种禁止子类化的方法。两者中较容易的是将类声明为final。另一种方法是将所有构造函数设为私有或包私有,并添加公共静态工厂代替构造函数。在第15条中讨论了提供内部灵活使用子类的灵活性的另一种方法。
final
有用的原因之一是,它确保您不能以违反父类合同的方式对类进行子类化。这样的子类将违反SOLID(最重要的是“ L”),并通过创建一个类来final
阻止它。
一个典型的例子是不可能以使子类可变的方式来子类化不可变的类。在某些情况下,这种行为更改可能会导致非常令人惊讶的效果,例如,当您在地图中将某物用作键时,认为键是不可变的,而实际上您使用的是可变的子类。
在Java中,如果能够继承String
并传递此类对象,则可以引入许多有趣的安全性问题(使它们可变)(或在有人调用其方法时使其回叫,从而有可能将敏感数据从系统中拉出)。围绕一些与类加载和安全性相关的内部代码。
在某些情况下,Final也有助于防止简单的错误,例如在方法中将相同的变量重复用于两件事等。在Scala中,鼓励您仅使用val
与Java中的final变量大致对应的变量,而实际上不使用a var
还是怀疑非最终变量。
最后,至少在理论上,编译器在知道某个类或方法是最终的时可以执行一些额外的优化,因为当您在最终类上调用某个方法时,您确切地知道将调用哪个方法,而不必执行通过虚拟方法表检查继承。
SecurityManager
。大多数程序不使用它,但它们也不运行任何不受信任的代码。+++您必须假设字符串是不可变的,否则您将获得零安全性和一堆任意错误作为奖励。任何假定Java字符串可以在生产代码中进行更改的程序员肯定都是疯了。
第二个原因是性能。第一个原因是因为某些类具有重要的行为或状态,因此不应更改这些行为或状态以使系统正常工作。例如,如果我有一个“ PasswordCheck”类,并且为了建立该类,我雇了一个安全专家团队,并且该类与数百个经过充分研究和定义的协议的ATM进行通信。允许刚从大学毕业的新员工参加“ TrustMePasswordCheck”课程,扩展上述课程可能对我的系统非常有害;这些方法不应该被覆盖,仅此而已。
当我需要一堂课时,我会写一堂课。如果我不需要子类,那么我不在乎子类。我确保类的行为符合预期,并且我使用该类的地方都假定该类的行为符合预期。
如果有人想继承我的班级,我想完全否认对发生的一切责任。我通过将类定为“ final”来实现。如果要对其进行子类化,请记住,在编写该类时并未考虑子类化。因此,您必须获取类源代码,删除“最终”,从那时起发生的一切事情完全由您负责。
您认为那是“不是面向对象的”吗?报酬让我去做一个应该做的事情。没有人为使班级可以继承而付钱给我。如果您获得报酬以使我的课程可重复使用,欢迎您这样做。首先删除“ final”关键字。
(除此之外,“最终”通常允许进行实质性的优化。例如,在Swift的“ final”中,对公共类或公共类的方法而言,这意味着编译器可以完全知道方法调用将执行什么代码,并可以用静态调度(微小的收益)代替动态调度,并且经常用内联(可能是巨大的收益)代替静态调度。
adelphus:“如果您想对其进行子类化,获取源代码,删除'final',这是您的责任”,那么很难理解吗?“最终”等于“公平警告”。
而且我没有付钱做可重用的代码。我有报酬编写符合其预期功能的代码。如果我有报酬来编写两个相似的代码,我会提取公共部分,因为这样便宜,而且我也不会浪费时间。使代码可重用而不被重用浪费我的时间。
M4ks:您总是将不应从外部访问的所有内容设为私有。同样,如果要子类化,则可以获取源代码,并根据需要将其更改为“ protected”,并对所做的事情负责。如果您认为需要访问我标记为私有的内容,那么最好知道您在做什么。
两者:子类化是重用代码的很小一部分。创建无需子类化即可适应的构建基块的功能要强大得多,并且可以从“最终”中受益匪浅,因为这些基块的用户可以依赖于所获得的东西。
假设平台的SDK包含以下类:
class HTTPRequest {
void get(String url, String method = "GET");
void post(String url) {
get(url, "POST");
}
}
应用程序将此类归为一类:
class MyHTTPRequest extends HTTPRequest {
void get(String url, String method = "GET") {
requestCounter++;
super.get(url, method);
}
}
一切都很好,但是使用SDK的人认为将方法传递给get
它很愚蠢,并使接口更好,确保强制实现向后兼容性。
class HTTPRequest {
@Deprecated
void get(String url, String method) {
request(url, method);
}
void get(String url) {
request(url, "GET");
}
void post(String url) {
request(url, "POST");
}
void request(String url, String method);
}
一切似乎都很好,直到上面的应用程序用新的SDK重新编译为止。突然,重写的get方法不再被调用,并且请求也没有被计数。
这称为脆弱的基类问题,因为看似无害的更改会导致子类破裂。任何时候在类内部调用方法的更改都可能导致子类中断。这往往意味着几乎任何更改都可能导致子类中断。
Final阻止任何人继承您的班级。这样,可以更改类中的哪些方法,而不必担心有人会完全依赖于哪个方法调用。
Final有效地意味着您的类将来可以安全地更改,而不会影响任何基于下游继承的类(因为没有),或者有关该类的线程安全的任何问题(我认为在某些情况下,字段上的final关键字会阻止一些基于线程的高Jinx)。
Final意味着您可以自由地更改类的工作方式,而不会在行为上发生任何意外更改,而这些更改会以其他人的代码为依托。
例如,我编写了一个名为HobbitKiller的类,这很不错,因为所有的霍比特人都是捣蛋鬼,应该死了。从头开始,他们肯定都必须死。
您可以将其用作基类,并添加一种很棒的新方法来使用喷火器,但可以将我的类用作基类,因为我有一种针对霍比特人的好方法(除了技巧十足之外,它们都很快捷),这你用来帮助瞄准你的喷火器。
三个月后,我更改了定位方法的实现。现在,在将来某个未知的升级库的时候,由于所依赖的(通常是不可控制的)超类方法的变化,类的实际运行时实现已发生根本性的变化。
因此,对我来说,要成为一名认真的开发人员,并确保在使用我的课程时将霍比特人的命运平稳地带到未来,我必须非常非常谨慎地对待任何可扩展的课程进行任何更改。
通过消除扩展的能力(除非我特别打算扩展课程),我为自己(以及希望其他人)省去了很多麻烦。
final
使用final
绝不违反SOLID原则。不幸的是,将开放/封闭原则(“软件实体应该开放以进行扩展,而封闭以进行修改”)解释为“而不是修改类,为其子类化并添加新功能”,这是极为普遍的。这并不是它最初的含义,通常被认为不是实现其目标的最佳方法。
遵守OCP的最佳方法是,通过专门提供抽象行为来设计类中的扩展点,这些抽象行为通过向对象注入依赖项来参数化(例如,使用“策略”设计模式)。这些行为应设计为使用接口,以便新的实现不依赖于继承。
另一种方法是使用其公共API作为抽象类(或接口)来实现您的类。然后,您可以产生可以插入相同客户端的全新实现。如果您的新界面需要与原始界面大致相似的行为,则可以:
对我来说,这是设计问题。
假设我有一个计算员工工资的程序。如果我有一个班级返回基于国家的两个日期之间的工作日数(每个国家/地区一个班级),则将其放在最后,并为每个企业提供一种仅为其日历提供免费工作日的方法。
为什么?简单。假设某个开发人员想要在WorkingDaysUSAmyCompany类中继承基类WorkingDaysUSA并对其进行修改,以反映出他的企业将因罢工/维护/无论火星2号为何而关闭。
客户订单和交付的计算将反映延迟并在运行时调用WorkingDaysUSAmyCompany.getWorkingDays()时进行相应的工作,但是当我计算休假时间时会怎样?我应该将火星2号作为每个人的假期吗?不会。但是由于程序员使用继承,而我没有保护类,因此可能导致混乱。
或者说,他们继承并修改了班级,以反映该公司在星期六不工作,而在该国,星期六在该国工作一半。然后,地震,电力危机或某些情况使总统宣布像最近在委内瑞拉发生的那样三个工作日不工作。如果继承类的方法每个星期六都已减去,则我对原始类的修改可能导致同一天减去两次。我将不得不转到每个客户端上的每个子类,并验证所有更改是否兼容。
解?将类设置为final,并提供一个addFreeDay(companyID mycompany,Date freeDay)方法。这样,您可以确保在调用WorkingDaysCountry类时,它是您的主类而不是子类
final
?许多人(包括我在内)发现,使每个非抽象类都成为一个很好的设计final
。