万字总结之设计模式(扫盲篇)

开发 后端
设计模式是对软件设计普遍存在的问题,所提出的解决方案。与项目本身没有关系,不管是电商,ERP,OA 等,都可以利用设计模式来解决相关问题。

[[319068]]

前言

今天我们来看设计模式。话不多说,let's go。

[[319069]]

 

什么是设计模式?

设计模式是对软件设计普遍存在的问题,所提出的解决方案。与项目本身没有关系,不管是电商,ERP,OA 等,都可以利用设计模式来解决相关问题。

当然如果这个软件就只有一小部分人用,并且功能非常简单,在未来可预期的时间内,不会做任何大的修改和添加,即可以不使用设计模式。但是这种的太少了,所以设计模式还是非常重要的。

为什么要使用设计模式?

使用设计模式的最终目的是“高内聚低耦合”。

  • 代码重用性:相同功能的代码,不多多次编写
  • 代码可读性:编程规范性,便于其他程序员阅读
  • 代码可扩展性:当增加新的功能后,对原来的功能没有影响

设计模式的七大原则

设计模式有7大原则,具体如下,即这些不仅是设计模式的依据,也是我们平常编程中应该遵守的原则。

1.单一职责原则

见名知意,我们设计的类尽量负责一项功能,如A类只负责功能A,B类只负责功能B,不要让A类既负责功能A,又负责功能B,这样会导致代码混乱,容易产生bug。

a.未使用单一职责原则

