公众号|松华说|设计模式学习分享
  • 分享在京东工作的技术感悟,还有JAVA技术和业内最佳实践,大部分都是务实的、能看懂的、可复现的

扫一扫
关注公众号

设计模式学习分享

博客首页文章列表 松花皮蛋me 2020-02-10 19:49

1、如何评价代码质量的高低


最常用到的几个评判代码质量的标准是:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。

  • (1)、可维护性:是否能在不破坏原有代码设计、不引入新的BUG的情况下,能够快速地修改或者添加代码;
  • (2)、可读性:代码是否符合编码规范、命令是否达意、注释是否详尽等等;
  • (3)、可扩展性:代码是否可以通过预留的功能扩展点来扩充功能,而不用改动大量的原始代码;
  • (4)、灵活性:易扩展、易复用、易用;简洁性:KISS原则,尽量保证代码简单;

2、抽象类与接口的应用场景区别


如果要表示一种is-a的关系,并且是为了解决代码复用问题,就使用抽象类。如果要表示一种has-a的关系,并且为了解决抽象而非代码复用问题,就使用接口。

3、基于接口而非实现编程(基于抽象而非实现编程)


做软件设计的时候要有抽象意识、封装意识、接口意识,不要暴露任何实现细节,保证上下游调用稳定性,降低耦合性。

4、多用组合少用继承


利用组合、接口、委托,解决层次过深、过复杂的继承关系,避免影响代码的可维护性。

5、常见的不满足单一职责原则的设计

  • (1)、类中大量的方法都是集中操作类中的某几个属性;
  • (2)、类中的代码行数、属性过多;
  • (3)、类依赖的其他类过多;
  • (4)、私有方法过多;
  • (5)、比较难给类起一个合适的名字;

6、常见的违背里氏替换原则的设计

  • (1)、子类违背父类注释中所罗列的任何特殊说明;
  • (2)、子类违背父类声明要实现的功能;
  • (3)、子类违背父类对输入、输出、异常的约定;

7、开闭原则


尽量让修改操作更集中、更少、更上层。

8、接口隔离原则


接口隔离原则提供了一种判断接口是否满足单一职责原则的方法。如果调用者只使用部分接口或者接口的部分功能,那接口的设计就不够职责单一。

9、依赖注入


依赖注入是一种具体的编程技巧,关注的是对象创建与类之前的关系,目的是提高代码的扩展性,我们可以灵活地替换依赖的类。实际上依赖注入和基于接口而非实现编程都是基于开闭原则思路的。

10、如何写出满足KISS原则的代码

  • (1)、不要过度优化;
  • (2)、不要使用同事可能不懂的技术来实现代码;
  • (3)、不要重复造轮子,减少维护成本;

11、如何提高代码的可复用性


功能语义重复和代码执行重复其实都是违反DRY原则的,那如何提高代码的可复用性呢?

  • (1)、减少代码耦合;
  • (2)、满足单一职责原则;
  • (3)、模块化;
  • (4)、业务与非业务逻辑分离;
  • (5)、通用代码下沉;
  • (6)、应用模板等设计模式;

12、迪米特法则


不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。

13、代码分层的作用


代码分层的作用有:代码复用、隔离变化、隔离关注点、提供可测性、应对系统的复杂性。
其实编程就是一个拼插积木的过程。系统划分成若干个层次,每一层专注解决某个领域的问题。大问题分解成若干个子问题,如果子问题还没有直接解决,则继续分解成子子问题,直到可以直接解决的程序。

14、重构思绪


一定要建立持续重构意识,把重构作为开发必不可少的部分,融入到日常开发中。

15、常见的测试不友好的代码有下面五种

  • (1)、代码中包含未决行为逻辑;
  • (2)、滥用可变全局变量;
  • (3)、滥用静态方法;
  • (4)、使用复杂的继承关系;
  • (5)、高度耦合的代码;

16、如何通过封装、抽象、模块化、中间层等解耦代码?


单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则等。当然还有一些设计模式,比如观察者模式。

