设计模式
为什么学习设计模式
应对面试中的设计模式相关问题;告别写被人吐槽的烂代码;提高复杂代码的设计和开发能力;让读源码、学框架事半功倍;为你的职场发展做铺垫。
设计模式作用
解耦
创建型设计模式
将创建和使用代码解耦
单例模式
一个类只允许创建一个实例,这个类就是单例类
为什么使用单例(why,when)
控制对于共享资源的顺序访问
降低内存、文件句柄等资源的开销
有些数据在系统中只应保存一份
如何实现单例(hwo)
懒汉(延迟加载)
饿汉(预先加载,早失败,早发现)
双重检查机制(减少锁的冲突)
静态类(懒加载 Holder里实例化)
枚举(最简单优雅)
单例有什么问题
如何理解单例唯一性
指进程内的唯一实例,进程间是不唯一的。
实现线程唯一实例
思路是用ConcurrentHashMap,key:线程ID,value为要创建的实例
java中有ThreadLoca实现线程唯一实例
实现集群唯一实例
进程间唯一,其实就是分布式唯一
多例模式
一个类可以创建多个对象,但是个数是有限制的
使用静态块+hashMap定义
使用concurrentHashMap定义
工厂模式
分类
简单工厂
创建一个工厂类,根据类型来返回不同对象
每次创建一个新对象
每次创建
类比:某手机店1个橱柜,摆放各式手机
工厂方法
创建多个工厂类,负责创建对应的对象
类比:某手机店n个橱柜,每个橱柜摆放对应款式手机
抽象工厂
创建一个综合工厂,创建多种类型的对象
类比:大型商超,里面有手机店,有电脑店,有家电店。
何时使用
当创建逻辑比较复杂,考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。
如规则配置解析,存在多个if-else,根据不同的类型创建不同对象,将if-else创建对象的逻辑放到工厂类中
如果创建对象的逻辑不复杂,考虑简单工厂
如果创建对象的逻辑复杂,考虑工厂方法
单个对象本身创建较为复杂,需要组合其他类对象,做各种初始化操作
是否要使用工厂模式
封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
代码复用:创建代码抽离到独立的工厂类之后可以复用。
隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
建造者模式builder
可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象
为什么使用建造者模式 why
避免过度参数导致的可读性和易用性
容易执行约束条件,校验逻辑
实现类对象不可变
避免对象的无效状态
工厂模式与建造者模式区别
工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
原型模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的
基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
何为“对象的创建成本比较大”?
对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取
结构型设计模式
将不同功能代码解耦
行为型设计模式
将不同行为代码解耦
行为型设计模式解决的就是“类或对象之间的交互”问题。
观察者模式
模板模式
作用
复用
所有子类可以复用父类中提供的模板方法的代码
扩展
框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码情况下,基于扩展点定制化框架功能
回调 vs 模板模式
回调的优势
像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。
策略模式
职责链模式
在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
职责链模式常用在框架开发中,用来实现框架的过滤器、拦截器功能
三种职责链常用的应用场景:过滤器(Servlet Filter)、拦截器(Spring Interceptor)、插件(MyBatis Plugin)。
Servlet Filter 采用递归来实现拦截方法前后添加逻辑
Spring Interceptor 的实现比较简单,把拦截方法前后要添加的逻辑放到两个方法中实现
MyBatis Plugin 采用嵌套动态代理的方法来实现,实现思路很有技巧。
状态模式
迭代器模式
访问者模式
备忘录模式
命令模式
解释器模式
中介模式
JDK中的设计模式
Calendar建造者模式和工厂模式
既然已经有了 getInstance() 工厂方法来创建 Calendar 类对象,为什么还要用 Builder 来创建 Calendar 类对象呢
建造者模式用于定制化创建复杂的对象,而工厂模式用于创建不同但相关类型的对象。本身不冲突。我们点了特级牛排,依然可以随心搭配喜欢的酱。
从 Calendar 这个例子,我们也能学到,不要过于死板地套用各种模式的原理和实现,不要不敢做丝毫的改动。模式是死的,用的人是活的。在实际上的项目开发中,不仅各种模式可以混合在一起使用,而且具体的代码实现,也可以根据具体的功能需求做灵活的调整。
Collections装饰器模式和适配器模式
为什么说 UnmodifiableCollection 类是 Collection 类的装饰器类呢?
UnmodifiableCollection 类可以算是对 Collection 类的一种功能增强,但这点还不具备足够的说服力来断定 UnmodifiableCollection
UnmodifiableCollection 的构造函数接收一个 Collection 类对象,然后对其所有的函数进行了包裹(Wrap):重新实现(比如 add() 函数)或者简单封装(比如 stream() 函数)。而简单的接口实现或者继承,并不会如此来实现 UnmodifiableCollection 类。所以,从代码实现的角度来说,UnmodifiableCollection 类是典型的装饰器类。
java.util.Collections#enumeration方法体现了适配器模式。Enumeration是适配器接口,Iterator是其适配实现。Enumeration类和enumeration方法存在的意义在于兼容旧版本的jdk代码升级到新版本时的兼容。
public static Enumeration enumeration(final Collection c) {
return new Enumeration() {
private final Iterator i = c.iterator();
public boolean hasMoreElements() {
return i.hasNext();
}
public T nextElement() {
return i.next();
}
};
}
Collections.sort模板模式,策略模式
sort方法传入comparator可以认为是模板方法的实现,同时也可以认为是一种算法,策略
List students = new ArrayList<>();
students.add(new Student("Alice", 19, 89.0f));
students.add(new Student("Peter", 20, 78.0f));
students.add(new Student("Leo", 18, 99.0f));
Collections.sort(students, new AgeAscComparator());
最终调用模板方法compare的地方
private static int countRunAndMakeAscending(T[] a, int lo, int hi,
Comparator super T> c) {
assert lo < hi;
int runHi = lo + 1;
if (runHi == hi)
return 1;
// Find end of run, and reverse range if descending
if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
runHi++;
}
return runHi - lo;
}
观察者模式
jdk中的观察者模式实现。有可能新加入的观察者会miss通知,参看notifyObservers方法
Observer/Observable
调用notifyObservers前先调用setChanged,否则不会通知,why?
public interface Observer {
/**
* This method is called whenever the observed object is changed. An
* application calls an Observable object’s
* notifyObservers
method to have all the object’s
* observers notified of the change.
*
* @param o the observable object.
* @param arg an argument passed to the notifyObservers
* method.
*/
void update(Observable o, Object arg);
}
private boolean changed = false;
protected synchronized void setChanged() {
changed = true;
}
public void notifyObservers(Object arg) {
/*
* a temporary array buffer, used as a snapshot of the state of
* current Observers.
*/
Object[] arrLocal;
synchronized (this) {
/* We don't want the Observer doing callbacks into
* arbitrary code while holding its own Monitor.
* The code where we extract each Observable from
* the Vector and store the state of the Observer
* needs synchronization, but notifying observers
* does not (should not). The worst result of any
* potential race-condition here is that:
* 1) a newly-added Observer will miss a
* notification in progress
* 2) a recently unregistered Observer will be
* wrongly notified when it doesn't care
*/
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
单例模式
Runtime.getRuntime
Runtime
that allows the application to interface withgetRuntime
method.
模板模式
Java Servlet
Junit TestCase
Java InputStream
Java AbstractList
享元模式
Integer -128~127
String常量池
职责链模式
Servlet Filter
Spring interceptor
迭代器模式
Iterator
Guava 中的设计模式
建造者模式CacheBuilder
Wrapper模式(代理模式、装饰器、适配器模式)
缺省的Wrapper实现,如com.google.common.collect.ForwardingCollection等,这样用户类继承这些缺省类后,只需实现接口中的少量方法。
Immutable模式
一个对象的状态在对象创建之后就不再改变,这就是所谓的不变模式
对应的是不变类,不变对象
java中String是不变类
普通不变模式,内部引用对象可变
深度不变模式,内部引用对象也不可变
构建
所有成员变量通过构造函数一次性设置好,不暴露set等修改成员变量的方法
不变模式常用在多线程环境下,因为不存在并发安全问题,所以避免加锁
Guava不变集合类ImmutableCollection、ImmutableList、ImmutableSet、ImmutableMap
与Jdk的不变类区别是jdk中对原集合修改会体现在早先创建的不变集合中,而guava的则会拷贝出新的集合,拷贝后对原集合的改动不会体现在新的集合中
Spring框架中的设计模式
经典设计思想或原则
框架的作用:利用框架的好处有:解耦业务和非业务开发、让程序员聚焦在业务开发上;隐藏复杂实现细节、降低开发难度、减少代码 bug;实现代码复用、节省开发时间;规范化标准化项目开发、降低学习和维护成本等等。简言之:简化开发
约定优于配置
使用默认配置,对于偏离默认配置的项目显式配置
低侵入松耦合
IOC容器替换,利用AOP技术来做非业务功能
模块化,轻量级
模块划分做得好,低耦合,可单独引入其中的模块,非常轻量级。
再封装、再抽象
Spring Data , Spring JDBC,Spring Cache DataAccessException
支持可扩展的2种设计模式
观察者模式 使用IOC容器的publishEvent特性
在监听者程序中使用异步,以防阻塞事件发布者
public class DemoEvent extends ApplicationEvent {
private String message;
public DemoEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return this.message;
}
}
@Component
public class DemoPublisher {
@Autowired
private ApplicationContext applicationContext;
public void publishEvent(DemoEvent demoEvent) {
this.applicationContext.publishEvent(demoEvent);
}
}
@Component
public class DemoListener implements ApplicationListener {
@Override
public void onApplicationEvent(DemoEvent demoEvent) {
String message = demoEvent.getMessage();
System.out.println(message);
}
}
模板模式, 回调式的模板模式
将要执行的函数封装成对象(比如,初始化方法封装成 InitializingBean 对象),传递给模板(BeanFactory)来执行。
Spring bean生命周期图
Spring框架用到的11种设计模式
适配器模式 Spring web mvc中对于多种Controller实现,采用适配器模式,统一处理接口
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
public class SimpleServletHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Servlet);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
((Servlet) handler).service(request, response);
return null;
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}
策略模式
创建AOP代理的策略
public interface AopProxyFactory {
/**
* Create an {@link AopProxy} for the given AOP configuration.
* @param config the AOP configuration in the form of an
* AdvisedSupport object
* @return the corresponding AOP proxy
* @throws AopConfigException if the configuration is invalid
*/
AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;
}
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (!NativeDetector.inNativeImage() &&
(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
Class> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
}
组合模式
CacheManager的实现有中间CacheManager,如CompositeCacheManager,也有叶子CacheManager,如SimpleCacheManager
如下getCache,getCacheNames的实现使用了组合模式
public interface CacheManager {
/**
* Get the cache associated with the given name.
* Note that the cache may be lazily created at runtime if the
* native provider supports it.
* @param name the cache identifier (must not be {@code null})
* @return the associated cache, or {@code null} if such a cache
* does not exist or could be not created
*/
@Nullable
Cache getCache(String name);
/**
* Get a collection of the cache names known by this manager.
* @return the names of all caches known by the cache manager
*/
Collection getCacheNames();
}
public class CompositeCacheManager implements CacheManager, InitializingBean {
@Override
@Nullable
public Cache getCache(String name) {
for (CacheManager cacheManager : this.cacheManagers) {
Cache cache = cacheManager.getCache(name);
if (cache != null) {
return cache;
}
}
return null;
}
@Override
public Collection getCacheNames() {
Set names = new LinkedHashSet<>();
for (CacheManager manager : this.cacheManagers) {
names.addAll(manager.getCacheNames());
}
return Collections.unmodifiableSet(names);
}
}
装饰器模式
org.springframework.cache.transaction.TransactionAwareCacheDecorator实现了Cache接口
持有Cache接口引用targetCache,将所有Cache操作委托给targetCache。在TransactionAwareCacheDecorator中根据事务的执行情况对targetCache进行相关操作。
public class TransactionAwareCacheDecorator implements Cache {
@Override
public void put(final Object key, @Nullable final Object value) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
}
});
}
else {
this.targetCache.put(key, value);
}
}
}
工厂模式
静态工厂(类的静态方法),动态工厂(实例工厂方法)
解释器模式
SpEL Spring Expression Language ,spring-expresssion模块
单例模式
IOC容器作用域范围内的单例
观察者模式
模板模式
RedisTemplate,JdbcTemplate等Template。大部分通过Callback回调实现
职责链模式
Interceptor
代理模式
AOP
Mybatis框架中的设计模式
Mybatis为简化数据库开发而生
MyBatis 如何权衡代码的易用性、性能和灵活性
与jdbc的比较
JdbcTemplate比Mybatis更加轻量,性能更好,缺点是代码和sql耦合,不具备orm功能,需要自己编写代码解析对象和数据关系。易用性不及Mybatis和Hibernate
与hibernate比较
hibernate更加重量级,mybatis是半自动化的orm框架,而hibernate是全自动的。hibernate可以生成sql。生成的sql对于优化sql的场景支持较差
因此mybatis较hibernate性能较好。程序员自己写sql,灵活性高。
一般来说框架易用性和性能成对立关系
JdbcTemplate 提供的功能最简单,易用性最差,性能损耗最少,用它编程性能最好。Hibernate 提供的功能最完善,易用性最好,但相对来说性能损耗就最高了。MyBatis 介于两者中间,在易用性、性能、灵活性三个方面做到了权衡。它支撑程序员自己编写 SQL,能够延续程序员对 SQL 知识的积累。相对于完全黑盒子的 Hibernate,很多程序员反倒是更加喜欢 MyBatis 这种半透明的框架。这也提醒我们,过度封装,提供过于简化的开发方式,也会丧失开发的灵活性。
如何利用职责链与代理模式实现 MyBatis Plugin
MyBatis Plugin 跟 Servlet Filter、Spring Interceptor 的功能是类似的,都是在不需要修改原有流程代码的情况下,拦截某些方法调用,在拦截的方法调用的前后,执行一些额外的代码逻辑。它们的唯一区别在于拦截的位置是不同的。Servlet Filter 主要拦截 Servlet 请求,Spring Interceptor 主要拦截 Spring 管理的 Bean 方法(比如 Controller 类的方法等),而 MyBatis Plugin 主要拦截的是 MyBatis 在执行 SQL 的过程中涉及的一些方法。
默认情况下,MyBatis Plugin 允许拦截的方法有下面这样几个
MyBatis 底层是通过 Executor 类来执行 SQL 的。Executor 类会创建 StatementHandler、ParameterHandler、ResultSetHandler 三个对象,并且,首先使用 ParameterHandler 设置 SQL 中的占位符参数,然后使用 StatementHandler 执行 SQL 语句,最后使用 ResultSetHandler 封装执行结果。所以,我们只需要拦截 Executor、ParameterHandler、ResultSetHandler、StatementHandler 这几个类的方法,基本上就能满足我们对整个 SQL 执行流程的拦截了。
利用 MyBatis Plugin,我们还可以做很多事情,比如分库分表、自动分页、数据脱敏、加密解密等等
MyBatis 中的职责链模式的实现方式比较特殊。它对同一个目标对象嵌套多次代理(也就是 InteceptorChain 中的 pluginAll() 函数要执行的任务)。每个代理对象(Plugin 对象)代理一个拦截器(Interceptor 对象)功能
当执行 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四个类上的某个方法的时候,MyBatis 会嵌套执行每层代理对象(Plugin 对象)上的 invoke() 方法。而 invoke() 方法会先执行代理对象中的 interceptor 的 intecept() 函数,然后再执行被代理对象上的方法。就这样,一层一层地把代理对象上的 intercept() 函数执行完之后,MyBatis 才最终执行那 4 个原始类对象上的方法。
MyBatis 框架中用到的十几种设计模式。
建造者模式:org.apache.ibatis.session.SqlSessionFactoryBuilder,不是标准的严格意义的建造者,只是为了封装复杂的Configuration的构造,它里面定义了n多build方法,具有不同的参数,与使用多构造函数重载的方式没有本质区别
工厂模式:org.apache.ibatis.session.SqlSessionFactory 也非标准工厂模式,更像建造者模式
举的这2个例子他们的名称具有很大的迷惑性。
模板模式:org.apache.ibatis.executor.BaseExecutor
模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用有待子类实现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法一般是一一对应的,抽象方法在模板方法前面多一个“do”,比如,在 BaseExecutor 类中,其中一个模板方法叫 update(),那对应的抽象方法就叫 doUpdate()
public abstract class BaseExecutor implements Executor {
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity(“executing an update”).object(ms.getId());
if (closed) {
throw new ExecutorException(“Executor was closed.”);
}
clearLocalCache();
return doUpdate(ms, parameter);
}
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
}
解释器模式:SqlNode
整个解释器的调用入口在 DynamicSqlSource.getBoundSql 方法中,它调用了 rootSqlNode.apply(context) 方法
public interface SqlNode {
boolean apply(DynamicContext context);
}
线程唯一式单例模式:
org.apache.ibatis.executor.ErrorContext
private static final ThreadLocal LOCAL = ThreadLocal.withInitial(ErrorContext::new);
装饰器模式:Cache
org.apache.ibatis.cache.impl.PerpetualCache
之所以 MyBatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸
迭代器模式:org.apache.ibatis.reflection.property.PropertyTokenizer
适配器模式:org.apache.ibatis.logging.Log 重复造轮子
如何使用设计模式
在项目中应用设计模式,切不可生搬硬套,过于学院派,要学会结合实际情况做灵活调整,做到心中无剑胜有剑。
代码质量
review
示例:https://time.geekbang.org/column/article/190979
总的从可读性,可维护性(对于其他特性的总的概括),可扩展性,可复用性,可测试性,是否简洁,灵活等角度把控
展开来说
高内聚、低耦合》目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、松耦合
设计原则》是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD 等)
设计模式》设计模式是否应用得当?是否有过度设计?
可扩展性》代码是否容易扩展?如果要添加新功能,是否容易实现?
可复用性》代码是否可以复用?是否可以复用已有的项目代码或类库?是否有重复造轮子?
可测试性》代码是否容易测试?单元测试是否全面覆盖了各种正常和异常的情况?
可读性》代码是否易读?是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等)?
功能性需求满足与否
代码是否实现了预期的业务需求?
逻辑是否正确?是否处理了各种异常情况?
代码是否存在并发问题?是否线程安全?
非功能性需求满足与否
可运维》日志打印是否得当?是否方便 debug 排查问题?
易用性》接口是否易用?是否支持幂等、事务等?
性能》性能是否有优化空间,比如,SQL、算法是否可以优化?
安全性》是否有安全漏洞?比如输入输出校验是否全面?
是否简洁灵活
可读性
高层实现面向抽象而不是细节编程,写了一大段代码,不如封装成一个意义明确的函数来得可读性高。
可维护性
可扩展性
可复用性
减少代码耦合
满足单一职责原则
模块化
不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。
业务与非业务逻辑分离
通用代码下沉
从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。
继承、多态、抽象、封装
应用模板等设计模式
编程语言的某些特性
泛型
可测试性
单元测试
what
代码层面测试
以类或函数为单元
why
code review
重构
发现bug和设计问题
TDD落地的改进方案
how
利用测试框架
建立正确认知
编写单元测试尽管繁琐,但并不是太耗时;
我们可以稍微放低对单元测试代码质量的要求;
覆盖率作为衡量单元测试质量的唯一标准是不合理的;
单元测试不要依赖被测代码的具体实现逻辑;
单元测试框架无法测试,多半是因为代码的可测试性不好
单元测试为何难落地
写单元测试本身比较繁琐,技术挑战不大,很多程序员不愿意去写
国内研发比较偏向“快、糙、猛”,容易因为开发进度紧,导致单元测试的执行虎头蛇尾
团队没有建立对单元测试正确的认识,觉得可有可无,单靠督促很难执行得很好
什么是代码的可测试性
针对代码编写单元测试的难易程度
如何写出可测试的代码
依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试的时候,可以通过 mock 的方法解依赖外部服务,这也是我们在编写单元测试的过程中最有技术挑战的地方。
有哪些常见的不好测试的代码?反模式
代码中包含未决行为逻辑
滥用可变全局变量
滥用静态方法
使用复杂的继承关系
高度耦合的代码
重构
对于review(自己或团队)后发现问题的点进行小步快速重构,每轮可以按照一个维度重构,比如
第一轮重构:提高代码的可读性
第二轮重构:提高代码的可测试性
第三轮重构:编写完善的单元测试
第四轮重构:所有重构完成之后添加注释
解耦
解耦重要性
高内聚,低耦合
代码可读性和维护性
如何判定代码是否需要解耦
把模块与模块之间、类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构。
如何给代码解耦
封装与抽象,屏蔽底层实现细节依赖
引入中间层,简化依赖
模块化(分而治之)
其他设计思想和原则
单一职责
基于接口而非实现编程
依赖注入
多用组合少用继承
迪米特法则
…
编码规范
命名与注释
命名
长度
小作用域,命名可以短些
大作用域,命名可以长些
熟悉的词,可以使用简写
长度太短影响可读性,太长导致语句换行也影响可读性
利用上下文简化命名,如类里的变量名,函数的参数
命名可读,可搜索
单词可以读出来
命名符合项目统一规约
接口和抽象类的命名
项目统一约定即可
接口
IUserService-UserService
UserService-UserServiceImpl
抽象类
带Abstract
不带Abstract
注释
内容
做什么,为什么,怎么做
类上注释需要写做什么
quickstart how to use
总结性注释
哪些地方写注释
类和函数上要写,且要全面,详细
函数内注释少些
函数出错返回类型
返回错误码 C语言中用得多
返回NULL值 表示要查找的资源不存在
返回空对象,应对NPE问题,减少NULL值判断
抛出异常对象
检查异常
非检查异常
函数异常处理的三种方法
直接吞掉
直接往上抛出
包裹成新的异常抛出
函数里是否要对null值或空字符串判断
对于私有函数,只在类内部调用,自己确保不传入null即可
对于公有函数,为了提高代码健壮性,需要在public函数中作判断
代码风格
重要的是在团队内保持一致
类、函数的大小
函数不要超出一屏显示器的高度
类很难理解了,臃肿了。职责过多时类就太大了
一行代码多长
最长不能超过显示器宽度
善于空行分隔单元块
类内、函数内
四格缩进、两格缩进
建议2格
IDEA中不要使用tab缩进
大括号另起一行
类中成员的排列顺序
成员变量在函数前面
成员变量和函数之间都是按照先静态后普通
成员变量和函数之间,都会按照作用域由大到小排列:public、protected、private
编程技巧
把代码分割成更小的单元块
注意逻辑复杂时才值得提炼为函数,否则2、3行的函数也要跳进跳出。
避免函数参数过多
4个还能接受
函数拆成多个函数减少参数
函数参数封装成对象,对于API接口可提高兼容性
勿用函数参数来控制逻辑
结合实际情况看bool参数控制的逻辑是否要拆分成2个函数。
根据null判断来执行分支的也可以拆分为多个函数
函数设计要职责要单一
移除过深的嵌套层次
嵌套最好不要超过2层
去除多余的if/else
使用编程语言提供的 continue、break、return 关键字,提前退出嵌套。
调整执行顺序来减少嵌套
将部分嵌套逻辑封装成函数调用,以此来减少嵌套。
通过使用多态来替代 if-else、switch-case 条件判断的方法
学会使用解释性变量
常量取代魔法数字。
使用解释性变量来解释复杂表达式
编程范式
范式之间并不是互相取代的关系,或者说并不完全互相取代,现代语言如Java中含有主流范式的影子,如lamda表达式是函数式编程的体现,java语言本身面向对象,而函数的实现也体现了过程式的影子
面向对象
面向对象的编程单元是类或对象
它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石。
面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
vs 面向过程的优势
面向对象编程是以类为思考对象。在进行面向对象编程的时候,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是采用曲线救国的策略,先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。
除此之外,面向对象编程还提供了一种更加清晰的、更加模块化的代码组织方式。比如,我们开发一个电商交易系统,业务逻辑复杂,代码量很大,可能要定义数百个函数、数百个数据结构,那如何分门别类地组织这些函数和数据结构,才能不至于看起来比较凌乱呢?类就是一种非常好的组织这些函数和数据结构的方式,是一种将代码模块化的有效手段。
主要从面向对象支持的四大特性和面向过程做比较
当软件复杂度提升后,面向过程式的开发,思维模式已很难应对。而面向对象以类的组织,及类与类的交互来组织软件的复杂协作关系。
面向过程
面向过程的编程单元是函数
面向过程编程也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。
函数式编程
函数式编程的编程单元是无状态函数
程序可以用一系列数学函数或表达式的组合来表示。函数式编程是程序面向数学的更底层的抽象,将计算过程描述为表达式
函数内部涉及的变量都是局部变量,不会像面向对象编程那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样的
Java语言提供了三个语法机制支持函数式编程
Stream类
Lambda表达式
函数式接口
面向对象OOP
封装
特性
信息隐藏(数据访问保护)
提供有限接口访问类内部数据状态
实现
java类和对象
java访问修饰符 private ,protected, public
意义
避免属性被随意访问和修改,导致代码可维护性差,牵一发动全身。
提供少量接口,屏蔽实现细节,降低使用门槛和出错率。
抽象
特性
隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
实现
interface关键字
abstract关键字
即便没有实现接口或继承抽象类,类中的函数本身也是一种抽象,比如调用C函数,我们无需关心内部如何实现。
因此抽象有时不作为面向对象特性,因为只要有函数存在就是一种抽象,这不是面向对象特有的。
意义
抽象体现一种宏观把控,建立高层视角,避免过多细节占用理解带宽。
抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用。
定义方法名时要体现抽象思维,如getAliyunPictureUrl()与getPictureUrl(),明显后者没有暴露细节
继承
特性
表示类之间的is-a关系
模式
单继承,继承一个父类
多继承 ,继承多个父类
实现
java-> A extends B
C+±> A:B
意义
代码复用
继承和组合都能做到
继承关联两个类表达is-a的关系符合人类认知。
继承大概是面向对象有争议的一个特性,过度使用继承,导致代码结构复杂,可维护性差。
多态
特性
多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现
实现
继承+重写
接口类语法
duck-typing语法,用在诸如python等动态语言中
If it looks like a duck and quacks like a duck, it’s a duck
注重实现的行为,而不管实际类型。
意义
提高代码的扩展性
增加新的实现
复用性
只需实现带有接口参数的函数,而不需要实现2个具有不同类型参数的函数。
多态是设计原则、设计模式的代码实现基础。
设计思想
高内聚低耦合
用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等
“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。
设计原则
不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。
单一职责SRP(Single Responsibility Principle)
A class or module should have a single responsibility
这个原则描述的对象包含两个,一个是类(class),一个是模块(module)
把模块看作比类更加抽象的概念,类也可以看作模块
把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。
不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类
不同的业务层面去看待同一个类的设计,对类是否职责单一,也会有不同的认识
实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
如何判断类的职责是否足够单一?(侧面回答)
类中的代码行数、函数或者属性过多;
类依赖的其他类过多,或者依赖类的其他类过多;
私有方法过多;
比较难给类起一个合适的名字;
类中大量的方法都是集中操作类中的某几个属性。
类的职责是否设计得越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
开闭原则OCP(Open Closed Principle)
software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification
如何理解“对扩展开放、对修改关闭”
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
如何做到“对扩展开放、修改关闭”?
具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
里氏替换原则LSP(Liskov Substitution Principle)
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
多态和里氏替换原则:从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的
多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路
而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
Design By Contract按照协议(父类的行为约定)来设计
子类遵循父类的行为约定,可以更改函数内部实现逻辑,但不能改变函数原有的行为约定
哪些是行为约定
函数声明要实现的功能;
对输入、输出、异常的约定;
甚至包括注释中所罗列的任何特殊说明
违反里氏替换原则的例子
子类违背父类声明要实现的功能
父类sortOrdersByAmount按订单金额大小排序,子类sortOrdersByAmount按订单创建日期排序
子类违背父类对输入、输出、异常的约定
在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
子类违背父类注释中所罗列的任何特殊说明
父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。
验证有没有违背里式替换原则的方法
用父类的单元测试来运行子类实现
接口隔离原则ISP(Interface Segregation Principle)
依赖反转原则DIP(Dependency Inversion Principle)
控制反转
原先由程序员自己控制程序流程执行,现由框架控制
实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。
比如使用Spring Web MVC框架,那么处理Web请求你只需在Controller类上使用对应的@Controller注解即可。框架会派发请求,程序员无需从头编写处理连接,解析请求,响应请求等流程,只需实现对应的业务即可。
依赖注入
依赖注入是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。
依赖注入框架
我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
依赖反转原则
依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
还是拿Servlet举例,tomcat框架可以认为是高层模块(调用方),业务代码可以认为是底层模块(被调用方,下游)。高层和低层没有直接的依赖,他们都依赖同一个抽象,Servlet规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
KISS原则
Keep It Simple and Stupid.侧重如何做
KISS 原则是保持代码可读和可维护的重要手段。KISS 原则中的“简单”并不是以代码行数来考量的。代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。除此之外,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。
不要使用同事可能不懂的技术来实现代码;
不要重复造轮子,要善于使用已经有的工具类库;
不要过度优化。
YAGNI
You Ain’t Gonna Need It你不会需要它侧重要不要做
实现逻辑重复
实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则。
功能语义重复
实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。
代码执行重复。
除此之外,代码执行重复也算是违反 DRY 原则
DRY
Don’t Repeat Yourself不要写重复的代码
Rule Of Three
第一次编写代码的时候,我们不考虑复用性;第二次遇到复用场景的时候,再进行重构使其复用。需要注意的是,“Rule of Three”中的“Three”并不是真的就指确切的“三”,这里就是指“二”。
编码原则
基于接口而非实现编程
做到
函数的命名不能暴露任何实现细节。
封装具体的实现细节。
为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程
原则初衷
将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性。
原则使用
如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了
越是不稳定的系统,我们越是要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。
组合优于继承
继承作用:表示 is-a 关系,支持多态特性,代码复用
继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性
组合缺点:继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本
组合并不完美,继承也不是一无是处
继承使用场景
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。
模板模式
一些第三方类,我们无法改动其API而又想实现多态时。
组合使用场景
遇到继承的缺点
子类和复用的父类不具备is-a的关系
装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)设计模式
组合替代继承
使用组合和接口的has-a关系来
用接口实现多态
代码复用通过组合和委托实现
原则使用
划分出单一职责的接口
实现接口
如果实现类实现接口的代码相同,则可能引发重复问题
组合使用实现接口的类,使用委托技术
定义实现接口的类A,原先实现接口的类转换为组合使用A,而不再实现接口
产品是如何诞生的
需求分析
借鉴其他竞品
搜索
体验
线框图
用户故事
系统设计
合理的模块划分
运用高内聚、低耦合的思想
设计模块与模块之间的交互关系
交互方式
同步接口调用
简单直接
上下游系统
消息中间件异步调用
解耦效果好
同层系统间调用
设计模块的接口、数据库、业务模型
通用框架设计
需求分析
需求分析可以用清晰的列表展示
功能性需求
非功能性需求
易用性
要有产品意识。框架是否易集成、易插拔、跟业务代码是否松耦合、提供的接口是否够灵活等等,都是我们应该花心思去思考和设计的。有的时候,文档写得好坏甚至都有可能决定一个框架是否受欢迎。
性能
对于需要集成到业务系统的框架来说,我们不希望框架本身的代码执行效率,对业务系统有太多性能上的影响。对于性能计数器这个框架来说,一方面,我们希望它是低延迟的,也就是说,统计代码不影响或很少影响接口本身的响应时间;另一方面,我们希望框架本身对内存的消耗不能太大。扩展性
扩展性
框架留给业务代码的扩展点
容错性
容错性这一点也非常重要。对于性能计数器框架来说,不能因为框架本身的异常导致接口请求出错。所以,我们要对框架可能存在的各种异常情况都考虑全面,对外暴露的接口抛出的所有运行时、非运行时异常都进行捕获处理。
通用性
框架在设计的时候,要尽可能通用。我们要多去思考一下,除了接口统计这样一个需求,还可以适用到其他哪些场景中
框架设计
TDD或最小原型
聚焦简单应用场景
实现简单原型,作为迭代设计的基础
最终设计
模块划分
软件开发
应对复杂软件开发
从设计原则和思想的角度来看,如何应对庞大而复杂的项目开发?
uninx是开源开发的,那么它是如何应对复杂软件开发的呢?
Everything is a file in Uninx/linux
封装
抽象
基于接口而非实现编程
分层与模块化
进程调度、进程通信、内存管理、虚拟文件系统、网络接口
Unix 系统也是基于分层开发的,它可以大致上分为三层,分别是内核、系统调用、应用层。
面对复杂系统的开发,我们要善于应用分层技术,把容易复用、跟具体业务关系不大的代码,尽量下沉到下层,把容易变动、跟具体业务强相关的代码,尽量上移到上层。
基于接口通信
如open() 文件操作函数
高内聚低耦合(和抽象、封装、基于接口通信相辅相成)
为扩展而设计
开闭原则
识别出代码可变部分和不可变部分,将可变部分封装起来,隔离变化,提供抽象化的不可变接口,供上层系统使用
KISS原则(和为扩展而设计冲突)
可读性和扩展性冲突时,首选可读性
最小惊奇原则
遵守开发/设计规范
在大型项目开发中尤其重要
从研发管理和开发技巧的角度来看,如何应对庞大而复杂的项目开发?
导致代码质量不高的原因有很多,比如:代码无注释,无文档,命名差,层次结构不清晰,调用关系混乱,到处 hardcode,充斥着各种临时解决方案等等
面对大型复杂项目的开发,如何长期保证代码质量,让代码长期可维护?
吹毛求疵般地执行编码规范
代码review并修正
细节决定成败,代码规范的严格执行极为关键。
主人翁心态
编写高质量的单元测试
集成测试、黑盒测试都很难测试全面,因为组合爆炸,穷举所有测试用例的成本很高,几乎是不可能的。单元测试就是很好的补充。它可以在类、函数这些细粒度的代码层面,保证代码运行无误。底层细粒度的代码 bug 少了,组合起来构建而成的整个系统的 bug 也就相应的减少了。
高质量的单元测试不仅仅要求测试覆盖率要高,还要求测试的全面性,除了测试正常逻辑的执行之外,还要重点、全面地测试异常下的执行情况。毕竟代码出问题的地方大部分都发生在异常、边界条件下。
不流于形式的 Code Review
关键还是要执行到位,不能流于形式。
开发未动、文档先行
对大部分工程师来说,编写技术文档是件挺让人“反感”的事情。一般来讲,在开发某个系统或者重要模块或者功能之前,我们应该先写技术文档,然后,发送给同组或者相关同事审查,在审查没有问题的情况下再开发。这样能够保证事先达成共识,开发出来的东西不至于走样。而且,当开发完成之后,进行 Code Review 的时候,代码审查者通过阅读开发文档,也可以快速理解代码
除此之外,对于团队和公司来讲,文档是重要的财富。对新人熟悉代码或任务的交接等,技术文档很有帮助。而且,作为一个规范化的技术团队,技术文档是一种摒弃作坊式开发和个人英雄主义的有效方法,是保证团队有效协作的途径。
持续重构、重构、重构
不要等到问题堆得太多了再去解决,要时刻有人对代码整体质量负责任,平时没事就改改代码。千万不要觉得重构代码就是浪费时间,不务正业!
特别是一些业务开发团队,有时候为了快速完成一个业务需求,只追求速度,到处 hard code,在完全不考虑非功能性需求、代码质量的情况下,堆砌烂代码。实际上,这种情况还是比较常见的。不过没关系,等你有时间了,一定要记着重构,不然烂代码越堆越多,总有一天代码会变得无法维护。
对项目与团队进行拆分
面对大型复杂项目,我们不仅仅需要对代码进行拆分,还需要对研发团队进行拆分
每个小团队对应负责一个小的项目(模块、微服务等),这样每个团队负责的项目包含的代码都不至于很多,也不至于出现代码质量太差无法维护的情况。
聚焦在 Code Review 上来看,如何通过 Code Reviwe 保持项目的代码质量?
为什么要进行 Code Review(代码审查),Code Review 的价值在哪里?树立正确的code review认知
Code Review 践行“三人行必有我师
永远不要觉得自己很厉害,写的代码就不需要别人 Review 了;永远不要觉得自己水平很一般,就没有资格给别人 Review 了;更不要觉得技术大牛让你 Review 代码只是缺少你的一个“approve”
中国有句老话,“三人行必有我师”,我觉得用在这里非常合适。即便自己觉得写得已经很好的代码,只要经过不停地推敲,都有持续改进的空间。
Code Review 能摒弃“个人英雄主义”
如果一个人默默地写代码提交,不经过团队的 Review,这样的代码蕴含的是一个人的智慧。代码的质量完全依赖于这个人的技术水平。这就会导致代码质量参差不齐。如果经过团队多人 Review、打磨,代码蕴含的是整个团队的智慧,可以保证代码按照团队中的最高水准输出。
Code Review 能有效提高代码可读性
Code Review 是技术传帮带的有效途径
Code Review 保证代码不止一个人熟悉
Code Review 能打造良好的技术氛围
好的技术氛围也能降低团队的离职率。
Code Review 是一种技术沟通方式
Code Review 能提高团队的自律性
在开发过程中,难免会有人不自律,存在侥幸心理:反正我写的代码也没人看,随便写写就提交了。Code Review 相当于一次代码直播,曝光 dirty code,有一定的威慑力
如何在团队中落地执行Code Review?
主要是排除认知障碍,树立正确认知
如何开发一个通用的功能模块
既然有了 JDK,为什么 Google 还要开发一套新的类库 Google Guava?是否是重复早轮子?两者的差异化在哪里?
如何在业务开发中,发现通用的功能模块,以及如何将它开发成类库、框架或者功能组件。
首先发现这些可能的通用模块
我们要有善于发现、善于抽象的能力,并且具有扎实的设计、开发能力,能够发现这些非业务的、可复用的功能点,并且从业务逻辑中将其解耦抽象出来,设计并开发成独立的功能模块
在业务开发中,跟业务无关的通用功能模块,常见的一般有三类(他们的特点是复用,和业务无关):
类库(library)
API
框架(framework)
DI
功能组件(component)
类似类库,但可能集成三方组件,更加重量级
如何将它设计开发成一个优秀的类库、框架或功能组件呢
产品意识
对于这些类库、框架、功能组件的开发,我们不能闭门造车,要把它们当作“产品”来开发。这个产品是一个“技术产品”,我们的目标用户是“程序员”,解决的是他们的“开发痛点”
是否易用、易集成、易插拔、文档是否全面、是否容易上手等,这些产品素质也非常重要
服务意识
从心态上,别的团队使用我们开发出来的技术产品,我们要学会感谢。这点很重要。心态不同了,做起事来就会有微妙的不同。
们还要有抽出大量时间答疑、充当客服角色的心理准备。有了这个心理准备,别的团队的人在问你问题的时候,你也就不会很烦了。
如果没有单独的机会
我建议初期先把这些通用的功能作为项目的一部分来开发。不过,在开发的时候,我们做好模块化工作,将它们尽量跟其他模块划清界限,通过接口、扩展点等松耦合的方式跟其他模式交互。等到时机成熟了,我们再将它从项目中剥离出来。因为之前模块化做的好,耦合程度低,剥离出来的成本也就不会很高。
现状
对于“如何做需求分析,如何做职责划分?需要定义哪些类?每个类应该具有哪些属性、方法?类与类之间该如何交互?如何组装类成一个可执行的程序?”等等诸多问题,都没有清晰的思路,更别提利用成熟的设计原则、思想或者设计模式,开发出具有高内聚低耦合、易扩展、易读等优秀特性的代码了。
可能原因
需求不明确
对策
面向对象分析主要的分析对象是“需求”,因此,面向对象分析可以粗略地看成“需求分析”。实际上,不管是需求分析还是面向对象分析,我们首先要做的都是将笼统的需求细化到足够清晰、可执行。我们需要通过沟通、挖掘、分析、假设、梳理,搞清楚具体的需求有哪些,哪些是现在要做的,哪些是未来可能要做的,哪些是不用考虑做的
缺少锻炼
相比单纯的业务 CRUD 开发,鉴权这个开发任务,要更有难度。鉴权作为一个跟具体业务无关的功能,我们完全可以把它开发成一个独立的框架,集成到很多业务系统中。而作为被很多系统复用的通用框架,比起普通的业务代码,我们对框架的代码质量要求要更高。开发这样通用的框架,对工程师的需求分析能力、设计能力、编码能力,甚至逻辑思维能力的要求,都是比较高的。如果你平时做的都是简单的 CRUD 业务开发,那这方面的锻炼肯定不会很多,所以,一旦遇到这种开发需求,很容易因为缺少锻炼,脑子放空,不知道从何入手,完全没有思路。
对策
尽管针对框架、组件、类库等非业务系统的开发,我们一定要有组件化意识、框架意识、抽象意识,开发出来的东西要足够通用,不能局限于单一的某个业务需求,但这并不代表我们就可以脱离具体的应用场景,闷头拍脑袋做需求分析。多跟业务团队聊聊天,甚至自己去参与几个业务系统的开发,只有这样,我们才能真正知道业务系统的痛点,才能分析出最有价值的需求。不过,针对鉴权这一功能的开发,最大的需求方还是我们自己,所以,我们也可以先从满足我们自己系统的需求开始,然后再迭代优化。
由简入难,不断优化
划分职责进而识别出有哪些类
定义类及其属性和方法
定义类与类之间的交互关系
将类组装起来并提供执行入口