Single类:

  1. public class single {  
  2.    public static void main(String[] args) {   
  3.          Vehicle vehicle = new Vehicle();     
  4.          vehicle.run("汽车");   
  5.          vehicle.run("轮船");     
  6.          vehicle.run("飞机");   
  7.          } 

Vehicle类:

  1. public class Vehicle {    
  2.   void run(String type){   
  3.       System.out.println(type+"在公路上开");   
  4.   } 

运行结果:

 

我们看下运行结果,汽车是在公路上开,但是轮船和飞机并不是在公路上。因为Vehicle类负责了不止一个功能,所以该设计是有问题的。

b.已使用单一职责原则

对于上面的例子,我们采用单一职责原则重写一下,将Vehicle类拆分成三个类,分别是Car,Ship,Plane,让他们各自负责陆地上,水上,空中的交通工具,使其互不影响。如果我们需要对水上交通做“风级大于8级,禁止出海”的限制,就只需要对Ship类进行修改。

single类:

  1. public class single {     
  2.   public static void main(String[] args) {     
  3.     Car car = new Car();     
  4.     car.run("汽车");      
  5.     Ship ship=new Ship();    
  6.     ship.run("轮船");     
  7.     Plane plane=new Plane();       
  8.     plane.run("飞机");     
  9.   } 

Car类:

  1. public class Car {   
  2.   void run(String type){    
  3.      System.out.println(type+"在公路上开");    
  4.  } 

Ship类:

  1. public class Ship {     
  2.   void run(String type){      
  3.    System.out.println(type+"在水里开");   
  4.   } 

Plane类:

  1. public class Plane { 
  2.    void run(String type){ 
  3.        System.out.println(type+"在天空开"); 
  4.    } 

运行结果:

 

c.优化

我们可以发现单一职责原则有点代码太多了,显得冗余。毕竟我们程序员是能少写就少写,决不能多写代码。那我们对其优化下,上面每个类只有一个方法,我们可以合并为一个类,其中有三个方法,每个方法对应着在公路上,在水上,在天空中的交通工具,将单一职责原则落在方法层面,而不再是类层面,代码如下:

single类:

  1. public class single { 
  2.    public static void main(String[] args) { 
  3.        Vehicle vehicle = new Vehicle(); 
  4.        vehicle.runOnRoad("汽车"); 
  5.        vehicle.runOnWater("轮船"); 
  6.        vehicle.runOnAir("飞机"); 
  7.    } 

Vehicle类:

  1. public class Vehicle { 
  2.    void runOnRoad(String type){ 
  3.        System.out.println(type+"在公路上开"); 
  4.    } 
  5.    void runOnWater(String type){ 
  6.        System.out.println(type+"在水里开"); 
  7.    } 
  8.    void runOnAir(String type){ 
  9.        System.out.println(type+"在天空开"); 
  10.    } 

运行结果:

 

d.优缺点总结

优点:

  • 降低类的复杂性,一个类只负责一个职责。
  • 提高代码的可读性,逻辑清楚明了。
  • 降低风险,只修改一个类,并不影响其他类的功能。

缺点:代码量增多。(可将单一职责原则落在方法层面进行优化)

2.接口隔离原则

类不应该依赖他不需要的接口,接口尽量小颗粒划分。

a.未使用接口隔离原则

People类:

  1. public interface People { 
  2.    void exam(); 
  3.    void teach(); 

Student类:

  1. public class Student implements People { 
  2.    @Override 
  3.    public void exam() { 
  4.        System.out.println("学生考试"); 
  5.    } 
  6.  
  7.    @Override 
  8.    public void teach() { 
  9.  
  10.    } 

Teacher类:

  1. public class Teacher  implements People{ 
  2.    @Override 
  3.    public void exam() { 
  4.  
  5.    } 
  6.  
  7.    @Override 
  8.    public void teach() { 
  9.        System.out.println("教师教书"); 
  10.    } 

test类:

  1. public class test { 
  2.    public static void main(String[] args){ 
  3.        People student=new Student(); 
  4.        student.exam(); 
  5.  
  6.        People teacher=new Teacher(); 
  7.        teacher.teach(); 
  8.    } 

运行结果:

 

注:此处代码并没有报错,正常运行的,但是看得代码冗余且奇怪。Student只需要实现People的exam方法,而Teacher只需要实现People的teach方法,但是现在Student实现了People接口,就必须重写exam和teach方法,Teacher也是如此。

b.已使用接口隔离原则

我们将People接口的两个方法拆分开,分为两个接口People1和People2,并且让Sudent实现People1接口,Teacher实现People2接口,使其互不干扰,具体代码如下:

People1类:

  1. public interface People1 { 
  2.    void exam(); 

People2类:

  1. public interface People2 { 
  2.    void teach(); 

Student类:

  1. public class Student implements People1 { 
  2.    @Override 
  3.    public void exam() { 
  4.        System.out.println("学生考试"); 
  5.    } 

Teacher类:

  1. public class Teacher  implements People2 { 
  2.    @Override 
  3.    public void teach() { 
  4.        System.out.println("教师教书"); 
  5.    } 

test类:

  1. public class test { 
  2.    public static void main(String[] args){ 
  3.        People1 student=new Student(); 
  4.        student.exam(); 
  5.  
  6.        People2 teacher=new Teacher(); 
  7.        teacher.teach(); 
  8.    } 

运行结果:

 

c.总结

某人要问了,那奇怪碍什么事,能正常运行就行?此处需要敲头,产品经理认为能跑就行我可以理解,但是咱身为程序员,不能就这点追求,要求代码优雅。。。(手动调侃产品经理)

 

[[319071]]

 

言归正传,如果将多个方法合并为一个接口,再提供给其他系统使用的时候,就必须实现该接口的所有方法,那有些方法是根本不需要的,造成使用者的混淆。

3.依赖倒转原则

高层模块不应该依赖底层模块,二者都应该依赖接口或抽象类。其核心就是面向接口编程

依赖倒转原则主要基于如下的设计理念:相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。

抽象指接口或抽象类,细节指具体的实现类。

这样讲太干涩,照搬宣科,没有灵魂,说了等于没说。接下来我们用例子来说明。

a.未使用依赖倒转原则

由于现在是特殊时期,我们先来一个买菜的例子。如下是傻白甜的例子,未使用到依赖倒转原则。

Qingcai类:

  1. public class Qingcai { 
  2.    public void run(){ 
  3.        System.out.println("买到了青菜"); 
  4.    } 

People类:

  1. public class People { 
  2.    public void bug(Qingcai qingcai){ 
  3.        qingcai.run(); 
  4.    } 

test类:

  1. public class test { 
  2.    public static  void main(String[] args){ 
  3.        People people=new People(); 
  4.        people.bug(new Qingcai()); 
  5.    } 

运行结果:

 

b.提出问题,思路转变(重点)

上述看着没啥问题,但是如果他不想买青菜,想买萝卜怎么办?我们当然可以新建一个萝卜类,再给他弄一个run方法,但是问题是People并没有操作萝卜类的方法,我们还需要在People添加对萝卜类的依赖。这样代码要修改的代码量太多了,模块与模块之间的耦合性太高,只要需要稍微有点变化,就要大面积重构,所以该设计不合理,我们看下其类图,如下:

 

这种设计是一般设计的思考方式,而依赖倒转原则中的倒转是指和平常的思考方式完全相反,先从底部开始,即先从Qingcai和Luobo开始,然后想是否能抽象出什么。很明显,他们都是蔬菜,然后我们再回头重新思考如何来设计,新的设计图如下:(请原谅我手残党,画图都画不好。。。)

 

我们可以看到将低层的类抽象出一个接口Shucai,其直接和高层进行交互,而低层的一些类则不参与,这样能降低代码的耦合性,提高稳定性。

c.已使用依赖倒转原则

思路有了,那就来代码耍耍把。

Shucai类:

  1. public interface Shucai { 
  2.    public void run(); 

Qingcai类:

  1. public class Qingcai implements Shucai{ 
  2.    public void run(){ 
  3.        System.out.println("买到了青菜"); 
  4.    } 

Luobo类:

  1. public class Luobo implements Shucai { 
  2.    @Override 
  3.    public void run() { 
  4.        System.out.println("买到了萝卜"); 
  5.    } 

People类:

  1. public class People { 
  2.    public void bug(Shucai shucai){ 
  3.        shucai.run(); 
  4.    } 

test类:

  1. public class test { 
  2.    public static  void main(String[] args){ 
  3.        People people=new People(); 
  4.        people.bug(new Qingcai()); 
  5.        people.bug(new Luobo()); 
  6.    } 

运行结果:

 

d.总结

该原则重点在“倒转”,要从低层往上思考,尽量抽象抽象类和接口。此例子很好的解释了“上层模块不应该依赖低层模块,他们都应该依赖于抽象”。在最开始的设计中,上层模块依赖了低层模块,调整后,上层模块和低层模块都依赖于接口Shucai,依赖关系从图中可以看出来了“倒转”。

4.里氏替换原则

a.继承的优缺点

里氏替换原则是1988年麻省理工姓李的女士提出,它是阐述了对继承extends的一些看法。

继承的优点:

  • 提高代码的重用性,子类也有父类的属性和方法。
  • 提高代码的可扩展性,子类有自己特有的方法。

继承的缺点:

当父类发生改变的时候,要考虑子类的修改。

里氏替换原则是继承的基础,只有当子类替换父类时,软件功能仍然不受到影响,才说明父类真正被复用啦。

a.使用里氏替换原则1

子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。

反例

父类A:

  1. public class A { 
  2.    public void run(){ 
  3.        System.out.println("父类执行"); 
  4.    } 

子类B:

  1. public class B extends A{ 
  2.    public void run(){ 
  3.        System.out.println("子类执行"); 
  4.    } 

测试类test:

  1. public class test { 
  2.    public static void main(String[] args) { 
  3.        A a = new A(); 
  4.        a.run(); 
  5.        System.out.println("将子类替换成父类:"); 
  6.        B b = new B(); 
  7.        b.run(); 
  8.    } 

运行结果:

 

注:我每次使用子类替换父类的时候,还要担心这个子类有没有可能导致问题。此处子类不能直接替换成父类,故没有遵循里氏替换原则。

b.使用里氏替换原则2

子类中可以增加自己特有的方法

父类A:

  1. public class A { 
  2.    public void run(){ 
  3.        System.out.println("父类执行"); 
  4.    } 

子类B:

  1. public class B extends A{ 
  2.    public void runOwn(){ 
  3.        System.out.println("子类执行"); 
  4.    } 

测试类test:

  1. public class test { 
  2.    public static void main(String[] args) { 
  3.        A a = new A(); 
  4.        a.run(); 
  5.  
  6.        System.out.println("将子类替换成父类:"); 
  7.        B b = new B(); 
  8.        b.run(); 
  9.  
  10.        b.runOwn(); 
  11.    } 

运行结果:

 

注:父类A 有run方法,继承父类A的子类B有runOwn方法,测试类test先是调用A类的run方法,接着用B类替换A类,发现还是执行的是父类A的run方法,最后再调用子类B特有的方法runOwn方法。如上,说明该段代码已使用了里氏替换原则。

c.使用里氏替换原则3

当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

父类A:

  1. public class  A { 
  2.    public void run(HashMap hashMap){ 
  3.        System.out.println("父类执行"); 
  4.    } 

子类B :

  1. public class B extends A{ 
  2.    public void run(Map map){ 
  3.        System.out.println("子类执行"); 
  4.    } 

测试类test:

  1. public class test { 
  2.    public static void main(String[] args) { 
  3.        A a = new A(); 
  4.        a.run(new HashMap()); 
  5.  
  6.        System.out.println("将子类替换成父类:"); 
  7.        B b = new B(); 
  8.        b.run(new HashMap()); 
  9.  
  10.    } 

运行结果:

 

我们可以看到在测试类test中,将父类A替换成子类B的时候,还是显示的执行结果“父类执行”,我们可以发现他并不是重写,而是方法重载,因为参数不一样,所以他其实是对继承的规范化,为了更好的使用继承。关于是否为方法重载或重写,我们从下图看:

 

如果是重写,在上图标红的位置会出现箭头,我们可以看出是实际为重载。

那如果没有使用这个规则,会是什么样?看下面的代码:

父类A:

  1. public class  A { 
  2.    public void run(Map map){ 
  3.        System.out.println("父类执行"); 
  4.    } 

子类B:

  1. public class B extends A{ 
  2.    public void run(HashMap hashMap){ 
  3.        System.out.println("子类执行"); 
  4.    } 

测试test:

  1. public class test { 
  2.    public static void main(String[] args) { 
  3.        A a = new A(); 
  4.        a.run(new HashMap()); 
  5.  
  6.        System.out.println("将子类替换成父类:"); 
  7.        B b = new B(); 
  8.        b.run(new HashMap()); 
  9.  
  10.    } 

运行结果:

 

我们可以看到将子类的范围比父类大的时候,替换的子类还是执行自己的子类方法。此不符合里氏替换原则。

d.总结

我们平常好像也没有遵循这些里氏替换原则,程序还是正常跑。其实如果不遵循里氏替换原则,你写的代码出问题的几率会大大增加。

5.开闭原则(重点)

a.基本介绍

前面四个原则,单一职责原则,接口屏蔽原则,依赖倒转原则,里氏替换原则可以说都是为了开闭原则做铺垫,其是编程汇总最基础,最重要的设计原则,核心为对扩展开发,对修改关闭,简单来说,通过扩展软件的行为来实现变化,而不是通过修改来实现,尽量不修改代码,而是扩展代码。

b.未使用开闭原则

接口transport:

  1. public interface transport { 
  2.    public void run(); 

Bus:

  1. public class Bus implements transport { 
  2.    @Override 
  3.    public void run() { 
  4.        System.out.println("大巴在公路上跑"); 
  5.    } 

当我们修改需求,让大巴也能有在水里开的属性,我们可以对Bus类添加一个方法即可。但是这个已经违背了开闭原则,如果业务复杂,这样子的修改很容易出问题的。

c.已使用开闭原则

我们可以新增一个类,实现transport接口,并继承Bus类,写自己的需求即可。

  1. public class universalBus extends Bus implements transport { 
  2.    @Override 
  3.    public void run() { 
  4.        System.out.println("大巴既然在公路上开,又能在水里开"); 
  5.    } 

6.迪米特原则

a.介绍

  • 一个对象应该对其他对象保持最少的了解。
  • 类与类关系越密切,耦合度越大
  • 一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息
  • 迪米特法则还有个更简单的定义:只与直接(熟悉)的朋友通信
  • 直接(熟悉)的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。

其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量 的形式出现在类的内部。

把上面的概念一一翻译成人话就是:

  • 我们这个类姑娘啊,因为太矜持了不善于社交,所以对其他类伙伴们不怎么熟悉。
  • 类姑娘实在是太害羞了,一旦与别人多说几句话就会紧张的不知所措,频频犯错。
  • 矜持的类姑娘尽管心思很活跃,爱多想。但是给别人的感觉都是纯洁的像一张白纸。
  • 因为类姑娘太过于矜持,害怕陌生人,认为陌生人都是坏人,所以只与自己熟悉的朋友交流。
  • 类姑娘熟悉的朋友有:成员变量,方法参数,方法返回值的对象。而出现在其他地方的类都是陌生人,坏人!本姑娘拒绝与你交流!!!

哈哈,这样应该大家都能理解了。总而言之就一句话:一个类应该尽量不要知道其他类太多的东西,不要和陌生的类有太多接触。

b.未使用迪米特原则

总公司员工Employee类:

  1. public class Employee { 
  2.    private String id; 
  3.  
  4.    public String getId() { 
  5.        return id; 
  6.    } 
  7.  
  8.    public void setId(String id) { 
  9.        this.id = id; 
  10.    } 

分公司员工SubEmployee类:

  1. public class SubEmployee { 
  2.    private String id; 
  3.  
  4.    public String getId() { 
  5.        return id; 
  6.    } 
  7.  
  8.    public void setId(String id) { 
  9.        this.id = id; 
  10.    } 

总公司员工管理EmployeeManager类:

  1. public class EmployeeManager { 
  2.    public List<Employee> setValue(){ 
  3.        List<Employee> employees=new ArrayList<Employee>(); 
  4.        for(int i=0;i<10;i++){ 
  5.            Employee employee=new Employee(); 
  6.            employee.setId("总公司"+i); 
  7.            employees.add(employee); 
  8.        } 
  9.        return  employees; 
  10.    } 
  11.  
  12.    public void printAllEmployee(SubEmployeeManager sub){ 
  13.        List<SubEmployee> list1 = sub.setValue(); 
  14.        for(SubEmployee e:list1){ 
  15.            System.out.println(e.getId()); 
  16.        } 
  17.  
  18.        List<Employee> list2 = this.setValue(); 
  19.        for(Employee e:list2){ 
  20.            System.out.println(e.getId()); 
  21.        } 
  22.    } 
  23.  

分公司员工管理SubEmployeeManager类:

  1. public class SubEmployeeManager { 
  2.    public List<SubEmployee> setValue(){ 
  3.        List<SubEmployee> subEmployees=new ArrayList<SubEmployee>(); 
  4.        for(int i=0;i<10;i++){ 
  5.            SubEmployee subEmployee=new SubEmployee(); 
  6.            subEmployee.setId("分公司"+i); 
  7.            subEmployees.add(subEmployee); 
  8.        } 
  9.        return subEmployees; 
  10.    } 

测试类:

  1. public class test { 
  2.    public static  void main(String[] args){ 
  3.        EmployeeManager employeeManager=new EmployeeManager(); 
  4.        SubEmployeeManager subEmployeeManager=new SubEmployeeManager(); 
  5.        employeeManager.printAllEmployee(subEmployeeManager); 
  6.    } 

运行结果:

 

上面的代码是正常运行的,但是可以看到一个问题,EmployeeManager类的printAllEmployee方法中使用的局部变量SubEmployee是不符合迪米特法则的,其是陌生朋友,应该拒绝沟通。

b.已使用迪米特原则

EmployeeManager类:

  1. public class EmployeeManager { 
  2.    public List<Employee> setValue() { 
  3.        List<Employee> employees = new ArrayList<Employee>(); 
  4.        for (int i = 0; i < 10; i++) { 
  5.            Employee employee = new Employee(); 
  6.            employee.setId("总公司" + i); 
  7.            employees.add(employee); 
  8.        } 
  9.        return employees; 
  10.    } 
  11.  
  12.    public void printAllEmployee(SubEmployeeManager sub) { 
  13.        sub.printAllSubEmployee(); 
  14.  
  15.        List<Employee> list2 = this.setValue(); 
  16.        for (Employee e : list2) { 
  17.            System.out.println(e.getId()); 
  18.        } 
  19.    } 
  20.  

SubEmployeeManager类:

  1. public class SubEmployeeManager { 
  2.    public List<SubEmployee> setValue(){ 
  3.        List<SubEmployee> subEmployees=new ArrayList<SubEmployee>(); 
  4.        for(int i=0;i<10;i++){ 
  5.            SubEmployee subEmployee=new SubEmployee(); 
  6.            subEmployee.setId("分公司"+i); 
  7.            subEmployees.add(subEmployee); 
  8.        } 
  9.        return subEmployees; 
  10.    } 
  11.  
  12.    public void printAllSubEmployee(){ 
  13.        List<SubEmployee> list1 = setValue(); 
  14.        for(SubEmployee e:list1){ 
  15.            System.out.println(e.getId()); 
  16.        } 
  17.    } 

我们将EmployeeManager类printAllEmployee方法中的打印分公司的代码移到了分公司的管理类SubEmployeeManager类中,再在方法中显示的调用SubEmployeeManager类的方法,这符合迪米特法则的。

7.合成复用原则

尽量使用合成/集合,不要用继承。

如果使用继承,会使得耦合性加强,尽量作为方法的输入参数或类的成员变量,这样可以避免耦合。

结语

所有的原则只是规范,为了代码更加优雅,为了让人一目了然。如果一定不遵循原则,那代码还是可以跑的,只是日后出bug的可能性提高。

以上,简单来说,主要包括两点:

1.找出应用中需要变化的独立出来,不要和固定的混合在一起。

2.面向接口编程,而不是面向实现编程。

责任编辑:武晓燕 来源: 学习Java的小姐姐
相关推荐

2020-01-15 09:53:59

MySQL缓存索引

2017-12-07 15:34:57

数据库MySQL优化原理

2021-03-16 08:21:29

Spark系统并行

2023-10-31 12:58:00

TypeScriptJavaScript

2022-06-02 15:31:26

深度学习AI

2023-10-19 13:47:58

2023-01-06 08:15:58

StreamAPI接口

2021-11-11 09:27:02

技术RedisMySQL

2021-10-18 11:58:56

负载均衡虚拟机

2022-09-06 08:02:40

死锁顺序锁轮询锁

2023-03-30 08:28:57

explain关键字MySQL

2013-05-27 12:59:22

设计模式GoF

2015-07-22 16:01:31

2021-01-19 05:49:44

DNS协议

2022-09-14 09:01:55

shell可视化

2019-11-06 10:12:19

B端设计流程分析

2020-11-11 11:11:23

Linux内核源码

2020-11-16 10:47:14

FreeRTOS应用嵌入式

2020-07-15 08:57:40

HTTPSTCP协议

2023-12-23 23:23:37

点赞
收藏

51CTO技术栈公众号