设计原则,是设计模式的内功心法,基本全部的设计模式都是基于设计原则进行的具体化,若是说设计模式是如何操做的话,那么设计原则就是为什么这么作的基石,所以,只要咱们能充分理解设计原则,那么在此基础上,对设计模式就能更好的理解,甚至能本身设计出一种设计模式来。
java
一个类或模块,只须要完成一个范围的功能,而不要搞得大而全。
程序员
例如咱们设计一个社交网站,如今要存储用户信息,类设计以下:web
public class UserInfo { private String name; private String like; private String location; }
如今,咱们想一想该类的设计是否符合单一职责原则?
答案是可能符合,也可能不符合。那么判断依据是什么呢?
缘由就是类或模块职责的判断是根据业务来定的,并无一个广泛认同的规则。例如,若是该须要要的网站还提供卖东西的功能,那么用户的地址信息,就是一个十分关键的信息,且该块功能就须要抽离出来做为一个单独的模块,此时,地址信息放在这里就不合适了,违反了单一职责原则。
但若是地址信息,只是一个值对象,也就是说其只是一个展现属性,那么放在这里就是合适的。
综上所述,能够看到单一职责原则并非设计出来就一成不变的,其须要结合业务发展的具体状况来判断。所以咱们在设计之初,能够考虑一个大而全的类,但随着业务的发展须要,须要持续不断的进行优化(也就是持续重构的思想)。
算法
单一职责,由于类的设计比较小而精,所以能够极大提升代码的可维护性和可读性。
此外由于每一个类或模块只涉及本身的功能部分,所以,也作到了高内聚。
编程
但类的设计也不是越单一越好,由于若是拆分的过细的话,可能上层一个类须要修改,会致使下层全部依赖其的类都要修改,又影响了代码的可维护性,所以仍是要根据业务须要来合理评估,重点是感受要对。
设计模式
字面意思,一个类的设计,应该要对拓展开放,对修改关闭。所以这里的重点就是如下定义该如何判断:api
public static void main(String[] args) { Demo demo = new Demo(); demo.consume(1); } // 根据传递过来的级别来进行不一样的会员逻辑判断 public void consume(int type) { if (type == 1) { Console.log("您好,1级会员!"); } if (type == 2) { Console.log("您好,2级会员!"); } }
如今,又提出一个新的需求,还须要根据对应的会员等级进行对应的金额扣除,若是是上述的设计方式,那么修改的方式则是下面这样:tomcat
public void consume(int type, int price) { if (type == 1) { Console.log("您好,1级会员,扣除金额{}", price); } if (type == 2) { Console.log("您好,2级会员,扣除金额{}", price); } }
很明显,这样的方式有问题,若是还要再传递一个字段,例如优惠比例,那么依照该方案,则还须要修改接口定义,这就意味着调用方都须要修改,测试用例也须要对应的修改。框架
那么若是按照开闭原则的话,该如何设计呢?
首先咱们对代码进行下重构ide
// 将全部相关属性封装起来 public class Vip { private int type; private int price; private int radio; }
针对每种处理方式,根据他们的公有行为抽象出一个抽象层:
public interface VipHandler { void consume(Vip vip); }
每种特殊处理方式实现对应的抽象:
public class FirstVipHandler implements VipHandler { @Override public void consume(Vip vip) { if (vip.getType() == 1) { Console.log("您好,1级会员,扣除金额{}", vip.getPrice() * vip.getRadio()); } } } public class SecondVipHandler implements VipHandler { @Override public void consume(Vip vip) { if (vip.getType() == 2) { Console.log("您好,2级会员,扣除金额{}", vip.getPrice() * vip.getRadio()); } } }
经过这样的处理方式,在每次接到新的任务后,就不须要从新修改原有的逻辑方法,能够直接进行拓展便可:
// 根据传递过来的级别来进行不一样的会员逻辑判断 public void consume(Vip vip, VipHandler vipHandler) { vipHandler.consume(vip); }
能够看到即便是上述的方式来拓展代码,仍旧会修改原有代码,那么这种方式是违反了开闭原则吗?
在这里,咱们判断其并符合了开闭原则,由于咱们判断是修改仍是拓展,并不能只是简单的根据看是否修改了原有代码,真正核心的关键问题应该是:
**
在上述的修改后,咱们若是加一种特殊的状况,并无修改到原先的处理逻辑类,这也就意味着原先的代码不会引入一些可能的 bug,针对原始代码的测试用例也仍是能够照常的进行编写,而不用再根据新的改动而进行改动。
开闭原则的关键点是代码的可拓展性,即如何快速的拥抱变化,当每次新的任务来后,没必要修改原始代码,而直接在原有的基础上进行拓展便可。
关闭修改是保持原有代码的稳定性。
子类对象能够代替父类对象出现的任何地方,并保证原来程序逻辑行为不被破坏。
由于要保证子类对象不能破坏原有程序逻辑行为,所以该方式跟多态的区别是:若是子类进行了重写,并在重写的逻辑中加入了跟父类对应方法不一样的逻辑,那么该方式能够称之为多态,但就不符合里氏替换原则了。
该原则最重要的做用是指导子类的设计,保证在替换父类的时候,不改变原有程序的逻辑行为。
在这里,重点是逻辑行为的不改变,这就意味着,咱们能够对实现的细节进行修改,只要保证业务含义不变,那么就是符合里氏替换原则的。
所以,针对这种状况,有一种用途是能够 改进 原有的实现,例如原先采用的排序算法比较低效,那么能够设计一个子类,而后重写对应排序算法,保证逻辑不发生变化。重点是,咱们作的是改进,无论如何改,排序的业务含义是不变的。
实现方式,则是按照协议进行编程,关键是子类重写过程当中,要保证不破坏原有函数声明的输入、输出、异常以及注释中罗列的任何特殊状况声明。
首先,咱们要对接口进行定义,明确其特殊含义,对于接口来讲,咱们将其分为两种类型的表现形式。
下面,咱们分别对其进行介绍。
public interface UserInfoService { boolean login(); void getUserInfoByPhone(); void deleteUserByPhone(); }
看上述这个接口定义是否符合接口隔离原则???
其实这跟单一职责原则同样,也是要看业务发展的。例如,若是该接口是提供给后台管理系统来使用的,那么没有问题,做为一个后台系统的 admin 权限人员固然能够有不少操做的能力。
但若是该接口是给第三方用户来使用的话,就不是很合适了。由于删除操做是一个高权限能力,做为用户来讲,通常是没有权限作的,那么在设计时,对应实现类就不该该实现所有接口内定义的方法,这就是接口隔离原则中所说的,不强制依赖接口中的全部方法。
**
而是,根据具体的定义,将其进行拆分,对应权限的实现类实现对应的权限行为。
public class Statistics { private int max; private int min; private int sum; //...... public Statistics count(List<Integer> data){ Statistics statistics = new Statistics(); // 计算 Statistics 中的每一个值 return statistics; } }
首先,咱们来看上述方法定义是否符合接口隔离原则???
在这里,咱们仍是要结合具体的业务场景才能作出结论,若是使用该函数的调用者,在大部分场景下都须要用到其中的大部分字段,那么该设计就是能够的。
可是若是每次只用到其中的几个,那么该设计就不合理了,其会浪费大量的无效计算能力,影响性能。在该场景下,就须要进行拆分。
public int max(List<Integer> data) { return data.stream().max(Statistics::compare).get(); } public int min(List<Integer> data) { return data.stream().min(Statistics::compare).get(); }
对于依赖翻转原则来讲,有不少看着很像的定义,咱们分别对其进行介绍,看看其都是什么含义,他们之间又有什么关联。
控制翻转,针对是在原有的程序设计流程中,整个程序的运行流程是直接交由程序员来控制的,可是若是使用控制翻转的思想,则是在一个架子中,已经定义好了执行的流程,而只是预先定义好了拓展点,后续程序员所能修改的只有拓展点,开发人员在拓展点里添加相关业务逻辑便可。
public abstract class VipProcess { public abstract boolean isVip(); public void consume() { if (isVip()) { Console.log("vip hello"); } else { Console.log("get out"); } } } public class Vip extends VipProcess { @Override public boolean isVip() { return true; } public static void main(String[] args) { VipProcess vip = new Vip(); vip.consume(); } } public class CommonPeople extends VipProcess { @Override public boolean isVip() { return false; } public static void main(String[] args) { VipProcess vip = new CommonPeople(); vip.consume(); } }
上述方式,就是经过模板方法来实现的控制翻转,提供一个拓展的 isVip() 逻辑来交给程序员来实现,而框架根据实际实现的方法返回来决定下面的程序流转。
依赖注入更好理解,一句话归纳:不用程序员显式经过 new
来建立对象,而是经过构造函数,函数传递的方式来传递对象。
即A类若是须要依赖B类,不是经过在 A 中 new 一个 B 出来,而是在外面建立好 B 后,传递给 A。经过这样的方式,能够在需求改变中,灵活的替换传递参数(B 实现 C 接口的话)。
而更进一步,如今一些框架都提供了 DI 的功能,只须要简单的配置一下相关类对象,所需参数等,框架就会自动接管对象的建立流程已经生命周期等。(AutoWired)
定义:高层模块不依赖于底层模块,而是经过一个抽象层来解耦。 该定义其实在咱们的日常业务开发中,不怎么会用到,由于咱们日常就是高层依赖着底层,例如 Controller 依赖 Service,Service 依赖 Repository,该原则的重点仍是指导框架层面的开发。 例如 Tomcat ,咱们知道 Tomcat 的运行是咱们将程序写完后,打成 war 包扔到对应目录就能够启动了,而 Tomcat 和应用程序就是经过一个共同的抽象 **Servlet **来关联的。 Tomcat 不直接依赖于底层实现:Web 程序,而是跟 Web 都依赖于 Servlet,而 Servlet 不依赖于具体的Tomcat 实现的 Web 的具体细节。 经过该实现方式,在编码中,能够灵活的进行替换,好比咱们仍是一个 web 程序,可是运行的容器不使用 tomcat 了,也能够无缝的进行切换,只要保证要替换的容器,仍是依赖于 Servlet 规范便可。