第1章 软件架构设计原则
1.1 开闭原则
开闭原则(Open-Closed Principle,OCP)是指一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。它强调的是用抽象构建框架,用实现扩展细节,可以提高软件系统的可复用性及可维护性。开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定、灵活的系统。例如版本更新,我们尽可能不修改源代码,但是可以增加新功能。
/**
* @author WangYifei
* @date 2020-12-03 10:36
* @describe 人的接口,每个人都有id,名称,年龄,抽离出来
*/
public interface IUser {
Integer getId();
String getName();
String getAge();
}
/**
* @author WangYifei
* @date 2020-12-03 10:37
* @describe 一个叫罗永健,他也是一个人,所以实现了用户,会有getId,但是后来发生了变异
* 这个时候怎么办,变异拥有了超能力,但是设计模式原则,对扩展开放,对修改关闭,那么怎么去增加呢?
* Java的继承来了
*/
public class ILuoYongJian implements IUser{
private Integer id;
private String name;
private String age;
ILuoYongJian(Integer id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public Integer getId() {
return id;
}
@Override
public String getName() {
return name;
}
@Override
public String getAge() {
return age;
}
}
/**
* @author WangYifei
* @date 2020-12-03 10:40
* @describe 继承了 罗永健原有的属性,进行一个扩展,新增了获取超能力的能力
*/
public class ISuperLuoYongJian extends ILuoYongJian{
ISuperLuoYongJian(Integer id, String name, String age) {
super(id, name, age);
}
@Override
public String getName() {
return super.getName();
}
public String getSuper() {
return "隐身术";
}
}
通过代码注释可以看到,总共使用了一个接口,两个类。一个是普通人,然后实现了IUser,基本的属性都有,这个时候突然发生了变异拥有了超能力,但是开闭原则是不修改原有的代码,这个好处是,以免版本更新或者新增功能,对原有功能造成了不好的影响,这是一个思想,希望自己以后在编写程序的时候可以立刻想到。
1.2 依赖倒置原则
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。*:要依赖于抽象,不要依赖于具体。简单的说就是面向抽象接口进行编程,衍生出具体的实现,而不是固定写死实现,这样代码耦合性太高,改动一处可能全局崩盘。
/**
* @author WangYifei
* @date 2020-12-03 11:24
* @describe 一个人叫lyj,他喜欢吃西瓜和苹果
*/
public class ILuoYongJian {
public void eatXg() {
System.out.println("吃西瓜");
}
public void eatPg() {
System.out.println("吃苹果");
}
}
/**
* @author WangYifei
* @date 2020-12-03 11:25
* @describe 操作尝试
*/
public class Test {
public static void main(String[] args) {
ILuoYongJian iLuoYongJian = new ILuoYongJian();
iLuoYongJian.eatPg();
iLuoYongJian.eatXg();
}
}
这样的话,就会打印出,吃苹果, 吃西瓜,如果这个B又想吃榴莲怎么办?
那我就要上去修改给ILuoYongJian.java 里面新增一个 eatLl();这样代码耦合性就很高,因为他可能吃很多东西,一会吃这个一会吃那个,现在对实现进行修改。
/**
* @author WangYifei
* @date 2020-12-03 11:23
* @describe 每一个人都会吃
*/
public interface Eat {
void start();
}
因为要吃不同的东西,但是吃这个动作是相同的,单独抽离出来。
/**
* @author WangYifei
* @date 2020-12-03 11:33
* @describe
*/
public class EatPg implements Eat{
@Override
public void start() {
System.out.println("吃苹果");
}
}
之后吃不同的东西,去实现这个吃的接口,我不管你吃什么,反正你就是吃了。
/**
* @author WangYifei
* @date 2020-12-03 11:33
* @describe
*/
public class EatXg implements Eat{
@Override
public void start() {
System.out.println("吃西瓜");
}
}
/**
* @author WangYifei
* @date 2020-12-03 11:24
* @describe 一个人叫lyj,他喜欢吃西瓜和苹果
*/
public class ILuoYongJian {
public void Eat(Eat eat) {
eat.start();
}
}
可以看到,这个时候,lyj只需要调用一个Eat,然后开始里面的吃,不知道吃什么,我吃什么看你传什么,你传什么我吃什么?!不挑食的好孩子。这样每次新增一个,不需要修改原有的代码,符合开闭原则,也减少了吃和对象之间的直接依赖关系,而是各自都面向抽象的Eat.java进行维护和修改。
看一下测试类。
/**
* @author WangYifei
* @date 2020-12-03 11:25
* @describe
*/
public class Test {
public static void main(String[] args) {
ILuoYongJian iLuoYongJian = new ILuoYongJian();
iLuoYongJian.Eat(new EatPg());
iLuoYongJian.Eat(new EatXg());
}
}
第一次喂食了苹果,第二次喂食了西瓜,如果还需要榴莲,那么只需要新增一个吃榴莲的类,去实现Eat,进行喂食就可以了。
1.3 单一职责原则
单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。假设我们有一个类负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能导致另一个职责的功能发生故障。这样一来,这个类就存在两个导致类变更的原因。如何解决这个问题呢?将两个职责用两个类来实现,进行解耦。后期需求变更维护互不影响。这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说,就是一个类、接口或方法只负责一项职责。*: 个人觉得这是一个降低耦合的设计模式,让编码逻辑不相互影响。
/**
* @author WangYifei
* @date 2020-12-03 12:35
* @describe 楼梯,上下楼梯
*/
public class Stairs {
public void go(String operating) {
if (operating.equals("上楼")) {
System.out.println("上楼");
} else if (operating.equals("下楼")) {
System.out.println("下楼");
}
}
}
一个地方有楼房,需要上楼下楼,我传上楼命令就是上楼,传下楼命令就是下楼。
/**
* @author WangYifei
* @date 2020-12-03 12:37
* @describe
*/
public class Test {
public static void main(String[] args) {
Stairs stairs = new Stairs();
stairs.go("上楼");
stairs.go("下楼");
}
}
但是,如果我上楼和下楼换成了自动电梯,观光梯,但是可能是不同厂商的,效果不同速度不同,设置不同,逻辑不同等等...这个时候,我改了一个可能对下楼有影响,最主要的代码不好维护,不易检查,因为负责多种电梯的效果。
改造一下:
/**
* @author WangYifei
* @date 2020-12-03 12:35
* @describe 楼梯,上下楼梯
*/
public interface Stairs {
void go();
}
把都有的操作,比如上楼下楼,封装成一个接口。
/**
* @author WangYifei
* @date 2020-12-03 12:40
* @describe
*/
public class ILyjStairs implements Stairs{
@Override
public void go() {
System.out.println("lyj牌电梯,我速度快");
}
}
lyj牌电梯,速度快,等一系列逻辑操作,都放在这,比如给电梯再加上关的操作就显而易见了。
/**
* @author WangYifei
* @date 2020-12-03 12:40
* @describe
*/
public class IWyfStairs implements Stairs{
@Override
public void go() {
System.out.println("wyf牌电梯,我速度慢");
}
}
lyj牌和wyf牌的关闭按钮不在一起,但是共用在一个商场,一个需要蹲着关,一个需要站着关,如果写在一起,那么需要同时关注两种电梯的业务逻辑,个人觉得这也是单一原则的用处。
1.4 接口隔离原则
/**
* @author WangYifei
* @date 2020-12-03 12:48
* @describe 行为接口类
*/
public interface Behavior {
// 游泳
void swim();
// 飞翔
void fly();
// 吃
void eat();
}
先拟定一个 行为接口类,里面有游泳,飞翔,和吃。
/**
* @author WangYifei
* @date 2020-12-03 12:49
* @describe 小鸟类
*/
public class Bird implements Behavior{
@Override
public void swim() {
}
@Override
public void fly() {
}
@Override
public void eat() {
}
}
/**
* @author WangYifei
* @date 2020-12-03 12:49
* @describe 狗类
*/
public class Dog implements Behavior{
@Override
public void swim() {
}
@Override
public void fly() {
}
@Override
public void eat() {
}
}
再看两个小动物种类,一个是狗,一个是鸟。都实现了行为,因为他们都有行为,那么问题来了?狗能飞吗?鸟能游泳吗?可能有的鸟可以= =,就造成了不必要的重写,那么就将这些行为抽离出去。
图不好放,我就不放了= =太麻烦,看一下实现类吧。
/**
* @author WangYifei
* @date 2020-12-03 12:49
* @describe 小鸟类
*/
public class Bird implements EatBehavior,FlyBehavior{
@Override
public void eat() {
}
@Override
public void fly() {
}
}
这个是小鸟的,利用java的多接口实现,这个是小鸟的,小鸟可以吃可以飞,但是不能游泳。
/**
* @author WangYifei
* @date 2020-12-03 12:49
* @describe 狗类
*/
public class Dog implements EatBehavior,SwimBehavior{
@Override
public void eat() {
}
@Override
public void swim() {
}
}
小狗的,小狗可以吃,可以游泳,但是不能飞。接口隔离原则还是挺好理解的吧= =这样就每个接口隔离开来,不需要全部重写。
1.5 迪米特原则
迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称为成员朋友类,而出现在方法体内部的类不属于朋友类。
/**
* @author WangYifei
* @date 2020-12-03 13:00
* @describe
*/
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader) {
List<String> courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new String());
}
teamLeader.checkNumberOfCourses(courseList);
}
}
老板类
/**
* @author WangYifei
* @date 2020-12-03 13:01
* @describe
*/
public class TeamLeader {
public void checkNumberOfCourses(List<String> courseList) {
System.out.println("这个人目前的课程数量:" + courseList.size());
}
}
员工类
看一下调用类:
/**
* @author WangYifei
* @date 2020-12-03 13:04
* @describe
*/
public class Test {
public static void main(String[] args) {
TeamLeader teamLeader = new TeamLeader();
Boss boss = new Boss();
boss.commandCheckNumber(teamLeader);
}
}
这个人目前的课程数量:20(这个是结果)
看起来没什么问题, 这个员工有20个课或者任何你好理解的形式,比如这个手下手里有20个员工。
我需要知道我这个手下有多少员工?那你手里的员工跟我什么关系?你要你是干嘛的,要你统计的,你不统计,你让我去数你手底下的人?那你手底下的人的手底下的人呢?我再去数?我不需要跟你手底下的人打招呼,你自己数好跟我说就行。*:迪米特原则:跟我没关系的就不要让我去操作。
修改之后的:
/**
* @author WangYifei
* @date 2020-12-03 13:01
* @describe
*/
public class TeamLeader {
public void checkNumberOfCourses() {
List<String> courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new String());
}
System.out.println("这个人目前的课程数量:" + courseList.size());
}
}
可以看到,现在数员工的活(for循环模拟数员工)交给了,自己的手下去办了。
/**
* @author WangYifei
* @date 2020-12-03 13:00
* @describe
*/
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader) {
teamLeader.checkNumberOfCourses();
}
}
而上司只需要得到一个结果,当然结果也是20,但是过程不一样了,我不需要管你的业务逻辑了。只需要指派我要查看哪一个手下的手里有多少人。
/**
* @author WangYifei
* @date 2020-12-03 13:04
* @describe
*/
public class Test {
public static void main(String[] args) {
TeamLeader teamLeader = new TeamLeader();
Boss boss = new Boss();
boss.commandCheckNumber(teamLeader);
}
}
1.6 里氏替换原则
里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为T1的对象o1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
这个定义看上去还是比较抽象的,我们重新理解一下。可以理解为一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,引申含义为:子类可以扩展父类的功能,但不能改变父类原有的功能。
(1)子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
(2)子类可以增加自己特有的方法。
(3)当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
(4)当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样。
通俗一点解释:*:子类的操作不能影响父类的操作,重载但是不能影响父类,不能重写。
使用里氏替换原则有以下优点:
(1)约束继承泛滥,是开闭原则的一种体现。
(2)加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的可维护性和扩展性,降低需求变更时引入的风险。
1.7 合成复用原则
合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/聚合(contanis-a)而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/聚合(contanis-a)而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
继承叫作白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合称为黑箱复用,我们是无法获取到类以外的对象的实现细节的。虽然我们要根据具体的业务场景来做代码设计,但也需要遵循OOP模型。以数据库操作为例先来创建DBConnection类::
/**
* @author WangYifei
* @date 2020-12-03 13:20
* @describe
*/
public class DBConnection {
public String getConnection() {
return "Mysql 数据库连接";
}
}
创建ProductDao类:
/**
* @author WangYifei
* @date 2020-12-03 13:21
* @describe
*/
public class ProductDao {
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct(){
String conn = dbConnection.getConnection();
System.out.println("使用" + conn + "增加产品");
}
}
这就是一种非常典型的合成复用原则的应用场景。但是,就目前的设计来说,DBConnection还不是一种抽象,不便于系统扩展。目前的系统支持 MySQL 数据库连接,假设业务发生变化,数据库操作层要支持Oracle数据库。当然,我们可以在DBConnection中增加对Oracle数据库的支持,但是这违背了开闭原则。其实,我们可以不修改 Dao 的代码,而将 DBConnection 修改为“abstract”的,来看代码:
/**
* @author WangYifei
* @date 2020-12-03 13:20
* @describe
*/
public abstract class DBConnection {
public abstract String getConnection();
}
/**
* @author WangYifei
* @date 2020-12-03 13:26
* @describe
*/
public class MysqlConnection extends DBConnection{
@Override
public String getConnection() {
return "Mysql连接";
}
}
/**
* @author WangYifei
* @date 2020-12-03 13:26
* @describe
*/
public class OracleConnection extends DBConnection{
@Override
public String getConnection() {
return "Oracle数据库连接";
}
}
ProductDao没有变,具体的选择哪一个数据库,就传入哪一款数据库。调用测试。
/**
* @author WangYifei
* @date 2020-12-03 13:27
* @describe
*/
public class Test {
public static void main(String[] args) {
ProductDao productDao = new ProductDao();
DBConnection dbConnection = new MysqlConnection();
productDao.setDbConnection(dbConnection);
productDao.addProduct();
}
}
感觉无形之中,用到了以前了解过的抽象工厂模式和单一职责原则。
这一串代码,就选用了mysql作为数据库产品进行增加。
1.8 设计模式总结
学习设计原则是学习设计模式的基础。在实际开发过程中,并不要求所有代码都遵循设计原则,我们要考虑人力、时间、成本、质量,不能刻意追求完美,但要在适当的场景遵循设计原则,这体现的是一种平衡取舍,可以帮助我们设计出更加优雅的代码结构。
大佬又发文章了 好屌