概念:设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
以下是对上面有下划线的关键字的通俗解释:
- 反复使用:在实际的开发中被使用的次数太多了,比如:单例模式、外观模式、工厂模式等
- 多数人知晓:作为一个程序员即使没看过相关书籍不了解所有设计模式的具体内容也会知道一些非常常见的几种设计模式,而且有些设计模式即使你不知道,在日常的代码开发中也会使用。
- 分类编目:就是说可以找到一些特征去划分这些设计模式,从而进行分类。
- 代码设计经验:代码写多了,就会积累代码设计的经验,设计模式就是从这些经验中总结出来的用来解决一些特定场景中的问题的方法。
优点:
设计模式可以帮助我们改良项目的代码,增强代码的健壮性、可扩展性,为以后开发和维护铺平道路。有过开发经验的人都知道一个项目的代码设计好坏对之后开发的影响,特别是从事维护项目工作的人应该有更深的体会(虽然我并未从事过维护= 。=),可以想象当一个看起来很简单的需求,但是由于项目设计问题,并没有考虑到这个需求的变更时或则由于需求不断变更导致代码变得臃肿,而导致当你修改其中一处可能导致其他功能出现异常,加深了维护代码的难度,这是一个非常严重的后果
注意点:
设计模式是可以改善项目的设计,但过多的使用甚至滥用将会导致项目变得复杂,难以读懂。所以当我们第一次设计一个系统时,请将你确定的变化点处理掉,不确定的变化点千万不要假设它存在,如果你曾经这么做过,那么请改变你的思维,让这些虚无的变化点在你脑子中彻底消失。因为我们完全可以使用另外一种方法来处理那些不确定的变化点,那就是重构。至于重构等讨论完设计模式后再进行探讨。
六大原则:
- 单一职责原则
概念:就一个类而言,应该仅有一个引起它变化的原因
描述的意思是每个类都只负责单一的功能,切不可太多,并且一个类应当尽量的把一个功能做到极致。如果一个类承担的职责过多,就等于把这些职责耦合在一起,这种耦合会导致脆弱的设计,即当其中一个职责发生变化时将会影响这个类完成其它职责的功能。以下代码就没有遵守该原则
public class Calculator { public int add() throws Exception{ File file = new File("E:/data.txt"); BufferedReader br = new BufferedReader(new FileReader(file)); int a = Integer.valueOf(br.readLine()); int b = Integer.valueOf(br.readLine()); return a+b; } public static void main(String[] args) throws Exception { Calculator calculator = new Calculator(); System.out.println("result:" + calculator.add()); } }
例子中的add方法拥有从文件中读取数字和对数字进行加法运算的职责。这将会导致我们若想增加一个减法运算的功能还得copy读取文件职责,当运算增加时,将会照成大量重复的代码。我们用两个类来实现这两个职责。
用来读取文件数据的类,如下代码所示
public class Reader { int a,b; public Reader(String path) throws Exception{ BufferedReader br = new BufferedReader(new FileReader(new File(path))); a = Integer.valueOf(br.readLine()); b = Integer.valueOf(br.readLine()); } public int getA(){ return a; } public int getB(){ return b; } }
用来进行运算的类,代码如下所示
public class Calculator { public int add(int a,int b){ return a + b; } public static void main(String[] args) throws Exception { Reader reader = new Reader("E:/data.txt"); Calculator calculator = new Calculator(); System.out.println("result:" + calculator.add(reader.getA(),reader.getB())); } }
将职责拆分后当我们需要多个运算时就不会出现重复编写读取文件的代码。
以上例子是出自左潇龙的博客,单一职责原则是我觉得六大原则当中最应该遵守的原则,在项目的开发过程中遵循它,减少大量重复的代码不会增加项目的复杂性,反而会令你的程序看起来井然有序。
总结:在开发过程中当发现有多于一个的动机会去改变一个类,那这个类就具有了多于一个的职责,就需要我们考虑对该类的职责进行分离。
- 里氏替换原则
概念:任何基类可以出现的地方,子类一定可以出现
描述的意思是一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,也就是说将程序中的父类替换为子类,程序的行为不会发生改变,若不遵守这个原则,当我们需要将父类替换为子类的时候还需要考虑是否会影响代码的正常运行,以下代码就没有遵守这个原则
父类代码public class Parent { public void method() { System.out.println("执行 Parent.method()"); } }
子类代码
public class Children extends Parent { public void method() { System.out.println("执行 Children.method()"); throw new RuntimeException(); } }
测试代码
public class Test { public static void main(String[] args) { new Parent().method(); new Children().method(); } }
从以上代码的运行结果可以看出,当我们将父类替换为子类的时候就抛出了异常。要想不出现以上问题,最直接的办法就是子类不要重写父类的方法,但是在某些特定的场景重写是不可或缺的,重写的时候我们需要进行判断重写带来的好处是否大于它所造成的危害,若是也可适当的违反这个原则
总结:有时候违反这个原则能让我们以失去一小部分的代价得到很多,有时则相反,我们若想活学活用,必须深刻的理解这个原则的意义所在
- 接口隔离原则
概念:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上
描述的意思是一个接口拥有的功能应尽可能的少,这样当一个类实现该接口时,不需要实现那些没用的方法,若违反这个原则往往会出现需要编写一个空的方法来实现那些没用的方法,而这会让调用方认为你有该功能,但真正调用时却没有的到想要的结果。若你不遵守这个原则,那你将有可能写出如下接口public interface Mobile { public void call();//手机可以打电话 public void sendMessage();//手机可以发短信 public void playBird();//手机可以玩愤怒的小鸟? }
以上接口中的方法playBird明显不是没一个手机都应该有的功能,当一个非智能的手机实现这个接口时,由于它并不能玩所以该方法只能空着,但调用方确发现非智能手机也有该方法,这就造成了误解
所以我们应该新建一个智能手机的接口继承手机接口并将playBird方法移动到智能手机接口中,这样就不会出现以上问题,智能手机的接口代码如下public interface SmartPhone extends Mobile{ public void playBird();//智能手机可以玩愤怒的小鸟 }
总结:遵守该原则能让我们实现一个接口时,不需要实现那些没用的功能,也就不会给调用方造成假象
- 依赖倒置原则
概念:高层模块不应该依赖低层模块,两个都应该依赖抽象。抽象不应该依赖细节,反之细节应该依赖抽象
描述的意思是细节即实现会随着场景的变化而发生改变,所以是不稳定的,反之抽象是稳定的,所以当依赖于抽象的时候不会因为其中一个实现的改变而影响另一个
上述单一职责原则中的计算器例子中,计算器就是依赖于数据读取类的,这种编程就不太好。若我们将从文件中读取修改为数据库,为了不影响计算器的代码就必须将读取类进行大量修改,当然我们也可以新增一个数据库读取的类DBReader并件计算器类中的所有Reader替换为DBReader。当再增加多种读取的方式,我们就需要一个更好的做法去实现,即通过抽象出一个读取的接口,代码如下所示public interface Reader { public int getA(); public int getB(); }
总结:在实际的编程中我们应该让客户端调用接口,而不是调用具体的实现,这样可以让我们的程序更具有灵活性
- 迪米特法则
概念:一个对象应当对其他对象有尽可能少的了解,不和其它类有太多接触
描述的意思是若一个类知道或者说是依赖于另外一个类太多细节,这样会导致耦合度过高,应该将细节全部高内聚于类的内部,其他的类只需要知道这个类主要提供的功能即可,类之间的耦合越弱,越有利于复用,当其中一个处于弱耦合的类被修改,不会对有关系的类造成影响。若不遵守该法则你可能写出如下代码public class Reader { int a,b; private String path; private BufferedReader br; public Reader(String path){ this.path = path; } public void setBufferedReader() throws FileNotFoundException{ br = new BufferedReader(new FileReader(new File(path))); } public void readLine() throws NumberFormatException, IOException{ a = Integer.valueOf(br.readLine()); b = Integer.valueOf(br.readLine()); } public int getA(){ return a; } public int getB(){ return b; } }
这样当客户端需要获取a和b的值时,需要写成如下代码所示
public class Client { public static void main(String[] args) throws Exception { Reader reader = new Reader("E:/test.txt"); reader.setBufferedReader(); reader.readLine(); int a = reader.getA(); int b = reader.getB(); } }
以上代码可以看出我们获取a和b之前先调用了reader的setBufferedReader方法和readLine方法,这样客户端与Reader之间的耦合度就过高了,其实客户端只需要获取a和b的方法,并不需要知道Reader还有前两个方法,我们可以将Reader类改成如下所示
public class Reader { int a,b; private String path; private BufferedReader br; public Reader(String path) throws Exception{ super(); this.path = path; setBufferedReader(); readLine(); } //注意,我们变为私有的方法 private void setBufferedReader() throws FileNotFoundException{ br = new BufferedReader(new FileReader(path)); } //注意,我们变为私有的方法 private void readLine() throws NumberFormatException, IOException{ a = Integer.valueOf(br.readLine()); b = Integer.valueOf(br.readLine()); } public int getA(){ return a; } public int getB(){ return b; } }
总结:尽可能将一个类的细节全部写在这个类的内部,不要漏出来给其他类知道,即尽量降低类中成员的访问权限,实现高内聚低耦合
- 开闭原则
概念:对于扩展是开放的,对于修改是关闭的
开闭原则是面向对象设计的核心。尽量的遵守这个规则可以实现代码的可维护、可扩展、可复用、灵活性好,在日常的开发中我们应该对那些经常发生改变的部分进行抽象,但也不能刻意对各个部分进行抽象,防止不成熟的抽象和抽象本身一样重要。
当我们的程序遵守了前5个规则,那么这个程序也就比较符合这个原则
总结:当需求修改或新增时,最好都是通过新增代码进行实现,而不是修改现有的代码