设计模式简介
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式由来
由GOF(四人帮)出版的一本名为《设计模式 - 可复用的面向对象软件元素》的书提出的设计模式的概念。该书基于以下的面向对象设计原则
- 对接口编程而不是对实现编程。
- 优先使用对象组合而不是继承。
设计模式分类
常说的23种设计模式是基于《设计模式 - 可复用的面向对象软件元素》提出的,这些模式可以分:创建型模式,结构性模式,行为型模式。
事实上还有一些关于J2EE设计模式,并发型模式,线程池模式等等。
创建型模式(5种):
描述:指提供另外一种创建对象的同时隐藏创建逻辑的方式,不是通过使用new运算符直接实例化对象
包含:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型模式(7种):
描述:关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式
包含:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
行为型模式(11种):
描述:关注对象之间的通信。
包含:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
下面是关于设计模式之间关系的图
设计模式的六大原则
设计模式遵守六大原则,分别是:开闭原则,里式代换原则,依赖倒转原则,接口隔离原则,迪米特法则(最少知道原则),合成复用原则。
也有一种说法,六大原则应该是七大原则,即在上面的基础上新增单一职责原则
开闭原则(Open Close Principle)
定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
意思是在设计的时候要考虑这个类是足够好的,一旦写好之后就不再修改,之后如果有新需求,只需要扩展一些类即可。所以这个原则有两个特性:“对扩展开放”,“对修改关闭”。想要达到这种效果,需要使用接口和抽象类
例子
![这里写图片描述](https://i-blog.csdnimg.cn/blog_migrate/0f6b7d39d8df086a94115457c8db1a2d.png)
这是一个书店卖书的例子。
书店有书这个抽象的接口类,里面有定义名字,价格,作者。
相比书这个抽象的接口类,有一个具体的实现类,即小说书。小说书实现了书的功能,通过构造器注入小说书的属性。
书店是使用这些书的一个使用者,进行出售。
如果我们现在要将小说书进行低价9折出售,有什么办法?办法有三:
- 修改接口IBook,新添加一个getOffPrice(),获取低价打折后的价格。但是修改之后,NovelBook需要新添加一个getOffPrice()方法,而BookStore类使用时也要进行修改,手动添加打折后的价格。所以不可取
- 修改NovelBook类的getPrice()方法,返回打折后的价格给用户,但是这有一个缺陷。即书的原价将会被隐藏掉,看到的只会是打折后的价格。
- 扩展NovelBook,新添加一个继承与NovelBook的子类OffNovelBook,该类覆写NovelBook的price,返回打折价格。这样如果书店里的购书者想看书的原价,那么我们就IBook book = new NovelBook()给他看原价,如果想看书的打折价格,就IBook book = new OffNovelBook()
好处
- 可以提高复用性
- 可以提高可维护性
- 针对面向对象开发的要求
里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。
里氏代换原则是对“开-闭”原则的具体实现手段之一。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
简单的来讲,就是父类能出现的地方,子类也能出现,即必须能够实现IBook book = new NovelBook();和NovelBook book = new NovelBook()。既java继承。
就是因为这种代换的特性,才使继承复合成为可能,才能谈得上扩展开放,修改关闭。
里式代换原则包含4层含义:
- 子类必须完全实现父类的抽象方法
此处的父类代指接口或抽象类,尽量把父类设计为抽象类或者接口。如果是普通类,那么子类必须继承父类的所有方法
子类可以有自己特有的方法
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松,
这样能保证父类方法一定会被执行。
如果你想让子类的方法能够执行,那么你只能覆写父类方法。
所以子类中方法的形参必须与父类中被覆写的方法的形参相同(覆写)或者更大(重载)。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
所以子类中方法的返回值必须与父类中被覆写的方法的返回值相同(覆写)或者更小(重载)。
注意:事实上里氏代换原则就是继承!注意项也都是正确使用继承中应该注意的点,只要形参和返回值与父类被覆写的方法相同,那么你将不会遇到违背继承意义的事情,也就不会违背里氏替换原则
依赖倒转原则(Dependence Inversion Principle)
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
这个是开闭原则的基础,要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
抽象类和接口定义方法,实现类实现方法。调用时针对接口进行编程,这就是依赖倒转原则,即实现类依赖于接口,而不是接口依赖于实现
注意: 总之,依赖倒置原则就是要我们面向接口编程,就是向上转型,理解了面向接口编程,也就理解了依赖倒置。
例子:
违背依赖倒置原则,不面向接口编程的UML图是这样的
如上图,司机可以开车,但是司机只能开奔驰车,不能开宝马车。如果司机要开宝马车,只能在司机本身上修改。这不符合要求,一个司机既然有了C驾照,那自然是宝马车和奔驰车都能开。
遵守依赖倒置原则,针对接口编程的UML图是这样的
IDriver接口可以理解为C级驾照,只要考了这个驾照就可以开C级车,即ICar接口,也就可以开宝马车和奔驰车。
所以只要司机有了C级驾照,就可以开奔驰车和宝马车了,这就复合我们的要求。
Client属于高层业务逻辑,Driver和Benz,BMW属于底层业务逻辑,client不依赖于Driver和Benz,而依赖于抽象ICar和IDriver。
可能你看到client这个高层模块调用了Benz类和Driver类这两个底层模块。但是你要知道,向上转型是什么意思。zhangSan的引用是IDriver,是一个抽象的接口,其后的操作都是以IDriver这个接口来进行调用操作的,Benz这个类的实现细节对zhangSan来说是屏蔽的。就像你只要能开车,但是不一定需要知道汽车为什么能开,怎么开,是一样的道理。
关于依赖注入
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中。
依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。
构造注入
如:
public Driver(ICar car){ this.car=car; }
setter注入
如:
public void setCar(ICar car){ this.car=car; }
接口注入
如:
public interface IDriver{ public void driver(ICar car); } public class Driver implements IDriver{ public void driver(ICar car){ car.run(); } }
因为Driver类实现了IDriver接口,当你IDriver driver = new Driver();实例化对象后,调用driver方法,传入参数。
这就是一个接口注入。
你是通过IDriver接口的driver方法将参数注入的,通过接口注入参数,实例化的是Driver类,调用了driver方法。
如何进行依赖倒置(面向接口编程)
如上面所说,程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类。
方法形参针对抽象类进行编程
如:
public void driver(ICar car){ car.run(); }
声明类型时针对抽象类生成引用
如:
ICar car = new Benz();
方法返回类型针对抽象类进行返回
如:
public ICar getCar(ICar car){ return car; }
总结:
为什么叫依赖倒置?
因为依赖正置就是实实在在的类的依赖,面向实现编程编程,高层模块依赖于底层模块,底层模块依赖于高层模块。这也是正常人的思维方式,我要开奔驰车就依赖奔驰车。
倒置是什么?
就是颠覆了人对依赖的认知,我们对奔驰车进行抽象化,抽象成车。那么我依赖的是车这个抽象,而不是奔驰车这个实体,奔驰车也不依赖我,依赖的是车这个抽象,这就是高层模块不应该依赖低层模块,二者都应该依赖其抽象。
接口隔离原则(Interface Segregation Principle)
定义:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口,类间的依赖关系应该建立在最小的接口上。
即建立单一接口,不要建立臃肿庞大的接口,接口尽量细化,同时接口中的方法尽量少。客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性,把一个臃肿的接口变更为多个独立的接口所依赖的原则就是接口隔离原则
例子:
美女的标准是身材好,脸蛋漂亮,气质好。这样一个接口有时候不能定义,所以可以分为身材好,脸蛋漂亮和气质好两个接口,定义了外表美女和气质美女
注意:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
迪米特法则(最少知道原则)(Demeter Principle)
定义:一个对象应该对其他对象有最少的了解。
典型例子:
- 只同你直接的朋友们通信
- 不要跟陌生人说话
- 每一个软件单位对其他的单位都只有最少的了解,这些了解仅局限于那些与本单位密切相关的软件单位
其根本思想,是强调了类之间的松耦合,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成影响,也就是说,信息的隐藏促进了软件的复用。
其实就是要求我们在设计的时候,尽量减少两个对象之间直接的交互。如果其中一个对象需要调用另一个对象时,引入一个第三者来转发这个调用
单一职责原则(Single Responsibility Principle, SRP):
定义:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
遵循单一职责原的优点有:
1.可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
2.提高类的可读性,提高系统的可维护性;
3.变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
所以遵循单一原则,就要遵循按职责划分接口,一个接口或类,只负责一项职责,是实现高内聚、低耦合的指导方针。它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
例子:
按职责划分,分为用户行为和用户信息,可划分两个接口
入门篇总结:
开闭原则:对扩展开放,对修改关闭。
里氏代换原则:实现子父类互相替换,即继承;
依赖倒转原则:针对接口编程,实现开闭原则的基础;
接口隔离原则:降低耦合度,接口单独设计,互相隔离;
迪米特法则,又称不知道原则:功能模块尽量独立;
单一职责原则:一个类只负责一项职责
合成复用原则:尽量使用聚合,组合,而不是继承;
后期会再次修改。。。。