重用代码的目标是什么?(我们为什么要重用代码)
答:避免重复发明轮子,有大量的已经写好的功能可以调用.这也是OO(面向对象的目标)
什么样的代码是可以复用的代码?
1.接口良好的.(利于用户阅读和使用)
1).类的接口应该展现一致的抽象层次.
public class EmpolyeeContainer extends LinkedList{
public void addEmpolyee(Empolyee e);//1
public void removeEmpolyee(Empolyee e);//2
public Empolyee nextItem();//3
public Empolyee firstItem();//4
public Empolyee lastItem();//5
}
这个接口暴露了两层抽象1,2是Container操作,3,4,5是List操作.实际上我们只需要1,2.这里不应该用继承而应该使用组合.这样做会暴露内部的实现机制,造成客户调用的混乱,
public class EmpolyeeContains{
public void addEmpolyee(Empolyee e);//1
public void removeEmpolyee(Empolyee e);//2
public Empolyee nextEmpolyee();
private LinkedList innerContainer ;//3
}
1,2的操作通过3的数据结构实现,但是接口本身不暴露内部的实现机制.暴露的内部细节越少越有利于修改.
2).提供成对(组)的服务.
3).尽可能让接口可编程,而不是表达语义.
暴露的接口应该仅仅通过编译器就能限制其使用方法,例如接口限定方法的参数类型,个数等等编译器可检查的错误.如果接口中有这样的方法step1(),step2(),隐含的表达step1必须在step2之前调用否则会出错,这种语义性的行为是编译器无法检查的.也就是说调用者有可能会调用错误.应该尽量避免这种接口设计,至少要通过注释文档说明这种语义.
commons-lang包中的StringUtils,这是个常用的字符串处理工具类,这是个设计很好的接口基本上每个方法的名字就说明了自己的行为,方法之间也没有语义的约束.而且方法往往分组,一些列的substringBetween,substringBefore,substringAfter.
2.代码是可读的.(利于用户和自己理解代码)
可以工作的代码,已经暴露的良好的接口,用户可以很方便的使用,而且他没必要了解内部实现,所以内部代码未必需要可读只要能工作就行.这种想法是错误(愚蠢)的.因为没有任何代码是一成不变的,需求总在变化,总有一天需要修改代码以适用新的需求,否则代码的生命周期就结束了也就不再可复用.
改善代码可读性的原则.
1).前面提到过的.统一的抽象层次有利于代码的阅读.
2).良好设计编码的子程序(方法)
只做一件事(单一职权原则)
这意味着方法不应该有副作用,考虑如下代码.
public boolean validateUser(User user){
session.init();
if(user.getPassword().equals(password)){
return true ;
}else{
return false ;
}
}
问题就出在session.init().方法名并没有暗示,方法会初始化session.方法名可以改为initSessionAndValidateUser(User user)但这依然违反了单一职权原则.但是却可以让用户清楚知道代码有这样的副作用.
方法不易过长.语句之间应保持统一的抽象(这个比较难做到,也难以理解).
30秒原则(任何方法如果在30秒内不能阅读理解,就应当做进一步拆分).
private HttpUriRequest buildHttpPost() {
String uri = uriPattern.replaceAll("\\?.*", "");//1
logger.info("POST请求:\n{}", uri);//2
HttpPost req = new HttpPost(uri);//3
HttpEntity entity = buildHttpEntity();//4
req.setEntity(entity);//5
return req;
}
3,4,5处于统一抽象层次,每一句都是描述如何构建一个HTTP_POST请求.但是1,2就不是,他们描述的是如何得到正确的请求URI.这两句可以进一步拆分成createPostUri().但是目前为止方法本身符合30秒原则,所以可读性是可以接受的,但如果未来由于需求变化,要做进一步的修改时,这个方法就必须做进一步拆分.
参数的数量.
大多数情况下,无参数或单参数的情况最好.例外的情况比如Point(x,y)这样的方法,Point具有其自然属性,所以两个参数比较合适.对于单参数方法最好方法名与参数名形成动词/名词形式.例如write(name)这个方法如果写成writeField(name)那么调用者就很容易知道name是一个field.
标示参数,有些方法使用标示参数比如有这样一个方法newWriter(File file , boolean append)当append为true时新建的Writer会将内容附加到file中,否则清空原来的文件写入内容.这时起码就要阅读以下文档调用者才知道如何使用标示参数.这是可以采用拆分为两个方法来去掉这个标示参数.newWriter(File file),newAppendableWriter(File file).这样就一目了然了.
输出参数是不建议使用的,应该尽量使用返回值做为输出结果.
变量和方法名称应当有意义,做到代码本身就是自解释的.
推荐书籍<Clean Code>代码整洁之道.代码大全,敏捷软件开发
3.遵循编码实践.
有很多编码的实践可以借鉴,以提高代码质量.例如经典的设计模式.(由于篇幅有限再次就不介绍了).在此只介绍一些我在日常工作中的体会.
1).面向接口编程,避免使用继承而使用对象组合.依赖倒置(依赖注入)
面向接口编程无疑可以减少对具体类的依赖,降低类之间的耦合.良好设计的接口是细小而稳定的,那么依赖接口可以减少对类的改变(符合开放封闭原则).
继承是OO中很方便的复用代码的方式,虽然Java只支持单继承,降低了继承的复杂度,但当继承层次增加,代码的可读性就会下降,内部细节也越复杂子类对父类的依赖和耦合也比较严重.特别是子类使用父类的保护成员和字段的时候,这样的类在阅读代码的时候是很晦涩的.
对象组合技术同样可以实现继承带来的功能(虽然有时要多些一些代码,但是可以让程序结构更简单灵活)
观察如下代码.
abstract class AbstractExampleDocument
{
// skip some code ...
public void output(Example structure)
{
if( null != structure )
{
this.format( structure );
}
}
protected void format(Example structure);
}
当要实现自定义格式的时候只需要继承该类并覆盖format保护方法即可.此时由于需求变化要在该类中添加一个将格式化好的structure保存到数据库的功能.但父类不知道子类如何格式化structure所以无法确定保存到数据库的实现.可以考虑在父类中增加一个save()抽象方法,待子类去实现.天啊,所有的子类都要实现一次save()方法.而且所有的格式化操作和保存到数据库的操作是紧密耦合的.将structure保存到数据库的操作可能在很多地方都有用,但是却无法从子类中单独提取出来使用.
class DefaultExampleDocument
{
private Formatter formatter ;
public void setFormatter(Formatter formatter){
this.formatter = formatter;
}
// skip some code ...
public void output(Example structure)
{
ExampleFormatter formatter =
(ExampleFormatter) manager.lookup(Roles.FORMATTER);
if( null != structure )
{
formatter.format(structure);
}
}
}
使用对象组合技术,只需要再编写一个Saver放到DefaultExampleDocument 中,在output结束之前调用Saver的save方法即可应对上述变化需求.此时依赖注入出场了,什么样formatter对应什么样的saver完全交由依赖注入决定,而且硬编码在一起,解决了耦合问题.而且对DefaultExampleDocument 的修改完全不需要影响所有的formatter.
可见优先使用对象组合,和面向接口编程可以很好的利用依赖注入实现代码的解耦提高代码的可维护性和可扩展性.
继承也不是一定不要用的,当父类和子类有is-a关系时候,适合使用继承.使用继承时要注意符合替换原则(在任何时候子类实例都可以替换父类实例使用)
单一职权和开放封闭原则.
该原则要求代码对扩展是开放的,对修改是封闭的.最佳状态是一个类或者方法,只有一个引起变化的理由.当然要做到这一点首先就是要符合单一职权原则(只做一件事).符合单一职权原则才可以保证只要在要做的那件事的需求有变化的时候才会修改代码.否则只需要扩展代码.实现开放封闭的方式有很多,例如前面的对象组合技术,还有很多有关的设计模式Bridge,Composite,Decorator,Observer,Strategy等等.
case/if语句
这种语法是大多编程语言中的基本流程控制语句.常常造成过多的缩进,和过多的流程分支.而且这种语句难以拆分成小方法.严重影响代码的可读性.当分支过多时,是不是可以考虑使用查表法?
例如,在分析xml文件时对不同的标签初始化不同的对象实例
TagInstance instance ;
if(tag==A){
instance = new A();
}else if(tag==B){
instance = new B();
}else if(tag ==C){
instance = new C() ;
}else{
instance = new D();
}
可以替换为
Map map = new HashMap();
map.put(“A”,new A());
…
map.put(“D”,new D());
TagInstance instance = map.get(tag);
这样明显逻辑清楚而且很容易修改,代码量也下降了.也很容易进行方法拆分.
做到上述的代码就是可复用的好代码吗?未必,作为一个开发人员编写好代码是贯彻始终的,所有的实践均来源于自己和他人的经验总结,到底如何才能写出好的可复用的代码每个人的标准也不完全一致.在此只是提及了一些我认为是大多数人公认的原则和实践.
参考资料,代码整洁之道,代码大全,敏捷软件开发.