17、让你快速地改善代码质量的20条编程规范

  • (1)、关于命名,命名的关键是能准确达意。
  • (2)、关于注释,注释的内容包括做什么、为什么、怎么做、如何用,但是注释有一定的维护成本,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提供代码可读性。
  • (3)、类内成员的排列顺序。先写成员变量后写函数,并且按照作用域大小依次排列,也可以把有调用关系的函数放在一块。

18、如何发现代码质量问题,常规checklist

  • (1)、目录设置是否合理、模块划分是否清晰、代码结构是否满足”高内聚、松耦合”?
  • (2)、是否满足经典的设计原则和设计思想(SOLID\DRY\KISS\YAGNI\LOD等)?
  • (3)、设计模式是否应用得当?
  • (4)、是否有过度设计?
  • (5)、代码是否容易扩展?如果要添加新功能,是否容易实现?
  • (6)、代码是否可复用?是否可以复用已有的项目代码或者类库?是否有重复造轮子?
  • (7)、代码是否易测试?单元测试是否全面覆盖各种正常和异常的情况?
  • (8)、代码是否易读?
  • (9)、是否符合编码规范(比如代码风格)?

19、如何发现代码质量问题,业务需求checklist

  • (1)、代码是否实现了预期的业务需求?
  • (2)、逻辑是否正常?
  • (3)、是否处理了各种异常情况?
  • (4)、日记打印是否得当?是否方便debug排查问题?
  • (5)、接口是否易用?是否支持幂等、事务等?
  • (6)、代码是否存在并发问题?是否线程安全?
  • (7)、性能是否有优化空间?比如SQL\算法是否可以优化?
  • (8)、是否有安全漏洞?比如输入输出校验是否全面?

20、程序出错该返回啥?错误码、NULL值、空对象、异常对象?


对于查找函数,数据不存在并非是异常情况,它是一种正常行为,所以返回不存在语义的NULL值比返回异常更加合理。


对于函数抛出的异常,我们有三种处理方法:直接吞掉、直接往上抛出、包裹成新的异常抛出。如果调用者关心该异常就往上抛出。如果向上抛出的异常和业务概率没有太大相关性,或者暴露过多实现细节,或者调用者看到这个异常,并不能理解异常到底代表了什么,该如何处理,那就必须包裹成新的异常。


如果函数是private类私有的,只在类内部被调用,完全在自己的掌控中,不需要判断非空。否则,为了尽可能提高代码的健壮性,就需要做NULL值或空字符串的判断。

21、单例模式


单例模式通常用来避免资源访问冲突和表示业务概念的全局唯一性。但是也存在一些缺点,比如对OOP特性的支持不友好、隐藏类之间的依赖关系、对代码的扩展性不友好(如资源池设计动态修改参数)、对代码的可测性不友好。基于此,我们可以使用工厂模式、IOC容器等解决方案替换单例模式。


单例中对象的唯一性的作用范围内是进程内的。更严格地说,对于Java语言来说,单例类对象的唯一性的作用范围是类加载器。因为在Java中,两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它的类加载器不同,那这两个类就必定不相等。


其他单例模式的问题:如何线程唯一的单例?可以利用Java语言本身的ThreadLocal工具类。如何实现集群环境下的单例?将单例对象序列化存储到外部空间,使用时进行反序列化。如何实现一个多例模式?可以利用ConcurrentHashMap的putIfAbsent方法。多例模式有点类似枚举单例和享元模式。

22、工厂模式


当创建逻辑比较复杂,是一个”大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。

判断要不要使用工厂模式的参考指标:封装变化(创建逻辑的变更对调用者透明)、代码复用、隔离复杂性(调用者无需了解过程)、控制复杂度(让原本的类职责更单一)。


23、DI容器


配置解析、对象创建、对象生命周期管理、提供执行入口。


24、建造者模式


针对类属性有默认值、不可变性、有依赖关系、有约束条件的复杂型对象,通常使用建造者模式来构建,然后在build方法中进行合法性校验。

25、原型模式


原型设计模式:如果对象的创建成本比较大,而同一个类的不同对象之间区别不大,在这种情况下,我们可以利用对已有对象进行复制的方式来创建对象,以达到节省创建时间的目的。


不过复制对象时尽量使用深拷贝的方式,比如先将对象序列化,然后再序列化成新的对象。


26、代理模式


