盘点设计模式的六大设计原则(SOLID)

前言

软件设计中通常有很多的设计模式,设计模式是软件开发中面对某一类型问题的通用解决方案,这些解决方式是由于有经验的开发人员在软件开发的多年经验中整理、总结出来的,设计模式的目的是为了让代码提升代码的可读性、可扩展性、可维护性,以及提供代码的复用率,从而提升代码的整体稳定性。而设计模式通常需要遵循一些设计原则,在设计原则的基础之上衍生出了各种各样的设计模式。设计原则是设计要求,设计模式是设计方案,使用设计模式的代码则是具体的实现。

 

一、设计模式六大设计原则

设计模式中主要有六大设计原则,简称为SOLID,是由于各个原则的首字母简称合并的来,六大设计原则分别如下:

1、单一职责原则(Single Responsibitity Principle)

2、开放封闭原则(Open Close Principle)

3、里氏替换原则(Liskov Substitution Principle)

4、接口分离原则(Interface Segregation Principle)

5、依赖倒置原则(Dependence Inversion Principle)

6、迪米特法则(Law Of Demter)

 

二、设计原则详细描述

2.1、单一职责原则

定义:当需要修改某个对象时,原因有且只有一个;每个类或方法的职责只有一个

一个类或方法只有一个职责,职责单一就可以职责解耦。当一个类或方法的职责越多,被复用的概率就越小,且当某个职责发生变化时很容易会引起其他职责受影响。

优点:

1、代码简单,可读性高,可扩展性强

2、代码复杂度降低,且职责解耦,降低某个职责变化导致影响其他职责的风险

3、代码可复用性增高;

案例如下:

业务场景:实现一个用户注册的Http接口

破坏单一原则示范:

 1 public class UserController {
 2 
 3     /** 用户注册*/
 4     @RequestMapping(value = "regist")
 5     public String userRegist(String userName, String password) throws SQLException {
 6         /** 第一步:校验参数*/
 7         if(StringUtils.isEmpty(userName)||StringUtils.isEmpty(password)){
 8             return "error";
 9         }
10         /** 第二布:获取数据库连接*/
11         Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","user","password");;
12         /** 第三步:构造SQL语句*/
13         String  sql = "";
14         PreparedStatement preparedStatement = connection.prepareStatement(sql);
15         /** 第四布:执行SQL*/
16         preparedStatement.execute();
17         return "success";
18     }
19 }

 

 

定义了一个UserController类定义了一个用户注册的接口,并且在该方法中完成了用户注册时校验和执行注册SQL的完成流程,初步看上去是没有什么问题,但是该方法userRegist中完成了全部流程的具体实现,就会出现几个问题:

1、代码比较臃肿,代码复用性不高,新增用户登录、查询用户等功能时,还是需要获取数据库连接,构造SQL语句,执行SQL的过程;

2、职责高耦合,用户注册的逻辑和数据库的执行逻辑耦合度过高;

3、职责变化风险过大,当数据库发生变化时,就会导致整个用户注册功能都需要发生变化,变化的成功过大;

 

遵循单一原则示范:

 1 public class UserController {
 2 
 3     @Resource
 4     private UserService userService;
 5 
 6     /** 用户注册*/
 7     @RequestMapping(value = "regist")
 8     public String userRegist(String userName, String password) throws SQLException {
 9         /** 第一步:校验参数*/
10         checkParam(userName, password);
11         /** 第二布:保存用户*/
12         userService.addUser(userName, password);
13         return "success";
14     }
15 
16     private void checkParam(String... params) {
17 
18     }
19 }

 

1 public interface UserService {
2 
3     public void addUser(String userName, String password);
4 
5 }

 

将参数校验逻辑抽离成单独的方法,当需要验证更多的参数时只需要修改checkParam方法即可;将保存用户的逻辑交给UserService去实现,UserController不需要关系UserService是将用户信息保存到哪一个数据库中,将职责分给UserService去具体实现。

这样无论是参数变化还是数据库层发生变化,接口层都无需发生任何变化。每个方法都只需要完成自己单独的职责即可。

 

2.2、开放封闭原则

