学习一个语言的语法,可能只需要一两天,然后就具备了搬砖的能力。但是如何做出可扩展、易维护、好管理的工程代码,还需要再补充一些知识和经验。本次先来看看设计原则。
“好的功能设计,在迭代开发时,不需要改动大量的代码,就像火车加一节车厢,插线板支持拔插各种插头,签字笔更换笔芯”
1.单一职责原则
单一指责原则:规定一个类应该只有一个发生变化的原因(职责)。
反例:
public class AnimalServiceImpl {
public void say(String animalType){
if("小狗".equals(animalType)){
System.out.println("汪汪汪");
} else if("小猫".equals(animalType)){
System.out.println("喵喵喵");
}
}
}
一个类承担包含多种不同的行为,但是在后续新增或变动需求的时候,继续在类上扩展添加逻辑,就会变得非常臃肿。
修改:
public interface AnimalService {
void say(String animalType);
}
public class DogServiceimpl implements AnimalService{
@Override
public void say(String animalType) {
System.out.println("汪汪汪");
}
}
public class CatServiceimpl implements AnimalService{
@Override
public void say(String animalType) {
System.out.println("喵喵喵");
}
}
定义动物的上层接口,将需要做的事抽象出来。猫和狗分别实现这个接口,互相不干扰,当新增一种动物的时候,也会比较方便,可读性也大大提升。
2.开闭原则
开闭原则:在面向对象领域中,规定软件中的对象、类、模块和函数对扩展应该是开放的,但对于修改是封闭的。通过这个原则,要求用抽象定义结构,用具体实现细节(即,面向抽象编程)。
主要的实现思路就是:继承父类,扩展需要的方法,同时保留原有的方法,新增自己需要的方法。这样一来,既扩展的方法,又保留的原有方法。
例如下面计算面积的类,提供了多种形状面积的计算方法
import cn.bugstack.design.ICalculationArea;
/**
* Create by 小傅哥(fustack) @2020
*/
public class CalculationArea implements ICalculationArea {
private final static double π = 3.14D;
public double rectangle(double x, double y) {
return x * y;
}
public double triangle(double x, double y, double z) {
double p = (x + y + z) / 2;
return Math.sqrt(p * (p - x) * (p - y) * (p - z));
}
public double circular(double r) {
return π * r * r;
}
}
在这个类已经在工程中普遍使用的情况下,如果需要修改π的精度,在不考虑开闭原则的情况下,可以直接修改π的值。
开闭原则的目的是不能因为个例需求的变化,而改变预定的实现类,除非预定的实现类有错误。
所以此时可以,新建一个类继承CalculationArea,扩展需要的方法,保留原有方法。
/**
* Create by 小傅哥(fustack) @2020
*/
public class CalculationAreaExt extends CalculationArea {
private final static double π = 3.141592653D;
@Override
public double circular(double r) {
return π * r * r;
}
}
3.里氏替换原则
**里氏替换原则:**如果B是A的子类,那么所有B类型的对象都可以在不破坏程序的情况下被A类型的对象替换。 即,子类可以扩展父类的功能,但不能改变父类原有的功能==子类尽量不要重写父类的方法。
含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
- 子类可以增加自己特有的方法;
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松;
- 当子类的方法实现父类的方法(重写、重载或实现抽象方法)时,方法的后置条件(即方法的输出或返回值)要比父类的方法更严格或者与父类的方法相等。
作用:
- 里氏替换原则是实现开闭原则的重要方式之一;
- 解决了继承中重写父类造成的可复用性变差的问题;
- 是动作正确的保证,即类的扩展不会给已有的系统引入新的错误,降低出错的可能性;
- 加强程序的健壮性,在变更时可以做到很好的兼容性。
反例:
如果不按照原则,随意重写父类的方法,最直接的影响就是会导致子类不再具备父类的功能。无法达到继承扩展且复用的目的。
例如上面计算面积的例子,如果子类在调用circular()方法的时候,始终都是高精度下的计算,无法再回到父类的低精度。
public class CalculationAreaExt extends CalculationArea {
private final static double π = 3.141592653D;
/**
* 扩展高精度之后的面积计算
*/
public double circularExt(double r) {
return π * r * r;
}
}
这样一来,子类既保留了父类低精度的计算,也扩展了高精度的计算。
4.迪米特法则原则
迪米特法则原则(最少知道原则):指一个对象类对于其他对象类来说,知道的越少越好。两个类之间,不要有过多的耦合关系,保持最少的关联性。
这个比较容易理解,例如存在学生、班主任老师、校长三级的对象关系,如果在校长类中,直接提供对学生信息的操作,就违背了迪米特法则,代码会变得臃肿。
可以一级一级的对象操作,教师对学生信息进行操作,校长通过调用教师提供的方法,获得学生的统计信息。
5.接口隔离原则
**接口隔离原则:**一个类对另一个类的依赖应该建立在最小的接口上。其目的是为了实现高内聚、低耦合。
在实际中,要求将臃肿庞大的接口,拆分成多个小接口,并有各个服务类实现。当一个大接口中的很多方法不需要被每一个类都要实现时,这样的接口很难维护,也不易扩展,每一次修改验证都有潜在的风险。
拆分的衡量依据:
- 接口应该只服务于一个子模块或业务逻辑,并不是无限小的;
- 为依赖接口的类定制服务,只提供调用者需要的方法,屏蔽不需要的方法;
- 了解环境,拒绝盲从;根据业务逻辑,定制接口拆分的标准;
- 提高内聚,减少对外交互:让接口用最少的方法完成最多的事情。
反例接口:
每个动物实现类要实现多个与自己无关的方法,无法控制外部不能调用,需要单独文档说明。
public interface AnimalSkill {
// 游泳
void swim();
// 飞翔
void fly();
// 打洞
void hole();
}
class EagleSkill implements AnimalSkill{
@Override
public void swim() {
}
@Override
public void fly() {
System.out.println("老鹰会飞");
}
@Override
public void hole() {
}
}
拆解后,只需要实现对应的接口:
public interface ISkillFly {
// 飞翔
void fly();
}
class EagleSkill implements ISkillFly{
@Override
public void fly() {
System.out.println("老鹰会飞");
}
}
6.依赖倒置原则
依赖倒置原则:代码的高层模块不应该依赖于底层模块,二者都应该依赖于抽象。即,抽象不应该依赖于细节,细节应该依赖于抽象。
是实现开闭原则的重要途径之一,降低了类之间的耦合,提高了系统的稳定性和可维护性,同时这样的代码一般更易读。
例如:小黎喂养自己的多种宠物,我们可以抽象出喂养动作,然后面向这个抽象做不同的实现。
反例:
class FeedControl {
// 喂狗狗
void feedDog(){
System.out.println("喂狗狗");
}
// 喂猫猫
void feedCat(){
System.out.println("喂肉球");
}
}
在一个类中,使用多个方法实现。
符合依赖倒置原则:
interface IFeedControl {
// 喂的抽像
void feed();
}
class DogFeed implements IFeedControl{
@Override
public void feed() {
System.out.println("喂狗狗");
}
}
class CatFeed implements IFeedControl{
@Override
public void feed() {
System.out.println("喂肉球");
}
}
这样一来任何一种抽象都可以有自己的实现类,既可以不断完善,也可以新增。当小黎之后,养其他宠物时,也方便扩展。
小结
名字 | 简述 |
---|---|
单一职责原则 | 规定一个类应该只有一个发生变化的原因(职责) |
开闭原则 | 规定软件中的对象、类、模块和函数对扩展应该是开放的,但对于修改是封闭的 |
里氏替换原则 | 子类可以扩展父类的功能,但不能改变父类原有的功能 |
迪米特法则原则 | 指一个对象类对于其他对象类来说,知道的越少越好 |
接口隔离原则 | 一个类对另一个类的依赖应该建立在最小的接口上 |
依赖倒置原则 | 代码的高层模块不应该依赖于底层模块,二者都应该依赖于抽象 |
通过对设计原则的了解,也就明晰了设计模式的一些主要思想,设计模式是对这些原则的各种实现。
文献:《重学java设计模式》