开闭原则
开闭原则的英文是 Open Closed Principle,缩写为 OCP。
开闭原则说的是:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的。
-
对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。
-
对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。
两者结合起来表述为:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
这里我们以出售电脑为例,首先定义一个顶层接口Computer,然后定义两个实现类,华硕电脑与苹果Mac,类层次结构如下图所示:
上面是我们一开始的需求,但是随着软件发布运行,我们的需求不可能一成不变,肯定要接轨市场。假设现在是双十一,华硕笔记本电脑需要搞促销活动。那么我们的代码肯定要添加新的功能。可能有些刚入职的新人会在原有的代码上做改动,这肯定不符合开闭原则,虽然这种做法最直接,也最简单,但是绝大部分项目中,一个功能的实现远比想像要复杂的多,我们在原有的代码中进行修改,其风险远比扩展和实现一个方法要大的多。正确的做法可以这样:
我们实现一个关于折扣的子类,其中包含一个关于折扣的方法,这方法相当于一个扩展方法。可以看到这个子类是AsusComputer的,那为什么不把他设计成一个共用的折扣类呢,比如DiscountComputer,所有实现类都继承这个折扣类。这是因为每种实现类的折扣方案可能是不一样的。所以我们最好能把它作为每个实现类的子类单独实现。如果你能确保你的业务中的新功能能兼容所有相关联的需求你也可以共用一个。
小结一下开闭原则就是:
**核心思想:**尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
**通俗来讲:**一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
里氏替换原则
里氏替换原则由Barbara Liskov提出,这个原则很明显,Java的多态或者C++的虚函数本身就允许把指向基类的指针或引用,在调用其方法或函数的时候,调用实际类型的方法或函数。我们来看一个简单的例子:Circle 和 Square 继承了基类 Shape,然后在应用的方法中,根据输入 Shape 对象类型进行判断,根据对象类型选择不同的绘图函数将图形画出来。
void drawShape(Shape shape) {
if (shape.type == Shape.Circle ) {
drawCircle((Circle) shape);
} else if (shape.type == Shape.Square) {
drawSquare((Square) shape);
} else {
……
}
}
这种写法的代码既常见又糟糕,它同时违反了开闭原则和里氏替换原则。
-
首先看到这样的 if/else 代码,就可以判断违反了(我们刚刚在上个部分讲过的)开闭原则:当增加新的 Shape 类型的时候,必须修改这个方法,增加 else if 代码。
-
其次也因为同样的原因违反了里氏替换原则:当增加新的Shape 类型的时候,如果没有修改这个方法,没有增加 else if 代码,那么这个新类型就无法替换基类 Shape。
要解决这个问题其实也很简单,只需要在基类 Shape 中定义 draw 方法,所有 Shape 的子类,Circle、Square 都实现这个方法就可以了:
public abstract Shape{
public abstract void draw();
}
上面那段 drawShape() 代码也就可以变得更简单:
void drawShape(Shape shape) {
shape.draw();
}
这段代码既满足开闭原则:增加新的类型不需要修改任何代码。也满足里氏替换原则:在使用基类的这个方法中,可以用子类替换,程序正常运行。
小结一下里氏替换原则就是:
**核心思想:**在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类。
**通俗来讲:**只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。
**好处:**增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行。
需注意:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。
接口隔离原则
接口隔离原则的英文是 SInterface Segregation Principle,缩写为 ISP。这个原则是说:客户端不应该强迫依赖它不需要的接口。
我们在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。举一个简单的例子:
public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}
public interface RestrictedUserService {
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}
public class UserServiceImpl implements UserService, RestrictedUserService {
// …省略实现代码…
}
删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,所以这个接口只限于给后台管理系统使用。如果我们把它放到 UserService 中,那所有使用到 UserService 的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。
参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外一个接口RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台管理系统来使用。
小结一下接口隔离原则就是:
**核心思想:**类间的依赖关系应该建立在最小的接口上
**通俗来讲:**建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
**需注意:**接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情为依赖接口的类定制服务。只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
依赖倒置原则
依赖倒置原则的英文是 Dependency Inversion Principle,缩写为 DIP。依赖倒置原则说的是:高层模块不依赖低层模块,它们共同依赖同一个抽象,这个抽象接口通常是由高层模块定义,低层模块实现。同时抽象不要依赖具体实现细节,具体实现细节依赖抽象。高层模块就是调用端,低层模块就是具体实现类,抽象就是指接口或抽象类,细节就是实现类。
来看一个简单的例子:假设我们要设计一个很简单的程序,将键盘的输入输出到打印机上。一个简单的设计的程序结构图如下所示。
上面的设计中有三个模块,Copy模块调用Read Keyboard模块来读取输出,然后Copy调用Write Printer模块输出字符。Read Keyboard和Write Printer是两个下层模块,并且很容易被复用。
然而我们的Copy模块却不能被复用于任何不包含键盘和打印机的场景中,而Copy恰恰是这个程序的业务逻辑所在的模块,也是我们最希望能够复用的。
比如,我们还希望将键盘的输入,复制到磁盘文件。我们当然希望复用Copy模块,而事实上,Copy依赖于键盘和打印机,缺一不可,所以不能被复用。我们也可以往Copy中增加一个if条件来支持新的磁盘文件输出,但是这就违背了开闭原则,最终随着功能的变多,代码将变得不可维护。
这个例子中的问题其实是高层级的模块(Copy模块)依赖于层级的模块(Read Keyboard和Write Printer);如果能够找到一个让Copy独立于它所控制的底层级模块的方法,那么我们可以自由地复用这个Copy模块。下图就是一种依赖反转的解决方案。
在这个新的设计中,我们的Copy模块有一个抽象的Reader和一个抽象的Writer。Copy不再直接依赖于具体的实现,不管有几个Reader或Writer的实现,我们都不需要修改Copy。
小结一下依赖倒置原则就是:
**核心思想:**高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;
**通俗来讲:**依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,互不影响,实现模块间的松耦合。
**好处:**依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。
总结
今天的内容一句话概括就是:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;接口隔离原则告诉我们在设计接口的时候要精简单一;依赖倒置原则告诉我们要面向接口编程。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
在实际开发过程中,并不是一定要求所有代码都遵循设计原则,我们要考虑人力、时间、成本、质量,不是刻意追求完美,要在适当的场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更加优雅的代码结构。
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
链图片转存中…(img-qieEoq1n-1714955777724)]
[外链图片转存中…(img-MUUDw1gc-1714955777724)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!