定义:对扩展开放,对修改关闭

对扩展开放和对修改关闭表示当一个类或一个方法有新需求或者需求发生改变时应该采用扩展的方式而不应该采用修改原有逻辑的方式来实现。因为扩展了新的逻辑如果有问题只会影响新的业务,不会因为老业务;而如果采用修改的方式,很有可能就会影响到老业务受影响。开闭原则是所有设计模式的最核心目标,也是最难实现的目标,但是所有的软件设计都应该以开闭原则当作标准,才能使软件更加的稳定和健壮。

优点:

1、新老逻辑解耦,需求发生改变不会影响老业务的逻辑

2、改动成本最小,只需要追加新逻辑,不需要改的老逻辑

3、提供代码的稳定性和可扩展性

案例如下:

业务场景:现有一个将Date类型转化成年月日字符串格式的工具类如下:

1 /** 将Date转化成年-月-日格式的字符串*/
2     public static String formatDate(Date date){
3         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
4         return format.format(date);
5     }

 

现在需求发生改变,需要在年月日的基础上添加时分秒

破坏开闭原则示范一:

1 /** 将Date转化成年-月-日 时:分:秒格式的字符串*/
2     public static String formatDate(Date date){
3         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
4         return format.format(date);
5     }

 

直接将格式替换成年月日时分秒的格式,虽然改动最快,但是涉及范围最大,因为这个工具类可以已经被多个地方调用了,而部分调用方的需求还是要年月日歌手的,这样就会导致其他业务受到了影响

破坏开闭原则示范二:

1 /** 将Date转化成指定格式的字符串*/
2     public static String formatDate(Date date, String dateFormat){
3         SimpleDateFormat format = new SimpleDateFormat(dateFormat);
4         return format.format(date);
5     }

 

将时间格式采用参数传入的方式,将Date类型转换成指定格式的字符串,虽然这样更灵活也不会影响其他业务逻辑变化,但是就会导致其他业务的代码需要发生变化,假设这个方法已经被100个地方调用了,那么就需要将100个调用方都一一修改增加一个参数,很显然修改代码的成功非常高,而代码修改的越多,出问题的风险就越高。

遵循开闭原则示范:

 1 /** 将Date转化成年-月-日格式的字符串*/
 2     public static String formatDate(Date date){
 3         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
 4         return format.format(date);
 5     }
 6 
 7     /** 将Date转化成年-月-日 时:分:秒格式的字符串*/
 8     public static String formatTime(Date date){
 9         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
10         return format.format(date);
11     }

 

保留原有方法的同时,新增将Date转换成年月日时分秒格式的方法,这样虽然代码变多了,但是不会导致老业务发生改变,风险最小

2.3、里氏替换原则

定义:一个子类实例在任何时刻都可以替换父类实例,从而形成IS-A关系

所有引用基类的地方都可以被必须透明的被子类所替换,也就是说子类可以在任何时候替换父类的对象,并且不会引起程序执行结构的变化。

表示在继承关系中,子类可以重写父类的抽象方法,对于父类已经实现了的方法,子类不应该重写;子类相当于父类而言只应该扩展业务,不应该修改父类的业务,否则就不是一个正确的继承关系。

优点:

1.提高了代码的复用率

2.使继承关系更完整,避免丢失父类的特性

案例如下:

业务场景:给两个用户类型进行排序,父类逻辑是按用户ID进行排序,逻辑如下:

 1 public class UserCompare {
 2 
 3     private User user;
 4 
 5     public int compareUser(User otherUser){
 6         if(user.getUserId() > otherUser.getUserId()){
 7             return -1;
 8         }else if(user.getUserId() < otherUser.getUserId()){
 9             return 1;
10         }else {
11             return 0;
12         }
13     }
14 
15     public User getUser() {
16         return user;
17     }
18 
19     public void setUser(User user) {
20         this.user = user;
21     }
22 }

 

此时当用户有了身份,比如学生,需要根据用户的年龄来进行排序,此时衍生学生比较子类StudentCompare继承之UserCompare

