一、设计模式的目的
1、代码重用性
2、可读性
3、可扩展性
4、可靠性
5、使程序呈现高内聚、低耦合的特性
二、设计模式的七大原则
- 单一职责原则
- 接口隔离原则
- 依赖倒置原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 合成复用法原则
1、单一职责
设计思想:一个类或者一个方法只负责一项职责,尽量做到类的只有一个行为原因引起变化。
问题:假如有类Class1完成职责T1,T2,当职责T1或T2有变更需要修改时,有可能影响到该类的另外一个职责正常工作。
优点:类的复杂度降低、可读性提高、可维护性提高、扩展性提高、降低了变更引起的风险。
实际项目中考虑的点:根据具体的业务去划分类或者方法职责问题,怎么拆分,拆分粒度的把控,怎么做到重复利用,扩展性的考虑,怎么避免变更引起的风险等。
2、接口隔离原则
设计思想:类间的依赖关系应该建立在最小的接口上
问题:类A通过接口interface依赖类B,类C通过接口interface依赖类D,如果接口interface对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
优点:提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情,为依赖接口的类定制服务。只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
实际项目中考虑的点:不要建立庞大的接口,建议抽取公共base接口,然后针对不同类的职责定义自己的接口,从而实现公共接口和属于类自己定义的接口,多接口实现。
3、依赖倒置原则
设计思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象(高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类)
本质:依赖倒置原则的本质就是通过抽象(接口或抽象类)使个各类或模块的实现彼此独立,互不影响,实现模块间的松耦合
问题:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
问题解决方案:将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率
优点:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。
实际项目中考虑的点:在spring中模块的划分就是遵循了依赖倒置的原则,将类依赖关系变成接口,接口的实现独立,如果需要更改实现,只需要改接口的实现类就可以。
实例:
/** * 不考虑设计原则下的写法 */ public class Persion { /** * 实例:人吃水果 * 开始想吃苹果就要去调吃苹果的类方法 * 后面想吃桔子就要去调吃桔子的类方法 * 这时候先相关改变就要变化eating方法的参数,显然每吃一次不同的水果都要去改变eating方法,违反了依赖倒置原则 * @param apple */ public void eating(Apple apple) { System.out.println(apple.eat()); } static class Apple{ public String eat() { return "苹果"; } } static class Orange{ public String eat() { return "桔子"; } } public static void main(String[] args) { Persion designPrinciples = new Persion(); designPrinciples.eating(new Apple()); } } /** * 优化后,让不同的水果都去实现 一个公共的接口,eating参数传抽象的接口,这样想吃不同的水果就不需要去更改eating方法参数了 */ public class Persion { public void eating(Shuiguo shuiguo) { System.out.println(shuiguo.eat()); } interface Shuiguo { String eat(); } static class Apple implements Shuiguo{ @Override public String eat() { return "苹果"; } } static class Orange implements Shuiguo{ @Override public String eat() { return "桔子"; } } }
4、里氏替换原则
设计思想:里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
问题:如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
优点:代码共享,减少创建类的工作量、提高代码的重用性、子类可以形似父类,又异于父类、提高父类的扩展性,实现父类的方法即可随意而为.
实际项目中考虑的点:
1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法(不能把Y基因变成了Z,O(∩_∩)O哈哈~)。
2、子类中可以增加自己特有的方法,但需要在父类中声明(要跟到老婆走,是不先要给父亲说些呢?)。
3、当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松,这个
从参数个数上考虑。
4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更加严格,如父类要
求返回List,那么子类就应该返回List的实现ArrayList,父类是采用泛型,那么子类则不能采用泛型,而是具体的返
回。
实例:
package yuanze; /** * 将父类声明为抽象类 * * @author Administrator * */ public abstract class Super { /** * 定义一个父类无须实现的抽象方法 */ public abstract void breathe(); /** * 定义父类需要实现的方法 */ public void eat() { System.out.println("我吃面"); } } package yuanze; /** * 定义子类继承父类 * * @author Administrator * */ public class Child extends Super { /** * 子类扩展的方法,但是在父类中有声明 */ @Override public void breathe() { // TODO Auto-generated method stub System.out.println("呼吸"); } } //客户端调用 package yuanze; public class Test { public static void main(String[] args) { Super c = new Child(); // 由子类的实例替代父类的实例 c.eat(); } }
5、开闭原则
设计思想:对拓展开放,对修改关闭:比如当某个业务增加,不是在原类增加方法,而是增加原类的实现类
问题:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
优点:可以提高代码的可复用性、粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性、可以提高软件的可维护性、遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
实际项目中考虑的点:从接口关系中去看: 要建立共同的接口,创建不同的业务类去实现公共的接口,从而达到业务类的隔离互不影响。从继承关系中去看:继承需要扩展类的并重写需要的方法,从而达到不影响原先的的方法。
实例:
//书籍接口 public interface IBook{ public String getName(); public String getPrice(); public String getAuthor(); } //具体书籍 public class NovelBook implements IBook{ private String name; private int price; private String author; public NovelBook(String name,int price,String author){ this.name = name; this.price = price; this.author = author; } public String getAutor(){ return this.author; } public String getName(){ return this.name; } public int getPrice(){ return this.price; } } //客户端调用 public class Client{ public static void main(Strings[] args){ IBook novel = new NovelBook("笑傲江湖",100,"金庸"); System.out.println("书籍名字:"+novel.getName()+"书籍作者:"+novel.getAuthor()+"书籍价格:"+novel.getPrice()); } }
现在来了新需求:项目投产生,书籍正常销售,但是我们经常因为各种原因,要打折来销售书籍,这是一个变化,我们要如何应对这样一个需求变化呢
修改接口
在IBook接口中,增加一个方法getOffPrice(),专门用于进行打折处理,所有的实现类实现此方法。但是这样的一个修改方式,实现类NovelBook要修改,同时IBook接口应该是稳定且可靠,不应该经常发生改变,否则接口作为契约的作用就失去了。因此,此方案否定。修改实现类
修改NovelBook类的方法,直接在getPrice()方法中实现打折处理。此方法是有问题的,例如我们如果getPrice()方法中只需要读取书籍的打折前的价格呢?这不是有问题吗?当然我们也可以再增加getOffPrice()方法,这也是可以实现其需求,但是这就有二个读取价格的方法,因此,该方案也不是一个最优方案。通过扩展实现变化
我们可以增加一个子类OffNovelBook,覆写getPrice方法。此方法修改少,对现有的代码没有影响,风险少,是个好办法。方案:增加一个打折类去继承具体的书籍类,并覆盖重写获取价格的方法
public class OffNovelBook extends NovelBook{ public OffNovelBook(String name,int price,String author){ super(name,price,author); } //覆写价格方法,当价格大于40,就打8析,其他价格就打9析 public int getPrice(){ if(this.price > 40){ return this.price * 0.8; }else{ return this.price * 0.9; } } }
现在打折销售开发完成了,我们只是增加了一个OffNovelBook类,我们修改的代码都是高层次的模块,没有修改底层模块,代码改变量少,可以有效的防止风险的扩散。
6、迪米特法则
设计思想:迪米特法则又叫最少知道原则,类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。于是就提出了迪米特法则。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
问题:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大
优点:迪米特法则可降低系统的耦合度,使类与类之间保持较低的耦合关系。
实际项目中考虑的点:
实例:
现在在线教育网的老板要知道在线教育网有多少课程,他直接向团队领导下命令即可,再由团队领导查询有多少课程。 //**************违反迪米特法则的写法**********************// //课程类:Crourse类 public class Course{ } //Boss类 public class Boss{ public void commandCheckNumber(TeamLeader teamLeader){ List<Course> courseList = new ArrayList<Course>(); for(int i = 0 ;i < 20;i++){ courseList.add(new Course()); } teamLeader.checkNumberOfCourses(courseList); } } //TeamLeader类 public class TeamLeader { public void checkNumberOfCourses(List<Course> courseList){ System.out.println("在线课程的数量是:"+courseList.size()); } } //测试类 public class Test { public static void main(String[] args) { Boss boss = new Boss(); TeamLeader teamLeader = new TeamLeader(); boss.commandCheckNumber(teamLeader); } }
这样做没有啥问题,但违反了迪米特法则,Boss类不需要和Course类发生交流,他只需和TeamLeader类发生交流即可。
Course类应由TeamLeader类创建,不应由Boss类创建.所以我们改进为如下代码
//Boss类: public class Boss { public void commandCheckNumber(TeamLeader teamLeader){ teamLeader.checkNumberOfCourses(); } } //Course类 public class Course { } //TeamLeader类 public class TeamLeader { public void checkNumberOfCourses(){ List<Course> courseList = new ArrayList<Course>(); for(int i = 0 ;i < 20;i++){ courseList.add(new Course()); } System.out.println("在线课程的数量是:"+courseList.size()); } } //Test测试类 public class Test { public static void main(String[] args) { Boss boss = new Boss(); TeamLeader teamLeader = new TeamLeader(); boss.commandCheckNumber(teamLeader); } }
7、合成复用原则
设计思想:在系统中尽量多使用组合或聚合关联关系,少使用或不使用继承关系。
问题:通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。 1.继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。 2.子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。 3.它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
优点:采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。 1.它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。 2.新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。 3.复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
实际项目中考虑的点:如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
实例:
汽车分类管理程序。
分析:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。图 1 所示是用继淨:关系实现的汽车分类的类图。
用继承关系实现的汽车分类的类图:
从图 1 可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,其类图如图 2 所示。 用组合关系实现的汽车分类的类图: