设计模式概述
最近开始学习设计模式相关的知识,记录一下。
设计模式是什么
学习一个东西,肯定要先知道这个东西是什么,用来解决什么问题。从网上找了一份设计模式的定义:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性
据说最开始软件工程中模式的概念还是借鉴于建筑学,由大名鼎鼎的“*”(GoF)引入了软件工程领域。其实软件模式除了设计模式还包括架构模式、分析模式、过程模式等,其他的有机会再了解。
简单来说,设计模式就是程序员在软件开发中发现有些类似的程序结构会不断地出现,反复地被使用,然后就有人把这些成熟的代码设计经验总结了成一套解决方案,提供给大家学习、交流和使用。
学习设计模式有什么好处
- 提高代码的可重用性,避免一些重复的工作,减少大段大段的复制粘贴。
- 提高开发速度。有时候我们会发现思考了好久想出来的设计方案其实早已经在设计模式里面了。
- 当项目的规模渐渐变大,如果系统没有足够的灵活性和可扩展性,开发和维护会变得苦不堪言。而设计模式的正确使用可以大大减轻这个问题。
- 懂得设计模式对于学习那些大牛写的开源工具、库和框架是很有帮助的,我觉得这一点很重要。研究开源框架时,找找里面的设计模式。
学习设计模式应该注意的地方
- 要搞清楚每个设计模式要解决的问题是什么,使用的场景条件是什么,是如何解决的,优缺点是什么。
- 最好能运用这个设计模式解决一个问题。
- 不能为了使用设计模式而使用设计模式,过多的设计模式也会使系统变得臃肿不堪。这个说着简单,其实很难量化,只能多学习大牛的源码,加上自己去体会。
- 并不所有的编程语言都需要用到这些设计模式,有些设计模式完全是为了弥补某些语言的缺陷而出现的,可以参考知乎上的一个回答。
设计模式的七个原则
单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者说就一个类而言,应该只有一个引起它变化的原因。
一个类应该只负责一个职责。负责的职责越多,类就会越复杂。当各种职责耦合在一个类中时,这个类被多次复用的可能性就会大大减小。而且当我们需要修改其中一个职责的代码时,很可能会影响到别的职责。因此最好将各个职责分离,放在不同的类中。
其实这个原则在我们的工作中是很常见的,比如下面这个例子:将用户照片上传到某图床,返回一个url,然后将这个url和用户其他信息一起保存到数据库,最后把所有数据显示在页面。
初始方案是一个UserService类实现,其中uploadPhoto()方法上传照片,saveUser()方法保存信息,displayUser()方法返回所有数据到页面。
现在这个类一看就不符合单一职责原则,我想只要有点经验的程序员都会向下面这么使用吧。
一个类只负责做一类事,这就是单一职责原则。
开闭原则(Open-Closed Principle, OCP):对扩展开放,对修改关闭。即尽量在不修改原有代码的情况下进行扩展。
ChartDisplay类想调用Barchart和PieChart的display方法就得通过if语句根据type来判断应该执行哪个实现的display方法。
public class ChartDisplay { public void display(String type) { if (type.equals("pie")) { PieChart chart = new PieChart(); chart.display(); } else if (type.equals("bar")) { BarChart chart = new BarChart(); chart.display(); } } }
重构后如果需要添加新的chart类就不需要修改ChartDisplay类,只需要新增一个AbstractChart实现类就好。
public class ChartDisplay { private AbstractChart chart; public void setChart(AbstractChart chart) { this.chart = chart; } public void display() { chart.display(); } }
开闭原则的关键就是抽象。将具体业务放在实现类,添加业务类型的时候,就可以通过添加实现类来扩展,而不用修改原来的类。
里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
这个原则的意思就是一个子类应该可以替换掉它的父类,而且程序不会产生异常。
因为一般来说,父类都是对外开放的接口,不能随便修改。我们使用子类重写父类的方法的时候应该按照父类的规定来写,不要违背父类的意思。比如ArrayList实现了List接口,他们都有size方法,size方法定义的就是返回List中元素的个数。我们自己写一个List实现类的时候就不要返回别的东西,不然子类就不能替代父类的位置,就不满足这个原则。
还有子类最好不要重写父类已经实现的方法,而是增加自己持有的新方法。
class A { public int func1(int a, int b) { return a - b; } } class B extends A { public int func1(int a, int b) { return a + b; } public int func2(int a, int b) { return func1(a, b) + 100; } }
很明显,B类的func1的做法是错误的。如果我在一个程序中使用了A类的func1方法,然后把A类替换成B类,这时程序就会出现问题。
依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。要针对接口编程,而不是针对实现编程。
要针对接口编程,而不是针对实现编程。这句很好理解。因为接口是稳定的,实现是容易变化的,所以我们要针对接口编程,这样实现的变化才不会对客户端产生影响。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
依赖倒转原则的例子和开闭原则的例子差不多。很多情况下开闭原则、里氏代换原则和依赖倒转原则会同时出现,看起来也比较类似。
开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口去提供所有功能。
接口隔离原则要求每个接口有自己专门的工作,不要把所有的功能一股脑的塞到一个接口里面。
一个接口的功能过多就会导致该接口使用起来很不灵活,因为如果客户端只需要使用其中的一个方法就得把所有的方法实现。所以,最好确保每一个接口只扮演一个角色,做好一份工作。
当然,在使用接口隔离原则的时候,我们也不能矫枉过正,把接口分得太细,导致接口泛滥。
合成复用原则(Composite Reuse Principle, CRP):复用时要尽量使用组合/聚合关系(关联关系),少用继承。
使用合成复用原则的原因就是,继承比组合/聚合关系(关联关系)的耦合性强。
现在有一个UserDAO类需要连接MySQL数据库进行数据操作,有一个DBUtil类中有获取MySQL数据库连接的方法getConnection。如果使用继承方案,使用UserDAO继承DBUtil得到获取数据库连接的方法getConnection,那么当需要新增一种连接Oracle数据库的方式时,就必须修改UserDAO或者DBUtil类的源码。
这是违反开闭原则的。因此应该使用关联复用来代替继承复用。
将DBUtil变为UserDAO类中的属性,采用依赖注入的方式把DBUtil对象注入UserDAO对象中。这样UserDAO和DBUtil之间的关系由继承关系变为关联关系。如果需要对DBUtil的功能进行扩展,添加其子类的实现就可以,比如OracleDBUtil。
迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
我觉得这个原则有两个意思:1、一个对象应该对其他对象保持最少的了解,对于被依赖的类而言,意思就是向外公开的public方法应该尽可能的少;2、不要和“陌生人”说话、只与你的直接朋友通信。直接朋友通常表现为关联,聚合和组成关系,两个对象之间联系很紧密,通常以成员变量,方法的参数和返回值的形式出现。如果两个对象之间不需要直接通信,那这两个对象就不应该有直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简单来说就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
第一条简单点说就是把应该设为private的属性和方法设置为private
public class Operation { public void openDoor() { System.out.println("把冰箱门打开"); } public void putIn() { System.out.println("把大象放进冰箱"); } public void closeDoor() { System.out.println("把冰箱关闭"); } public void operate() { openDoor(); putIn(); closeDoor(); } } public class Person { private Operation operation; public void setOperation(Operation operation) { this.operation = operation; } public void operate() { // 要把大象放入冰箱可以执行以下流程 operation.openDoor(); operation.putIn(); operation.closeDoor(); // 或者 operation.operate(); } }
上面的Operation类暴露的方法太多,会让使用者产生迷惑。
应该改成这样:
public class Operation { private void openDoor() { System.out.println("把冰箱门打开"); } private void putIn() { System.out.println("把大象放进冰箱"); } private void closeDoor() { System.out.println("把冰箱关闭"); } public void operate() { openDoor(); putIn(); closeDoor(); } }
对于Person来说,它只关心把大象放进冰箱的整体操作,不关心分了几步。所以这个Operation类只需要暴露一个操作方法operate()。
第二条有点像代理的意思。比方说,现在有个工人Operator是专门做把大象放进冰箱这个工作的(这工作真奇葩~)。我们普通人就不需要自己做这件事情,我们只要跟这个工人沟通,让他去做这件是就可以了,毕竟他更专业。普通人擅长与人沟通,工人擅长做这个工作。这就叫只依赖应该依赖的对象。
public class Operation { private void openDoor() { System.out.println("把冰箱门打开"); } private void putIn() { System.out.println("把大象放进冰箱"); } private void closeDoor() { System.out.println("把冰箱关闭"); } public void operate() { openDoor(); putIn(); closeDoor(); } } public class Operator { private Operation operation; public void operate(){ operation.operate(); } } public class Person { private Operator operator; public void operate() { // 要把大象放入冰箱可以执行以下流程 operator.operate(); } }
这里通过引入一个专门用于操作的中间类(Operator)来降低操作和人的耦合度。
设计模式的类型
23个GoF设计模式一共可以分为三种类型:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns。
创建型模式:不使用 new 运算符直接实例化,对象隐藏创建逻辑的方式。
- 工厂模式(Factory Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 单例模式(Singleton Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
结构型模式:这些设计模式着重于类和对象的组合。
- 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 过滤器模式(Filter、Criteria Pattern)
- 组合模式(Composite Pattern)
- 装饰器模式(Decorator Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
行为型模式:这些设计模式着重于对象之间的通信。
- 责任链模式(Chain of Responsibility Pattern)
- 命令模式(Command Pattern)
- 解释器模式(Interpreter Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 观察者模式(Observer Pattern)
- 状态模式(State Pattern)
- 空对象模式(Null Object Pattern)
- 策略模式(Strategy Pattern)
- 模板模式(Template Pattern)
- 访问者模式(Visitor Pattern)