破坏里氏替换原则:

 1 public class StudentCompare extends UserCompare{
 2 
 3     @Override
 4     public int compareUser(User otherUser){
 5         if(user.getAge() > otherUser.getAge()){
 6             return -1;
 7         }else if(user.getAge() < otherUser.getAge()){
 8             return 1;
 9         }else {
10             return 0;
11         }
12     }
13 }

 

这里直接将父类的方法进行重写,虽然可以满足需求,但是会导致父类的逻辑丢失,如果此时再需要根据用户的ID进行排序,通过子类就无法实现。如果将父类对象替换成子类对象,很显然排序的结果就不是预期的结果

遵循里氏替换原则:

 1 public class StudentCompare extends UserCompare{
 2 
 3     public int compareUserByAge(User otherUser){
 4         if(user.getAge() > otherUser.getAge()){
 5             return -1;
 6         }else if(user.getAge() < otherUser.getAge()){
 7             return 1;
 8         }else {
 9             return 0;
10         }
11     }
12 }

 

这里既保留了父类的根据用户ID排序的逻辑,又扩展了新的排序逻辑,此时就可以用子类对象再任何地方替换父类的对象。

2.4、接口分离原则

定义:实现类不应该依赖不需要的接口方法,采用多个专用的接口来替换单个复杂的总接口

客户端实现接口的方法不应该依赖和实现不需要的方法,实现者只需要实现特定的接口方法即可。表示定义接口时应该将不同职责功能定义在不同的接口中,同一个接口中的职责都是一样的,这样才可以低耦合高内聚

优点:

1、规避接口中包含不同职责的方法,每个接口的职责均单一

2、接口之间低耦合,接口本身高内聚,接口的责任划分更清晰

案例如下:

业务场景:

电商系统中的订单模块,需要保存订单、查询订单、订单支付等职责

破坏接口分离原则:

 1 public interface OrderService {
 2 
 3     /** 保存订单 */
 4     public void addOrder();
 5 
 6     /** 查询订单 */
 7     public void queryOrder();
 8 
 9     /** 支付 */
10     public void payOrder();
11 
12     /** 退款*/
13     public void refundOrder();
14 }

 

这里将订单的职责和支付的职责都放在了OrderService接口中,这就需要订单服务的实现者还需要实现和订单模块耦合不高的支付功能,这样就使得订单和支付的耦合度非常高;如果有以下的场景就会导致代码改动非常大

1、订单需要支持积分支付的方式,很显然就不能直接使用payOrder方法了

2、有其他业务模块设计资金业务,也需要调用支付和退款方法时,就需要依赖订单服务的接口,就导致本身不同的业务之间互相耦合

遵循接口分离原则:

public interface OrderService {

    /** 保存订单 */
    public void addOrder();

    /** 查询订单 */
    public void queryOrder();
}
public interface PayService {
    /** 支付 */
    public void pay();

    /** 退款*/
}

 

将接口方法按模块进行拆分,订单服务仅提供和订单本身相关的职责,将支付职责交给支付的服务提供,这样将不同职责的服务完成拆分开,互相之间不会耦合。

 

2.5、依赖倒置原则

定义:高层模块不应该依赖于底层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象

1、高层模块不应该依赖底层模块,二者都应该依赖抽象。

2、抽象不应该依赖细节,细节应该依赖抽象。

3、依赖倒置的中心思想是面向接口编程。

4、依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。

5、使用接口或抽象类的目的是指定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类来完成。

因为抽象改动的概率不大,比较稳定,而实现往往会随着需求的变化而变化,所以抽象的定义不能依赖于实现,否则抽象的稳定性会大打折扣

案例如下:

定义一个给ArrayList和HashSet排序的接口

破坏依赖倒置原则:

1 public interface SortService {
2 
3     /** 将ArrayList进行排序*/
4     public void sortList(ArrayList list);
5 
6     /** 将HashSet进行排序*/
7     public void sortSet(HashSet set);
8 }

 

很明显SortService这个抽象的接口的定义了依赖了ArrayList和HashSet这两个具体的实现,这样的缺点是一旦ArrayList的逻辑有变动就会导致SortService也进行对应的修改;而且这样的设计可复用性不强,因为无法满足其他的List的排序,比如LinkedList