代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能,比如在代理类中通过委托进行增强。普通的代理模式都需要为每个原始类创建代理类,过于麻烦。所以我们可以使用动态代理来进行优化。


代理模式的应用场景,包括业务系统的非功能性需求开发(监控、统计、限流、事务、日记)、在RPC和缓存中的应用等。可以看到,它的目的主要是控制访问。

27、桥接模式


桥接模式可以理解为将抽象和实现解藕,让它们可以独立变化。另外一种的理解是一个类存在多个独立变化的维度,我们就应该通过组合的方式,让这些维度可以独立进行扩展。


桥接模式的经典应用是JDBC驱动,我们只需要修改很少的配置或者代码,就可以从一种数据库切成另外一个数据库。

28、装饰器模式

通过将原始类以组合的方式注入到装饰器类中,以增强原始类的功能,而不是使用继承,避免继承结构爆炸。另外,装饰器类通常和原始类继承相同的抽象类或者接口,此时如果方法不需要增强,重新调用原始类的方法即可。

应用示例:Java IO操作中的BufferedInputStream、InputStream、FilterInputStream。

29、适配器模式


适配器是一种事后补救策略,它提供一个新的接口,将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容不能一起工作的类可以一起工作。


适配器的类型分别为类适配器(基于继承)、对象适配器(甚于组合)。它的应用场景包括:封装有缺陷的接口设计、统一多个类的接口设计、替换依赖的外部系统、兼容老版本接口、适配不同格式的数据。比如兼容老版本接口时,不直接删除需要废弃的接口而是利用组合委托给新版本实现。


应用示例:JAVA的日志框架Slf4j。

31、享元模式

享元模式通过缓存技术,将重复或者相似的对象设计成不可变对象,让其他对象共享它们,从而达到节省空间的目的。不过,它对垃圾回收不友好。


应用示例:棋牌类游戏、文本编辑器(特点是格式有限)、Integer对象(特点是提前创建好缓存)、String字符串(特点是运行时根据需要创建和缓存)。

32、观察者模式


将频繁变更的方法后置处理逻辑独立成对象,这些对象都实现了消息接收接口。当方法执行后,依次遍历这些观察者对象的消息接收方法,或者以异步非阻塞的方式进行遍历。


应用示例:邮件订阅、RSS Feeds

33、模板方法


在一个方法中定义好算法骨架,并将某些步骤推迟到子类中实现,这样子类就可以在不改变整体行为的情况下将业务代码通过扩展点内嵌到骨架中。


应用示例:模板方法定义为final,避免被重写,需要子类重写的方法定义为abstract。

34、回调方法

回调是指将包含执行方法的类对象传递给另外一个通用类,由它来触发执行方法。这样可以复用通用类的功能,比如创建连接、关闭资源等。可以看出,同步回调类似模板方法,区别是回调是基于组合方式实现的。异步回调则类似于观察者模式。

应用示例:ShutdownHook优雅关闭、JdbcTemplate类。

35、策略模式


通过查表法动态获取对应的策略,而不是通过冗长的if-else条件判断。可以看到,这种模式解藕了策略的定义、创建、使用,有效控制了代码的复杂度,让每个部分都不至于过于复杂、代码量过多。

36、职责链模式


将多个处理器通过链表或者数组的方式组成一条链路,然后请求依次经过处理器的逻辑处理,或者某个处理器中断后不再往后执行,另外,我们还可以基于配置文件动态加载处理器列表。显然,这种模式能让我们的代码易于扩展。


应用示例:校验链Javax.servlet.Filter、拦截器Spring Interceptor。

37、状态模式


状态模式的过程为,满足其中一种条件后就将当前状态转移为新的状态并执行相关动作。一般情况下借助二维数组来表示状态转移图。


应用示例:游戏状态机引擎。


38、迭代器模式

迭代器的设计思路如下,迭代器定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。可以看到,这种模式将容器和迭代操作分离,各自职责更加单一,而且我们可以轻松替换迭代算法。


不过,在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。但是,我们可以利用快照版本进行优化,每次新增或者删除容器元素时不真正执行操作,而是标注操作时间,当遍历时根据时间进行过滤。

应用示例:数据库中的游标。

持续更新中……。更多精彩,欢迎关注公众号:松花皮蛋的黑板报