设计模式 设计原则实现高内聚低耦合的编程思想
目录
一、概述
二、7个设计原则
1、单一职责原则 ( SRP )
2、开闭原则 ( OCP )
3、里氏替换原则 ( LSP )
4、依赖倒置原则 ( DIP )
5、接口隔离原则 ( ISP )
6、最少知道原则(迪米特原则)
7、合成/聚合复用(CARP)
三、创建型模式 ( 5种 )
1.单例模式
2.工厂方法模式
3.抽象工厂模式
4.建造者模式
5.原型模式
四、结构型模式(7种)
1.适配器模式
2.桥接模式
3.装饰模式
4.组合模式
5.外观模式
6.享元模式
7.代理模式
五、行为型模式 ( 11种 )
1.模板方法模式
2.命令模式
3.迭代器模式
4.观察者模式
5.中介者模式
6.备忘录模式
7.解释器模式
8.状态模式
9.策略模式
10.责任链模式
11.访问者模式
前言
编程思想中,入门心法就是面向对象思想,而最高心法就是设计模式思想。如果你能够参悟明白设计模式,并且能熟练掌握灵活运用它们,那你读各种库源码的时候会忽然发现并不难懂了。设计模式追求的目的就是“高内聚、低耦合”,在实际编程中运用设计模式能够使我们工程代码更规范、重用性更高,同时也能保证代码的可靠性、提高开发效率。
一、概述
面向对象编程有七大原则,即经常提到的设计模式(Design Pattern),提倡它的根本原因是为了代码复用,增加可维护性。设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。
设计模式是继面向对象思想提升的更高级编程思想,分为三类 :创建型、结构型、行为型 。常用的有如下:1、创建型是为了隐藏类的创建,不再直接 new 对象实例。例如,单例模式、工厂模式、建造者模式。2、结构型是为了隐藏调用对象的方法,不再直接访问对象实例。例如,代理模式、适配器模式、装饰者模式。3、行为型是为了优化对象与对象之间一对多调用关系。例如,观察者模式、中介者模式、访问者模式。
二、7个设计原则
1、单一职责原则 ( SRP )
SRP(Single Responsibility Principle),表示,定义是一个类应该只有一个引起它变化的原因。类变化的原因就是职责,如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。
2、开闭原则 ( OCP )
OCP(Open Close Principle),定义是软件实体(包括类、模块、函数等)应该对于扩展时开放的,对于修改是封闭的。开闭原则是是面向对象设计中最重要的原则之一,其它很多的设计原则都是实现开闭原则的一种手段。
3、里氏替换原则 ( LSP )
LSP(Liskov Substitution Principle) ,是面向对象设计的基本原则之一。 定义是任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏替换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。
4、依赖倒置原则 ( DIP )
DIP (Dependence Inversion Principle) ,这个原则是开闭原则的基础,依赖倒置原则就是要求调用者和被调用者都依赖抽象,这样两者没有直接的关联和接触,在变动的时候,一方的变动不会影响另一方的变动。依赖倒置强调了抽象的重要性,针对接口编程,依赖于抽象而不依赖于具体。
5、接口隔离原则 ( ISP )
ISP(Interface Segregation Principle) ,这个原则的意思是使用多个隔离的接口,比使用单个接口要好。目的就是降低类之间的耦合度,便于软件升级和维护。
6、最少知道原则(迪米特原则)
一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。通俗地说就是不要和陌生人说话,即一个对象应对其他对象有尽可能少的了解。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
7、合成/聚合复用(CARP)
CARP(Composite Reuse Principle) ,合成/聚合复用原则经常又叫做合成复用原则。合成/聚合复用原则的潜台词是:我只是用你的方法,我们不一定是同类。继承的耦合性更大,比如一个父类后来添加实现一个接口或者去掉一个接口,那子类可能会遭到毁灭性的编译错误,但如果只是组合聚合,只是引用类的方法,就不会有这种巨大的风险,同时也实现了复用。
三、创建型模式 ( 5种 )
创建型模式是指这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用新的运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
1.单例模式
定义
确保某一个类只有一个实例,并自行实例化向整个系统提供这个实例。
简介
单例模式理解起来不难,典型例子有一个公司只能有一个CEO。它主要是为了保证一个类仅有一个实例,这个类中自己提供一个返回实例的方法,方法中先判断系统是否已经有这个单例,如果有则返回,如果没有则创建。如果创建多个实例会消耗过多的资源或者某种类型的对象只应该有且只有一个时,应该考虑使用单例模式。
实现
单例模式理解起来不难,重要的是需要掌握它的几种常见写法。
1 2 写法一、懒汉式写法 3 4 public class Singleton { 5 6 private static Singleton instance; 7 8 //构造函数私有 9 private Singleton (){ 10 } 11 12 public static synchronized Singleton getInstance() { 13 if (instance == null) { 14 instance = new Singleton(); 15 } 16 return instance; 17 } 18 } 19 20 写法二、DCL(Double Check Lock) 双重校验锁 21 22 public class Singleton { 23 24 private volatile static Singleton singleton; 25 26 private Singleton (){ 27 } 28 29 public static Singleton getSingleton() { 30 31 if (singleton == null) { 32 synchronized (Singleton.class) { 33 if (singleton == null) { 34 singleton = new Singleton(); 35 } 36 } 37 } 38 return singleton; 39 } 40 } 41 42 写法三、静态内部类单例模式 43 44 public class Singleton { 45 46 private Singleton (){ 47 } 48 49 public static final Singleton getInstance() { 50 return SingletonHolder.INSTANCE; 51 } 52 53 private static class SingletonHolder { 54 private static final Singleton INSTANCE = new Singleton(); 55 } 56 }
上面只列出了单例模式常见的三种写法,单例模式还有好几种别的写法,这里就不全部列出了。上面的第一种懒汉式写法做到了延迟创建和线程安全,缺点是每次调用getInstance()时都必须进行同步,效率不佳。第二种DCL方式比较常见,两次判空,第一次判空避免了不必要的同步,第二次保证了单例创建,这种方式比较不错,但是在高并发环境下有时会出现问题。第三种方法最被推荐,线程安全也保证了实例唯一。
2.工厂方法模式
- 定义
定义一个用于创建对象的接口,让子类决定实例化哪一个类。
- 举例
工厂方法模式的典型例子,自行车分为山地自行车和公路自行车等,当需要买自行车时,我们直接去自行车厂里告诉厂长我们需要的自行车即可。
- 例子
1 1、定义一个接口自行车,Bike。 2 3 public interface Bike{ 4 void ride(); 5 } 6 7 2、定义实现类山地自行车,MBike 8 9 public class MBike implements Bike{ 10 @Override 11 public void draw() { 12 System.out.println("MBike Rides... "); 13 } 14 } 15 16 3、定义实现类公路自行车,RBike 17 18 public class RBike implements Bike{ 19 @Override 20 public void draw() { 21 System.out.println("RBike Rides... "); 22 } 23 } 24 25 4、工厂类,负责创建对象 26 public class BikeFactory { 27 //使用 getShape 方法获取形状类型的对象 28 public Bike getBike(String bikeType){ 29 30 if(bikeType == null){ 31 return null; 32 } 33 34 if(bikeType.equalsIgnoreCase("MBike")){ 35 return new MBike(); 36 }else if(shapeType.equalsIgnoreCase("RBike")){ 37 return new RBike(); 38 } 39 40 return null; 41 } 42 } 43 44 5、子类决定实例化哪一个类 45 46 public class FactoryPatternDemo { 47 public static void main(String[] args) { 48 49 BikeFactory bikeFactory = new BikeFactory(); 50 51 Bike bike1 = bikeFactory.getBike("MBike"); 52 bike1.ride(); 53 54 Bike bike2= bikeFactory.getBike("RBike"); 55 bike2.ride(); 56 } 57 }
3.抽象工厂模式
定义
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
简介
抽象工厂模式主要解决接口选择的问题,典型例子是手机和电脑问题,假设抽象产品1是手机,具体产品是Android手机和iOS手机,抽象产品2是电脑,具体产品是Windows电脑和Mac电脑,抽象工厂是同时生产手机和电脑的公司,具体工厂是联想公司和苹果公司。
抽象工厂的优点是当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。缺点是产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象工厂里加代码,又要在具体工厂里加代码。
4.建造者模式
定义
将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
简介
建造者模式主要解决在软件系统中一个复杂对象的创建工作。通常一个复杂对象是由各个部分的子对象用一定的算法构成,由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
建造者模式的一个典型例子是Android中的AlertDialog的构建过程。还有个例子是计算机的组装,计算机是个复杂的对象,它是有很多零件组装而成,显示器、操作系统,鼠标等,通过创建Builder接口来控制零件的组装过程,这样当组件发生变化时,虽然经过同样的构建过程,但是最后得到的结果不同。
5.原型模式
定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
简介
原型模式不难理解,它主要是用在实例创建的时候,因为有的时候我们通过new创建一个对象时可能成本过高,这时我们可以考虑直接通过直接克隆实例快速创建对象。克隆后的实例与原实例内部属性一致。原型模式需要注意一个深拷贝和浅拷贝的问题。
四、结构型模式(7种)
结构型模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
结构型模式
1.适配器模式
定义
适配器模式使得原本由于对象接口不同而不能一起工作的那些类可以一起工作。
简介
适配器模式包括两种类型,类适配器模式和对象适配器模式。这里主要介绍对象适配器模式,因为它更灵活。查看更多
例子
1 //播放动作 2 public interface MediaPlayer { 3 public void play(String audioType, String fileName); 4 } 5 6 7 public class Mp3Player{ 8 public void playMp3(String fileName) { 9 //播放文件fileName 10 } 11 12 } 13 14 public class Mp4Player { 15 public void playMp4(String fileName) { 16 //播放文件fileName 17 } 18 } 19 20 //音频播放适配器 21 public class MediaAdapter implements MediaPlayer { 22 23 private Mp3Player mp3 24 private Mp4Player mp4; 25 public MediaAdapter(Mp4Player mp4, Mp3Player mp3){ 26 this.mp3 =mp3; 27 this.mp4 =mp4; 28 } 29 30 @Override 31 public void play(String audioType, String fileName) { 32 33 if(audioType.equalsIgnoreCase("mp3")){ 34 mp3.playMp3(fileName); 35 }else if(audioType.equalsIgnoreCase("mp4")){ 36 mp4.playMp4(fileName); 37 } 38 } 39 } 40 41 //Main 方法入口 42 public static void main(String[] args) { 43 MediaAdapter adapter = new MediaAdapter(new Mp4Player() , new Mp3Player()); 44 45 adapter.play("mp3", "beyond.mp3"); 46 adapter.play("mp4", "alone.mp4"); 47 }
2.桥接模式
定义
将抽象部分与实现部分分离,使它们都可以独立的变化。
-简介
在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种“多维度的变化”?这就要使用桥接模式。桥接模式需要重点理解的抽象部分,实现部分,脱耦。一个典型的例子是咖啡加糖问题,抽象部分有Coffee,其下有LargeCoffee,SmallCoffee,实现部分是CoffeeAdd,其下有Sugar,Normal,抽象类Coffee中引用CoffeeAdd,这样CoffeeAdd其实就是一个桥接。
3.装饰模式
定义
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
简介
装饰模式是作为现有的类的一个包装,它允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。装饰模式主要是装饰器类Decorator的设计,它对指定对象进行装饰。
案例
1 //形状 绘制 接口 2 public interface Shape { 3 void draw(); 4 } 5 //矩形 继承 绘制 6 public class Rectangle implements Shape { 7 @Override 8 public void draw() { 9 System.out.print("Rectangle"); 10 } 11 } 12 //圆形 继承 绘制 13 public class Circle implements Shape { 14 @Override 15 public void draw() { 16 System.out.print("Circle"); 17 } 18 } 19 //装饰者父类 20 public abstract class ShapeDecorator implements Shape { 21 //被装饰者 22 protected Shape shape; 23 public ShapeDecorator(Shape shape){ 24 this.shape = shape; 25 } 26 27 public void shapeDraw(){ 28 shape.draw(); 29 } 30 } 31 //装饰者子类 加 红色 32 public class RedShapeDecorator extends ShapeDecorator { 33 public RedShapeDecorator(Shape shape) { 34 super(shape); 35 } 36 37 @Override 38 public void draw() { 39 super. shapeDraw(); //调用父方法 40 setRedBorder(shape);//加红色 41 } 42 43 private void setRedColor(Shape shape){ 44 System.out.println(" Add Color Red"); 45 } 46 } 47 //Main 方法 48 public static void main(String[] args) { 49 50 Shape redCircle = new RedShapeDecorator(new Circle()); 51 Shape redRectangle = new RedShapeDecorator(new Rectangle()); 52 53 redCircle.draw(); 54 //打印结果: Circle Add Color Red 55 redRectangle.draw(); 56 //打印结果: Rectangle Add Color Red 57 }
4.组合模式
定义
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
简介
组合模式理解起来相对简单,典型的例子就是假设公司A,里面有不同的部门,不同的部分下有不同的员工,这样一个部门下的所有员工组合成了一个部门,所有部门组合成了整个公司。
5.外观模式
定义
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
简介
外观模式的一个典型例子是去医院看病,挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。
6.享元模式
- 定义
运用共享技术有效地支持大量细粒度的对象。
- 简介
在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
7.代理模式
- 定义
为其他对象提供一个代理类以控制对这个对象的访问。
- 简介
代理模式主要解决一个类在直接访问对象时带来的问题。例如,博物馆展示一张无价的古画,为了避免古画被游客破坏,故制作一张高仿的画进行展示,而且可以随意对假画?进行政治化修改,这样才能发挥文化的更大效果。
- 例子
-
1 //展示图片动作 2 public interface Image { 3 void display(); 4 } 5 6 //真画 7 public class RealImage implements Image { 8 9 private String fileName; 10 11 public RealImage(String fileName){ 12 this.fileName = fileName; 13 } 14 15 @Override 16 public void display() { 17 //展示 18 } 19 } 20 21 // 假画(代理类) 22 public class ProxyImage implements Image{ 23 24 private RealImage realImage; 25 private String fileName; 26 27 public ProxyImage(String fileName){ 28 this.fileName = fileName; 29 } 30 31 @Override 32 public void display() { 33 if(realImage == null){ 34 realImage = new RealImage(fileName); 35 } 36 realImage.display(); 37 } 38 } 39 40 //Main 方法入口 41 public static void main(String[] args) { 42 //创建假画实例 43 Image image = new ProxyImage("test_10mb.jpg"); 44 image.display(); 45 }
五、行为型模式 ( 11种 )
这些设计模式特别关注对象之间的通信。
1.模板方法模式
定义
一个操作中的算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
例子
模板方法模式一个典型例子就是Android中的异步任务类AsyncTask,它对异步任务的执行进行了流程封装,子类继承它时,只需在指定的流程中实现具体的操作即可。
2.命令模式
定义
将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作
简介
命令模式主要是通过调用者调用接受者执行命令,这个模式中需要理解的是三个角色:(1) Receiver 真正的命令执行对象 (2) Command 持有一个对Receiver的引用,调用Receiver的相关方法。(3) Invoker 请求者,持有一个对Command的引用,调用Command的方法执行具体命令。
3.迭代器模式
- 定义
提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
- 简介
在Java集合框架中我们知道对于一个指定的集合类,我们可以使用一个特定的Iterator迭代器来对集合中的所有元素进行遍历。这样结合来看,迭代器模式很好理解了。
4.观察者模式
定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
简介
观察者模式其实很容易理解,因为是对象之间一对多的通信关系,就很像我们常见的教室里,一个老师站在讲台被台下所有同学关注着,一旦老师喊‘举手’,同学们纷纷响应
- 例子
Teacher.Class 老师类
1 public class Teacher { 2 //创建同学们的观察者集合 3 private List<Students> observers 4 = new ArrayList<Students>(); 5 6 private boolean isNotify;//发起通知举手 7 8 9 public void setNotify(boolean isNotify) { 10 this. isNotify = isNotify; 11 notifyAllObservers(); 12 } 13 14 //依赖添加新的同学 15 public void attach(Students observer){ 16 observers.add(observer); 17 } 18 19 //循环通知每位观察者们 20 public void notifyAllObservers(){ 21 for (Students observer : observers) { 22 observer.update(); 23 } 24 } 25 }
Student.Class 同学类
1 public abstract class Student { 2 3 //抽象方法必须实现 4 public abstract void update(); 5 6 protected Teacher teacher; 7 }
1 //子类:具体同学——小明 2 public class xiaoMingObserver extends Student{ 3 4 public xiaoMingObserver(Teacher teacher){ 5 this.teacher = teacher; 6 this.teacher.attach(this); 7 } 8 9 @Override 10 public void update() { 11 //小明举手 12 } 13 } 14 15 //子类:具体同学——小红 16 public class xiaoHongObserver extends Student{ 17 18 public xiaoHongObserver(Teacher teacher){ 19 this.teacher = teacher; 20 this.teacher.attach(this); 21 } 22 23 @Override 24 public void update() { 25 //小红举手 26 } 27 }
Main 方法调用
1 public static void main(String[] args) { 2 3 Teacher teacher = new Teacher(); 4 5 new xiaoMingObserver(teacher); 6 new xiaoHongbserver(teacher); 7 8 teacher.setNotify(ture);//通知 9 }
5.中介者模式
定义
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
简介
中介者模式的典型例子就是未加入 WTO 之前各个国家相互贸易,结构复杂,大家都加入WTO后是各个国家通过 WTO 来互相贸易,变得规范。
6.备忘录模式
定义
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
简介
备忘录模式的典型例子就是git版本管理工具,它帮我们保存了每次提交后的项目状态,在必要的时候我们可以回退到指定的版本中。
7.解释器模式
定义
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
简介
解释器的典型例子是在编译原理中的应用,如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
8.状态模式
定义
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
简介
状态模式主要解决对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。典型的例子是一个人在不同的状态下完成一件事的结果可能是不同的。
9.策略模式
定义
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
简介
从策略模式的定义可以看到它主要是将算法和客户独立开,一个典型的例子是排序算法,我们给定一个数组,输出排序后的结果,但是过程中我们可以采取不同的排序算法,这些算法其实就是策略。
10.责任链模式
定义
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
简介
责任链模式,避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
11.访问者模式
定义
封装一些作用于某种数据结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
简介
访问者模式是一种将数据操作和数据结构分离的设计模式,它通常使用在对象结构比较稳定,但是经常需要在此对象结构上定义新的操作,或者需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。点击查看更多
例如,老师要进行家访送出学生的成绩单。每个家长都提供接受家访的接口,老师调用家访接口,
例子
1 //接受老师访问的接口 2 public interface HomeVisiting { 3 4 //接受老师的访问 5 void accept(TeacherVisitor visitor); 6 } 7 8 //小明家 9 public class XiaoMingHome implements HomeVisiting { 10 public String homeName ="XiaoMing"; 11 private String homePager ="";//成绩 12 @Override 13 public void accept(TeacherVisitor visitor) { 14 //老师进行访问 15 visitor.visit(this); 16 } 17 } 18 19 //小红家 20 public class XiaoHongHome implements HomeVisiting { 21 public String homeName ="XiaoHong"; 22 private String homePager ="";//成绩 23 24 @Override 25 public void accept(TeacherVisitor visitor) { 26 //老师进行访问 27 visitor.visit(this); 28 homePager = visitor.paper; 29 } 30 } 31 32 //老师家访动作 33 public interface TeacherVisitor{ 34 public void visit(HomeVisiting home); 35 } 36 37 //老师进行家访 38 public class Teacher implements TeacherVisitor{ 39 public String paper ="成绩单"; 40 @Override 41 public void visit(HomeVisiting home){ 42 // 这个家庭home 的访问情况 + home.gethomeName 43 } 44 } 45 46 //Main 方法入口 47 public static void main(String[] args) { 48 //创建老师访问者 49 TeacherVisitor teacher = new Teacher(); 50 //创建家庭被访问者 51 HomeVisiting home =new XiaoMingHome(); 52 53 home.accept(teacher);//接受访问 54 55 home = new XiaoHongHome(); 56 57 home.accept(teacher); 58 }
转自:https://blog.csdn.net/csdn_aiyang/article/details/73177084