做者:yangwq
博客:https://yangwq.cnjava
软件设计是一门关注长期变化的学问,平常开发中需求不断变化,那咱们该怎么编写出能够支撑长期变化的代码呢?大多数人都认同的解决方案是利用设计模式,这里就有一个问题:怎么融汇贯通的将设计模式应用到实际项目中呢?这就是咱们本篇文章的主题:设计原则。程序员
我的认为设计原则是软件设计的基石之一,全部语言均可以利用设计原则开发出可扩展性、可维护性、可读性高的项目,学好设计原则,就等于咱们拥有了指南针,不会迷失在各个设计模式的场景中。spring
郑晔老师的《软件设计之美》指出:设计模式是在特定问题上应用设计原则的解决方案。咱们能够类比设计原则是心法,设计模式是招式,二者相辅相成,虽然脱离对方都能使用,可是不能融会贯通。设计模式
本章主要涉及的设计原则有:mybatis
接下来对各个原则进行详细说明,有错误或语义不明确的地方欢迎你们指正。app
本原则的定义经历过一些变化。之前的定义是:一个模块(模块、类、接口)仅有一个引发变化的缘由,后面升级为: 一个模块(模块、类、接口)对一类且仅对一类行为者负责。框架
咱们重点关注的是“变化”一词。下面咱们用代码来进行示例:ide
背景:设计一个订单接口,能作到建立、编辑订单和会员的赠送及过时。函数
public interface OrderService { int createOrder(); int updateOrder(); // 下单完成后分配vip给用户 int distributionVIP(); // vip过时 int expireVIP(); }
OrderService包含对订单、VIP的操做,无论是订单业务或VIP业务的改变,咱们都须要改变这个类。这样有什么问题?有多个引发OrderService变化的缘由致使这个类不能稳定下来,对VIP代码的改动有可能致使本来运行正常的订单功能发生故障,没有作到高内聚、低耦合。工具
一个模块最理想的状态是不改变,其次是少改变。咱们能够将对VIP的处理单独放到一个类:
public interface OrderService { int createOrder(); int updateOrder(); } public interface VIPService{ // 下单完成后分配vip给用户 int distributionVIP(); // vip过时 int expireVIP(); }
这样咱们对订单或VIP的改动都不会影响到对方正常的功能,极大程度上减小了问题发生的几率。
这个定义比上面的定义多加了一个内容:变化的来源。
上面的例子可能区分不出来变化的来源,像vip这类功能通常都是订单系统体系内的。从下面这个例子说明:
背景:在上面例子的背景下,增长对地址信息的维护。
public interface OrderService { int createOrder(); int updateOrder(); // 订单地址的修改 int updateOrderAddress(); }
OrderService中对订单地址的修改,多是订单负责人提出的需求,也多是物流部门提出来:须要共用订单地址。
这里就须要区分两种业务场景。
若是是订单负责人提出的,那上面这个设计就是合理的,由于咱们维护的是订单附属内容,并且变化的来源只有订单系统。
但若是是物流部门提出共用订单地址,那就须要将更改地址的接口抽离出来,由于这个需求变化的来源有两拨人:多是订单,也多是物流部门。改动以下:
public interface OrderService { int createOrder(); int editOrder(); } public interface AddressService { // 订单修改地址 int updateAddressByOrder(); // 物流修改地址 int updateAddressByLogistics(); }
为了职责明确咱们有对接口的命名进行重构,这样更容易被使用者接受,经过将地址的变化隔离在AddressService,后续维护地址只用修改这个类,提高了代码的可读性和可维护性。
定义:对扩展开放,对修改关闭。简而言之: 不修改已有代码(尽量不更改已有代码的状况下),新需求用新代码实现。
如何作到?分离关注点,找出共性构建模型/抽象,设计扩展点。
代码示例:
背景:设计一套通用的文件上传下载功能,须要支持本地盘和阿里云OSS。一开始的设计多是这样的:
public void FileUtil { void upload(UploadParam uploadParam) { if(type == 1){ // 上传文件到本地盘 }else if (type == 2){ // 上传文件到阿里云OSS } } void download(DownloadParam downloadParam){ if(type == 1){ // 从本地盘下载文件 }else if (type == 2){ // 从阿里云OSS下载文件 } } }
上面的设计有什么问题?首先第一点UploadParam 和 DownloadParam 参数职责太重,不一样方式的上传、下载参数混合在一个类,可读性不高,并且加入其余存储方式的时候可能只加了上传,漏掉了下载的改动,容易产生问题。
那咱们先经过分离关注点:不一样存储方式都须要提供对应的上传、下载操做。因而咱们能够将动做拆分红上传、下载,参数须要按不一样场景选用不一样的对象。改动后以下:
// 全部参数的父类接口 public interface BaseFileParam{ } // 统一的上传下载接口类 public interface FileService<U,D>{ /** * 上传 */ void upload(); /** * 下载 */ void download(); } // 抽象实现,将参数做为属性放到类中,子类可使用 public abstract class AbstractFileService<U,D> implements FileService<U,D>{ protected U uploadParam; protected D downloadParam; public AbstractFileService() { } protected FileService<U, D> buildUploadParam(U uploadParam){ this.uploadParam = uploadParam; return this; } protected FileService<U, D> buildDownloadParam(D downloadParam){ this.downloadParam = downloadParam; return this; } protected U getUploadParam() { return uploadParam; } protected D getDownloadParam() { return downloadParam; } } // OSS实现 public class OssFileServe extends AbstractFileService<OssFileServe.OssUpload, OssFileServe.OssDownload> { /** * 上传到阿里云 */ @Override public void upload() { } /** * 从阿里云下载文件 */ @Override public void download() { } public class OssUpload implements BaseFileParam{ } public class OssDownload implements BaseFileParam{ } } // 本地盘实现 public class LocalFileService extends AbstractFileService<LocalFileService.LocalFileUploadParams, LocalFileService.LocalFileDownloadParams> { /** * 上传到本地磁盘 */ @Override public void upload() { } @Override public void download() { } public static class LocalFileUploadParams implements BaseFileParam { } public static class LocalFileDownloadParams implements BaseFileParam { } } // 使用入口 public class FileServiceDelegate { public FileService<? extends BaseFileParam,? extends BaseFileParam> getFileService(String type, BaseFileParam upload, BaseFileParam download){ if("local".equals(type)){ return new LocalFileService().buildUploadParam(upload != null ? (LocalFileService.LocalFileUploadParams) upload : null) .buildDownloadParam(download != null ? (LocalFileService.LocalFileDownloadParams) download : null); }else if ("oss".equals(type)) { return new OssFileServe().buildUploadParam(upload != null ? (OssFileServe.OssUpload) upload : null) .buildDownloadParam(download != null ? (OssFileServe.OssDownload) download : null); }else { throw new RuntimeException("未知的上传类型"); } } public void upload(String type, BaseFileParam baseFileParam){ getFileService(type,baseFileParam, null).upload(); } public void download(String type, BaseFileParam baseFileParam){ getFileService(type,null, baseFileParam).download(); } }
以上是比较粗糙的方案,只作案例演示。后续若是须要加入亚马逊S3存储,咱们须要改动的点:
// 加入S3实现 public class S3FileService extends AbstractFileService<S3FileService.S3UploadParams, S3FileService.S3DownloadParams> { /** * 上传到S3 */ @Override public void upload() { } /** * 从S3下载文件 */ @Override public void download() { } public class S3UploadParams implements BaseFileParam { } public class S3DownloadParams implements BaseFileParam { } } // 修改入口类 public class FileServiceDelegate { public FileService<? extends BaseFileParam,? extends BaseFileParam> getFileService(String type, BaseFileParam upload, BaseFileParam download){ if("local".equals(type)){ return new LocalFileService().buildUploadParam(upload != null ? (LocalFileService.LocalFileUploadParams) upload : null) .buildDownloadParam(download != null ? (LocalFileService.LocalFileDownloadParams) download : null); }else if ("oss".equals(type)) { return new OssFileServe().buildUploadParam(upload != null ? (OssFileServe.OssUpload) upload : null) .buildDownloadParam(download != null ? (OssFileServe.OssDownload) download : null); } // 加入S3处理 else if("s3".equals(type)){ return new S3FileService().buildDownloadParam(upload != null ? (S3FileService.S3DownloadParams) upload : null) .buildDownloadParam(download != null ? (S3FileService.S3DownloadParams) download : null); }else { throw new RuntimeException("未知的上传类型"); } } public void upload(String type, BaseFileParam baseFileParam){ getFileService(type,baseFileParam, null).upload(); } public void download(String type, BaseFileParam baseFileParam){ getFileService(type,null, baseFileParam).download(); } }
上面咱们修改了两个地方,一个是加入了S3的实现类,另外一个是更改入口类加入了S3的处理,这就符合新功能用新代码实现,但可能有人说改动了入口类,其实只要改动的代码没有影响到原有的功能,小幅度的修改是能够接受的。
定义:子类必须可以替换其父类,并保证原来程序的逻辑行为不变及正确性不被破坏。
如何实现?站在父类的角度设计接口,子类须要知足基于行为的IS-A关系,更具体的来说:子类遵照父类的行为约定,约定包含:功能主旨,异常,输入,输出,注释等。
违背功能主旨:
public interface OrderService { Order updateById(Order order); } public class OrderServiceImpl { public Order findById(Order order) { // 其实是经过订单编号进行更新的 return orderMapper.updateBySn(order); } }
父类的定义本来是按订单ID更新,在子类实现中却变成了按订单编号更新,这个方法就违背了功能主旨。会出现什么问题?使用者会发现执行结果与本身指望的不一致,并且有隐藏BUG:一开始传了订单编号,后面订单编号没了,这个方法就报错了,更严重一点,若是是使用mybatis的xml判断了编号不为空进行条件拼接,此时因为编号为空就没有了条件过滤而后更改了整个表的数据。
异常:父类规定接口不能抛出异常,而子类抛出了异常。
输入:父类输入整数类型就行,子类要求正整数才能执行。
输出:父类执行方法要求有异常时返回null,子类重写后直接将异常抛出来了。
关于里氏替换原则,咱们就只要记住一点:从父类角度设计行为一致的子类。
定义:不该强迫使用者依赖于它们不用的方法。 通俗的理解:对接口设计应用单一职责,根据调用者设计不一样的接口。
示例:
public class UserController{ int addUser(User user); int updateUser(User user); int deleteUser(int id); // 锁定用户 int lockUser(User user); }
上面是一个对订单crud的接口,如今有其余项目组的同事须要锁定用户的功能,而后你可能一拍脑壳直接把上面整个接口UserController扔给他(或者直接扔一个swagger文档),这样同事会很懵逼:我只要锁定用户就行,为何还要这么多接口?
这样作暴露的问题:
因此咱们尽可能要最小化暴露接口,根据不一样的调用者仅提供他们当前须要的接口,提供的公共接口越多越难以维护。
接口隔离原则与单一职责的区别:
一、单一职责要求的的是模块、类、接口的职责单一,
二、接口隔离原则要求的是暴露给使用者的接口尽量少。
能够这么理解:一个类某个职责有10个接口都暴露给其余模块使用,按单一原则来说是合理的,可是按接口隔离来说是不容许的。
定义:高层模块不直接依赖底层模块,依赖于抽象,底层模块不依赖于细节,细节依赖于抽象。
这一点若是咱们是使用spring开发的项目就已经用到了。spring的依赖注入就是依赖倒置原则的体现。
// 之前没有使用spring的时候,咱们是这样初始化service的 // 存在的问题:一、若是须要替换成一个新的实现类,改动点太多,简单点说就是高耦合; // 二、使用者不须要关注具体的实现类,只关注有哪些接口能用就行; // 三、对象实例不能共享,每一个使用的地方都是新建的实例,实际上用同一个实例就好了。 UserService userService = new UserServiceImpl();
经过spring的IOC容器,咱们只要定义好依赖关系,IOC容器就能够帮咱们管理对应的实例,起到了松耦合的做用。
还有其余的使用场景吗?
有,举例:
public class UserServiceImpl { private KafkaProducer producer; public int addUser(User user){ // 建立用户 // 发送消息到消息队列,由感兴趣的系统订阅并消费。 producer.send(msg); } }
这里初看没有什么问题,但若是后续咱们更换了kafka为rabbitmq,那上面使用到kafka的类都须要从新调整。
咱们利用"高层模块不直接依赖底层模块,依赖于抽象"对上面代码进行调整,让咱们的实现类UserServiceImpl不直接依赖KafkaProducer,而是依赖接口类MessageSender。
public class UserServiceImpl { private MessageSender sender; public int addUser(User user){ // 建立用户 // 发送消息到消息队列,由感兴趣的系统订阅并消费。 sender.send(msg); } } public interface MessageSender { void send(Map<String,String> params); } // kafka 实现 public class KafkaProducer implements MessageSender{ public void send(Map<String,String> params) { } }
这样一来,就算咱们切换成RabbitMq,改动的点无非是对MessageSender实现的更改,而有了spring的IOC容器,咱们很容易就能够更改实例实现。
// rabbitmq 实现 public class RabbitmqProducer implements MessageSender{ public void send(Map<String,String> params) { } }
控制反转:控制反转是一个比较笼统的设计思想,并非一种具体的实现方法,通常用来指导框架层面的设计。这里的控制指的是程序执行流程的控制,反转是从程序员变为框架控制。
依赖注入:一种具体的编码技巧,不直接使用new建立对象,而是在外部将对象建立好后经过构造函数、方法、方法参数传递给类使用。
这三个原则是偏理论性的概念,主要目的是指导咱们学习设计原则后不要过分设计。
定义: 尽可能保持简单。保持简单可让咱们的代码可读性更高,维护起来也更容易。但这是一个比较抽象的概念:对于“简单”的定义没有统一规范,每一个人的理解都不一致,这个时候就须要code review,同事有不少疑问的代码就要考虑是否是代码不够“简单”。
实践过程当中怎么编写知足KISS原则的代码?如下几点供你们参考:
定义: 你不会须要它。咱们能够这样理解:如非必要,勿增功能。
这一个原则咱们能够用在两个方面:需求和代码实现。
对于产品人员提出的需求,按照二八原则,80%的功能是用不上的,因此咱们能够不作对用户没有价值的需求。
对于开发人员的代码实现,除非编写的模块之后会频繁变化,这种状况咱们能够提早构建扩展点,但若是模块变化不多,咱们就不须要作过多的扩展点,保持功能正常运行就行。
KISS原则和YAGNI原则区别:
KISS原则关注的怎么作,YAGNI原则关注的是需不须要作。
定义:不要重复本身。普遍的认知是不写重复代码,更深刻一点的理解是不要对你的知识和意图进行复制。
在我看来:解决重复代码是每一个程序员都会作的事情,可是重复的代码必定要解决吗?首先要明白解决重复代码的重点是创建抽象,那这个抽象有没有存在的意义?咱们应该根据实际的业务场景,若是发现引发该抽象改变的缘由超过一个,这说明该抽象没有存在的意义。
例如,咱们开发crud接口中常见的VO和Entity:
public class UserEntity { private String username; private String name; private Integer age; private String password; } public class UserVO { private String username; private String name; private Integer age; // 用户拥有的菜单 private List menuList; }
咱们若是按DRY原则将重复的代码合并到一个类:
public class BaseUser{ private String username; private String name; private Integer age; private String phone; } public class UserEntity extends BaseUser{ private String password; } public class UserVO { // 用户拥有的菜单 private List menuList; }
改为这样会有什么问题?若是后续UserVO不容许暴露age属性或者须要对手机号加密,这个时候就须要改动BaseUser和UserEntity,对UserVO的维护就会改动到BaseUser和UserEntity,一方面违反了单一职责,另外一方面须要对发现全部使用BaseUser、UserEntity、UserVO的地方进行测试,增长了维护成本。
基于以上考虑,咱们须要将对UserVO的改动隔离起来:还原成刚开始重复代码的场景。
实行DRY原则的方式:
三次法则(Rule of Three):
第一次先写了一段代码,不考虑复用性;
第二次在另外一个地方写了一段相同的代码,能够标记为需清除重复代码,可是暂不处理;
再次在另外一个地方写了一样的代码,如今能够考虑解决重复代码了。
本篇的宗旨是给你们树立一个观点:设计原则是设计模式的基础,而不是设计模式的附属物。设计模式是在特定问题应用设计原则的解决方案。可是只用设计原则开发软件离目标是有误差的,因此咱们也要借鉴设计模式:熟悉不一样场景下设计原则的使用方式,这样才能开发出可扩展性、可维护性、可读性高的软件。
本篇文章若有错误或语义不明确的地方欢迎你们指正。