原则 or 不原则?
我们都知道设计模式的学习是进阶Java架构师的必经之路,而设计模式的设计都或多或少遵循了一些设计原则,所以设计原则的学习能够加深对于设计模式的理解,也是很有必要的。当然,设计原则是死的,甚至于有时候我们在实际场景中对于设计模式的使用是反设计原则的。
那么,我们到底还有没有必要学习设计原则呢?
当然是,有!如果我们把软件设计比作一门武功,那么设计原则乃至于设计模式都是武学中的一招一式。在金庸武侠中,武侠的最高境界是出手无招,那么怎么做到出手无招呢?就像《笑傲江湖》里风清扬对令狐冲讲的:先将每一招融会贯通,然后尽量忘得一干二净。
所以要想做到出手无招,必须还得先有招!
同理,在软件设计中,我们先得对于设计原则以及设计模式融会贯通,然后再忘掉它们,而后做到不拘泥与招式,能够设计出最符合当前实际情况的软件。
单一职责原则
我今天给大家带来的就是第一个设计原则:单一职责原则(Single responsibility principle),简称SRP原则
。
顾名思义,单一职责原则就是一个类或者模块只负责完成一个职责(
A class or module should have a single reponsibility
)
还记得之前在学习Spring框架的相关知识时,总是会听到六字真言:高内聚,低耦合。 当时总是觉得不能体会其中真意,而今正好,单一职责原则的目的正是为了设计出高内聚,低耦合
的软件。如下图两种设计思路都是违反了单一职责原则:
当我们碰到耦合度高的模块时,按照单一职责原则我们要进行必要的拆分,如图:
而当我们碰到低内聚的模块时,按照单一职责原则,我们则要进行一些必要的合并,如图:
现实层面
设计模式或者说设计原则是思想层面上的东西,编程语言只是一种实现方式,因此它不局限于编程语言。当然,它也体现在我们的日常生活中。如下图:
我们在家中使用的沐浴设备可能大多都是这种:一个旋钮控制水的温度,如往左旋水的温度越高,往右旋水的温度越低。可是这种设计往往有个痛点:当你将旋钮旋到一个临界点时,往往不是水温高了就是水温低了,即很难控制水温到一个舒适的温度。我们来试着画图分析一下:
按照上图,我们可以很容易的得出这个水温旋钮的耦合度过高。而按照我们之前的分析,当我们碰到耦合度过高的模块时,需要进行模块的拆分,如图:
所以,这个水温旋钮应该如上图般设计才合理。当然,这个可不是本博主的凭空想象啊。这个设计正是最近本博主去外面出差所住酒店的设计。如下图所示,这个水温调节起来是真的方便,这个澡洗得是真的舒服。
当然,本博主主要致力于Java相关方向的一些知识的分享,怎能不上Java代码呢?接下来给大家上代码!!!
代码层面
可能是由于人类的惰性,我们编写的代码大多容易耦合度过高,而不是内聚过低。所以本文中的案例都是讲解高耦合如何解耦的。关于内聚过低的实际案例以及相关思考,欢迎各位小伙伴在文章下方评论或者私信我。
如以下代码,我们创建了一个交通工具类Vehicle
的对象vehicle
,分别传入不同的交通工具执行了run()
方法:
/**
* @author guqueyue
* @Date 2020/6/23
**/
public class SingleResponsibility {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("摩托車");
vehicle.run("汽車");
vehicle.run("飞机");
vehicle.run("轮船");
}
}
/**
* 交通工具类
* 方式一
* 1.违反了单一职责原则
* 2.解决方案:根据交通工具运行方法不同,分解成不同的类
*/
class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + " 在公路上运行...");
}
}
运行应用程序,控制台输出如下:
这个时候我们会发现,什么?飞机和轮船竟然也在公路上跑!!!这肯定不行呀,那怎么办呢?
方案一
这个时候我们可能一般都会想到:既然交通工具分天上飞的,地上跑的以及水里游的,那我来个海陆空分类不就好了吗?再用if-else
来判断一下。自己简直太机智了,然后刷刷刷写下以下代码:
/**
* @author guqueyue
* @Date 2020/6/23
**/
public class SingleResponsibility1 {
public static void main(String[] args) {
Vehicle1 vehicle = new Vehicle1();
vehicle.run("摩托車", "road");
vehicle.run("汽車", "road");
vehicle.run("飞机", "air");
vehicle.run("轮船", "water");
}
}
/**
* 交通工具类
* 用if - else 区分不同的交通工具
*/
class Vehicle1 {
/**
* 非常复杂
* @param vehicle 交通工具
* @param type 类型
*/
public void run(String vehicle, String type) {
if ("road".equals(type)){
System.out.println(vehicle + " 在公路上运行...");
}else if ("air".equals(type)) {
System.out.println(vehicle + " 在天空上飞...");
}else if ("water".equals(type)) {
System.out.println(vehicle + " 在水里游...");
}else {
throw new RuntimeException("小朋友,请输入正确的类型");
}
}
}
运行应用程序,控制台打印如下:
这样一看似乎觉得已经非常完美了,但是我们试着画图来分析一下:
这样一画图,我们可以很轻易的看出:Vehicle1模块
能够得出天上飞,公路上运行以及水里游三种结果,也就是说Vehicle1模块
的耦合度非常高。 那么这样会有什么缺点呢?来,帮你总结好了:
- 类的复杂度非常高。
这么多的if-else,能不高吗? - 类的可读性以及可维护性较差。 试想一下,如果要增加可以遁地、潜水以及在外太空翱翔的交通工具类型,又要往里写if-else判断;而之后有一天需要删除某些不要的交通工具类型,这个if-else是不是删的你很崩溃!
- 变更有风险。 即当我因为职责1的需求改变时,而改变
Vehicle1模块
时可能会造成其他职责的执行错误。
比如在陆地上的交通工具,需要增加一个时间参数,以便判断是否需要限速时;那么这个时候海和空的交通工具就会受到影响。
方案二
所以,这个时候方案二闪亮登场。根据单一职责原则:一个类或者模块只负责完成一个职责就好啦。 所以,我们可以将之前的Vehicle1类
划分为三个类:
/**
* @author guqueyue
* @Date 2020/6/23
**/
public class SingleResponsibility2 {
public static void main(String[] args) {
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("摩托車");
roadVehicle.run("汽車");
AirVehicle airVehicle = new AirVehicle();
airVehicle.run("飞机");
WaterVehicle waterVehicle = new WaterVehicle();
waterVehicle.run("轮船");
}
}
/**
* 遵守了单一职责原则
* 但是改动很大,需要分解类,修改客户端
* 改进:直接修改Vehicle类,这样改动比较少
*/
class RoadVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在公路上跑...");
}
}
class AirVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在天上飞...");
}
}
class WaterVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在水里游...");
}
}
运行程序,结果如下:
这下看起来似乎很完美,也非常符合我们之前所说的单一职责原则:一个类只做一件事情! 但是这样做的话,似乎对于代码的改动非常的大,那么有没有避免如此改动大的方案呢?
方案三
之前我们说到方案二对于代码的改动很大,那么我们特此提出方案三:
/**
* @author guqueyue
* @Date 2020/6/23
* 适用于类的方法比较少
**/
public class SingleResponsibility3 {
public static void main(String[] args) {
Vehicle2 vehicle2 = new Vehicle2();
vehicle2.runRoad("摩托车");
vehicle2.runRoad("汽车");
vehicle2.runAir("飞机");
vehicle2.runWater("轮船");
}
}
/**
* 方式三
* 没有对原来的类做大的修改,只是增加了方法
* 没有完全遵守单一职责原则(类级别上),但是在方法级别上仍然遵守了单一职责
*/
class Vehicle2 {
public void runRoad(String vehicle) {
System.out.println(vehicle + "在公路上跑...");
}
public void runAir(String vehicle) {
System.out.println(vehicle + "在天空中飞...");
}
public void runWater(String vehicle) {
System.out.println(vehicle + "在水里游...");
}
}
运行程序,结果如下:
结果没毛病!但是这个时候有人可能就会问了:博主你等等,你之前不是说单一职责原则是一个类只负责一个职责吗?你一个类都负责三个职责了,这不是违反了单一职责原则了吗?
话虽如此,可这种写法虽然没有在类的级别上遵循单一职责原则,但它在方法级别上遵循了单一职责原则呀!
最后
在上文的分析中,我们总共提出了三种方案,其中方案二最符合单一职责原则。那么是不是我们应该严格按照规定,每次设计软件的时候都按照方案二的思路来设计呢?其实不然!!伟大的马克思主义从小就教育我们:实践才是检验真理的唯一标准。切不可生搬硬套,应当视具体情况而定!方案一和方案三也是有它们的适用场景的:
- 方案一:适用于代码逻辑简单。
- 方案三:适用于类中的方法的数量较少。
所以,
我们在实际软件开发过程中,不必严格遵守原则,完全可以先设计一个粗粒度的类,然后再根据业务的发展情况,对代码进行重构嘛!
就比如近期微服务
的概念一直很火热,面试的时候面试官不问你点微服务
好像都不正常。那是不是在实际开发过程中,我们不管三七二十一不用单体架构就直接采用微服务呢?其实不然,单体架构比起微服务来说有以下两个优点:
- 开发速度快。
- 所需资源少,也就是性价比高。
我们在业务初期,单体架构够用的情况下,就用单体架构就好了。当当前架构不再适合当前业务的发展时,才考虑是否要更换架构。
最后的最后,由于本人水平有限,本篇博客内容如有不足之处,还望各位雅正。如果觉得写的还行,帮忙点个赞呗!关于Java设计模式的博客我会一直坚持写下去的(加关注,不迷路哦)( ̄▽ ̄)~*
参考资料: 尚硅谷_韩顺平.图解Java设计模式
KK.一种可拯救产品与开发关系的良药——“高内聚低耦合”