遵循依赖倒置原则: 

1 public interface SortService {
2 
3     /** 将List进行排序*/
4     public void sortList(List list);
5 
6     /** 将Set进行排序*/
7     public void sortSet(Set set);
8 }

 

抽象的集合排序接口定义只定义了两个给List和Set排序的方法,但是没有定义给哪一种List和Set进行排序,这样就可以在实现时实现任何List的实现的排序功能和Set的实现的排序功能。

2.6、迪米特法则

定义:一个对象应该对其他对象有最少的了解,不和陌生对象交流

迪米特法则也叫做最少知道法则(least knowledge Principle),一个类应该尽量少的依赖其他类,降低各个类之间的耦合,类之间耦合度越低,代码的复用性就越高。

迪米特法则中的朋友是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象存在关联、聚合或组合关系,可以直接访问这些对象的方法

优点:

1、降低类之间的耦合度,提高模块的相对独立性。

2、由于亲和度降低,从而提高了类的可复用率和系统的扩展性。

遵循迪米特法则虽然会将两个有关联的类进行解耦,但是往往会伴随一个中介角色来进行互相调用的工作。

案例如下:

业务场景,某集团需要给全公司所有部门的员工发送一份通知

破坏迪米特法则:

 1 public class Company {
 2 
 3     private String companyName;
 4 
 5     private List<Department> departmentList;
 6 
 7     /** 发送通知 */
 8     public void sendNotice(){
 9         for (Department department : departmentList){
10             List<Employee> employeeList = department.getEmployeeList();
11             for(Employee employee : employeeList){
12                 System.out.println("发送通知给:" + employee.getUserName());
13             }
14         }
15     }
16 
17     public String getCompanyName() {
18         return companyName;
19     }
20 
21     public void setCompanyName(String companyName) {
22         this.companyName = companyName;
23     }
24 
25     public List<Department> getDepartmentList() {
26         return departmentList;
27     }
28 
29     public void setDepartmentList(List<Department> departmentList) {
30         this.departmentList = departmentList;
31     }
32 }

 

在这里集团本应该是只需要和部门交互的即可,但是由于需要发送给所有人,便把Employee引入到了Company类中,这样Company就需要依赖Employee,增加了Company和Employee之间的耦合

遵循迪米特法则:

 1 public class Company {
 2 
 3     private String companyName;
 4 
 5     private List<Department> departmentList;
 6 
 7     /** 发送通知 */
 8     public void sendNotice(){
 9         for (Department department : departmentList){
10             department.sendNotice();
11         }
12     }
13 
14     public String getCompanyName() {
15         return companyName;
16     }
17 
18     public void setCompanyName(String companyName) {
19         this.companyName = companyName;
20     }
21 
22     public List<Department> getDepartmentList() {
23         return departmentList;
24     }
25 
26     public void setDepartmentList(List<Department> departmentList) {
27         this.departmentList = departmentList;
28     }
29 }

 

 1 public class Department {
 2 
 3     private String deptName;
 4 
 5     private List<Employee> employeeList;
 6 
 7     public void sendNotice() {
 8         for(Employee employee : employeeList){
 9             System.out.println("发送通知给:" + employee.getUserName());
10         }
11     }
12 
13     public String getDeptName() {
14         return deptName;
15     }
16 
17     public void setDeptName(String deptName) {
18         this.deptName = deptName;
19     }
20 
21     public List<Employee> getEmployeeList() {
22         return employeeList;
23     }
24 
25     public void setEmployeeList(List<Employee> employeeList) {
26         this.employeeList = employeeList;
27     }
28 }

 

这样Company只会和Department交互,而Department负责和Employee交互,这样就去除了Company和Employee之间的依赖关系,降低了耦合度

 

三、总结

设计原则本质上算是一种思想,并没有要求代码设计时必须要按什么的代码规范来进行设计,也并非说完全安全设计原则来进行代码设计就肯定能大幅度提升代码的性能和稳定性。设计原则的主要目的是从代码的可读性、稳定性、可扩展性等方面对代码进行了提升,并且重要是一个思想是代码块之间达到低耦合和高内聚的设计思想。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章