软件设计七大原则
1、开闭原则
-
开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
-
用抽象构建框架,用实现扩展细节。
-
优点: 提高软件系统的可复用性及可维护性。
不要改你以前写的代码,你应该加一些代码去扩展原来的功能,来实现新的需求。好处很好理解,改原来的代码很容易影响原来的功能,特别是接手别人的代码,不理解原先业务场景的情况下。
新添加校验规则的话,尽量不要修改以前的校验规则。可以回引入新的风险。
具体代码实现:

举例: 比如有一个课程抽象类,该课程包含课程id,名称,价格。同时开放的课程有java课程,C语言课程。上面的UML图就是两者之间的关系。
如果在此基础上需要添加优惠课程。那么我们有两种实现方式。一种就是直接在抽象接口中声明一个优惠获得价格的方法。另一种就是在java课程中直接修改获取价格的方法。
但是这两种方式都会存在问题。前者的问题在于修改的地方太多,接口是不应该经常变化的,尤其是在有很多实现类的前提下,这样修改会让我们修改很多实现类。后者的问题在于我们就无法获取原来的价格。不符合需求。
那么究竟怎样是合理的呢?
如果只是java课程打折,那么我们最好使用继承的方式,对原来的java课程进行扩展。

尽量不对底层的方法进行修改,防止风险的扩散。越高层的影响越小,提高了复用性。
2、依赖倒置原则
-
定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象。
-
抽象不应该依赖细节;细节应该依赖抽象。
-
针对接口编程,不要针对实现编程。
-
优点: 可以减少类间的耦合性,提高系统稳定性,提高代码可读性和可维护性,可降低修改程序造成的风险。

上图是典型的面向实现编程,一旦我们需要添加新的课程就需要修改该类,改动是很频繁的。这里我们引入抽象。然后实现接口的方式。在应用层通过参数注入的方式调用具体实现类。

当然这样会有一个问题,我们在Mical类中只能注入一次ICourse的实例。那么为了多次调用,需要将ICourse暴露出去。

3、单一职责原则
-
单一职责原则:不要存在多于一个导致类变更的原因。
-
一个类、接口、方法只负责一项职责。
-
优点: 降低类的负责度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。
例如声明一个鸟类,方法为行动的方式
public class Bird {
public void mainMoveMode(String birdName) {
System.out.println(birdName+ "用翅膀飞");
}
}
在实际调用时,属于鸟类的有大雁,企鹅,鸵鸟。那么其实企鹅和鸵鸟其实不是使用翅膀的,这就出现了需求不一致的问题。我们一般的做法就是根据传入的参数判断,然后输出不同的结果。
public class Bird {
public void mainMoveMode(String birdName) {
if (birdName.equals("鸵鸟")){
System.out.println(birdName + "用脚走");
}else {
System.out.println(birdName + "用翅膀飞");
}
}
}
这样会更改实现的具体流程,不留神就改错了。尤其是对于边界条件比较复杂的处理流程而言。

再比如课程类
public interface Course {
//获取课程信息
String getCourseName();
byte[] getCourseVideo();
//课程管理
void studyCourse();
void refundCourse();
}
在这个类中,我们可以看到获取课程信息和退单是相互影响的。
那么我们可以将其进行拆分。

4、接口隔离原则
-
用多个专门的接口,而不是单一的总接口,客户端不应该依赖它不需要的接口。
-
一个类对一个类的依赖应该建立在最小的接口上。
-
建立单一的接口,不要建立庞大臃肿的接口。
-
尽量细化接口,接口中的方法尽量少。
-
优点: 符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性,可扩展性和可维护性。
image
比如狗是不会飞的,那么我们要实现这个接口,就必须将它的fly方法不予实现。造成了浪费。
我们可以适度的将接口拆分,但不可过于细

5、迪米特法则
- 一个对象应该对其他对象保持最少的了解。又叫做最少知道原则。
- 尽量降低类与类之间的耦合。
- 强调只和朋友交流,不和陌生人说话。
- 朋友: 出现在成员变量,方法的输入,输入参数重的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。
- 优点: 降低类之间的耦合。
防疫站需要每个宠物的信息,这时候需要一个EpidemicStation对象。如何获取宠物信息有2种方式
通过宠物的主人,EpidemicStation早已有了Host对象,而Host中有一系列关于宠物(Animal)的方法
直接new一个宠物的对象,获取相关信息。
我们肯定直接会选择第一种。。。直觉是可以少建个对象。。。早说了我就是这种偷懒的程序猿。。。然而莫名的契合了迪米特法则。
Host只需要对Animal对象保持关注,而EpidemicStation只需要对Host对象保持关注。EpidemicStation对象不需要了解Animal。感觉直接点就是,在一个类中应该少出现其他类,其实这也是解耦的需要。
6、里氏替换原则
- 该原则是在1988年,由麻省理工学院的以为姓里的女士提出的。
- 如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
由定义可知,在使用继承时,遵循里氏替换原则,在子类中尽量不要重写和重载父类的方法。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之一,在给程序设计带来巨大遍历的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
public class JavaCourse implements ICourse {
private Integer id;
private String name;
private Double price;
public JavaCourse( Integer id,String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public Integer getId() {
return this.id;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
@Override
public String toString() {
return "JavaCourse{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
'}';
}
}
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getDisCountPrice(){
return super.getPrice()* 0.8;
}
@Override
public Double getPrice() {
return super.getPrice() ;
}
}
public static void main(String[] args) {
ICourse javaCourse = new JavaDiscountCourse(95,"Java架构设计",128.0);
System.out.println(
"课程Id:"+javaCourse.getId()
+ "课程名称:"+ javaCourse.getName()
+ "课程原格:"+ javaCourse.getPrice()
+ "课程优惠价格:"+ ((JavaDiscountCourse) javaCourse).getDisCountPrice()
);
}
7、合成复用
-
尽量使用对象组合、聚合,而不是继承关系达到软件复用的目的。
-
聚合has-A和组合contains-A
-
优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
举个例子还是会有助于理解哈:比如一个人可以是学生也可以是经理而“学生”和“经理”都可以成为某公司的“雇员”,这里就可以来理解一下Has-A和Is-A了 学生和经理都是人的一种 那么他们与人的关系就是 Is-A 的关系,而 雇员是人的一个属性 所以 雇员与人之间是 Has-A 的关系,如果聚合复用原则来设计的话关系图如下:

145天以来,Java架构更新了 428个主题,已经有91位同学加入。微信扫码关注java架构,获取Java面试题和架构师相关题目和视频。上述相关面试题答案,尽在Java架构中。