Subscribe to XML Feed

22 Apr 2009

重构技巧──inline类成员变量

这是今天在和 李教授 pair的时候学到的:

问题

有一个这样的类定义(简化版):


public class OnCurrentActivityPage {
  private String pipelineName;
  
  public OnCurrentActivityPage(ScenarioState scenarioState) {
    this.pipelineName = scenarioState.pipelineName();
  }

  public void triggerPipeline() {
    selenium.trigger(pipelineName);
  }

  public void rerunStage() {
    selenium.rerun(pipelineName, stageName);
  }

  public void pausePipeline() {
    selenium.pause(pipelineName);
  }

  // 很多其它方法也要用到pipelineName
}

我们发现在OnCurrentActivityPage构造函数里调用scenarioState.pipelineName()有bug,需要把它改成不缓存pipelineName而在每次需要它的时候直接调用scenarioState.pipelineName()。

解决方案一(人工替换)

最直接的办法是删掉pipelineName,把scenarioState保存成类成员变量,然后把所有使用pipelineName的代码替换成使用scenarioState.pipelineName()。由于这个类里面有不少方法(超过10个)需要修改,这个方案颇花时间。

解决方案二(自动重构)

Intellij支持对类成员变量做inline重构。但是前提是:

  • 成员变量必须在声明的同时做初始化
  • 成员变量必须被标记为final

这两点要求都很容易满足。为了满足第一点,我们可以把pipelineName的初始化移到声明处:


private String pipelineName = scenarioState.pipelineName();

这时候IDE会报错说scenarioState没有定义,不要理它,因为inline的时候Intellij会把该代码原样移走。

为了满足第二点,我们只需要把pipelineName标记为final:


private final String pipelineName = scenarioState.pipelineName();

然后,再在任何一个使用pipelineName的地方用Ctrl + N执行inline重构,并且指定重构范围为所有的pipelineName调用。转瞬之间,费时费力的重构被工具自动化完成了!

View Comments
06 Mar 2009

用visitor来封装实现细节

前几天我写过一篇 用封装来解决代码重复 ,其中谈到了设计缺少封装性所带来的一个问题────重复。其实,缺乏封装性所带来的问题还有很多,比如说对变化的适应性。

下面我来举个实际的例子。用过Cruise的朋友都知道,Cruise里面有一个核心的概念叫pipeline。用户定义好他们想要的pipeline以后,Cruise就可以把这些pipeline从配置文件里读出来使用了。使用方式类似于这样:


public class CruiseConfig {
    List<Pipeline> pipelines = new ArrayList<Pipeline>();
    
    public List<Pipeline> getPipelines() {
        return pipelines;
    }
}

public class TriggerService {
    public void trigger() {
        for (Pipeline pipeline : cruiseConfig.pipelines) {
            // ...
        }
    }
}

这里面有几个地方比较关键:

  • CruiseConfig里面包含了pipeline的定义信息,该信息目前是以ArrayList的形式保存在pipelines字段里
  • CruiseConfig通过一个getter把pipelines的定义信息直接暴露给外部使用者,也就是TriggerService
  • TriggerService直接访问cruiseConfig.pipelines以获得想要的pipeline定义信息
  • TriggerService只是众多外部使用者之一……

这个设计最初并没有什么问题,直到最近我们决定在pipeline之上再引入一个新的概念────pipeline group。有了新的需求,再回过头来看这段代码里面所体现的模型,我们就发现原有设计的严重不足了。其中最主要的不足有:

  • 外部使用者不应该直接访问到pipeline,而是应该先访问group,再到pipeline
  • 即使为了向后兼容,我们暂时允许外部使用者直接访问pipeline的话(假定所有的pipeline都是在一个组里),由于CruiseConfig内部用于表示pipeline group的数据结构变化了,再把ArrayList原封不动地交给外面就是错误的
  • 如果要改,getPipelines方法已经有了不少的地方在调用,一个个地改工作量着实不小

经过对这些问题的分析,我们发现问题的根源在于设计的封装性不足。也就是说,CruiseConfig向外暴露了不应该暴露的实现细节,而暴露的细节越多,外部对于这些细节的依赖程度就越高,一旦改动起来,其所带来的影响也就越大了。那么应该怎么解决它呢?

经过一番重构,我们把代码改成了下面的样子:


public class CruiseConfig {
    List<Group> groups = new ArrayList<Group>();
    
    public void accept(PipelineVisitor visitor) {
        for (Pipeline pipeline : defaultGroup()) {
            visitor.visit(pipeline);
        }
    }
}

public interface PipelineVisitor {
    public void visit(Pipeline pipeline);
}

public class TriggerService {
    public void trigger() {
        cruiseConfig.accept(new PipelineVisitor() {
            public void visit(Pipeline pipeline) {
                // ...
            }
        } );
    }
}

其中最主要的变化有几处:

  • CruiseConfig不再直接向外部暴露pipelines的实现细节,因为这种细节对于外部来说是既不需要知道也不应该知道的
  • CruiseConfig按照某一种简单的约定来向外部提供pipeline信息,这种约定(在例子里是以PipelineVisitor接口的方式提供的)只把外部最关心的信息本身(pipeline对象)暴露出去,至于其它的细节(比如说pipeline是以什么方式存储的)则不会暴露了
  • TriggerService为了获得pipeline信息,现在只需要按照PipelineVisitor的定义去创建一个对象,然后把该对象交给CruiseConfig就可以了。这个对象只关心怎么做好自己的操作就可以了

经过这样一番重构,CruiseConfig很好地把pipeline的实现细节封装起来,通过用一个简单而且明确的接口来向外界提供pipeline信息,它把外部对于pipeline定义的依赖程度降到了最低。以后如果再有关于pipeline存储方式或者设计方式的变化,只要外边的接口不变,那么使用者就不需要改变了。

View Comments
18 Feb 2009

用封装来解决代码重复

最近一段时间,项目上改进了Cruise中artifact repository部分的设计,在原有的代码基础之上做了一次重构,同时加入了新的功能。

在重构过程中,我们发现原有的代码存在不少的问题。比如说下面这个例子,为了得到存放一个特定job的artifact repository路径,原有的代码类似于:

File artifactPath = new File(cruiseConfig.getArtifactRoot(), String.valueOf(jobId));

这样写代码至少存在两个问题:

  • 重复:每次调用的时候都要做类似的文件名拼接
  • 暴露实现细节:artifact repository的内部存放结构属于Cruise中的领域设计细节,这一细节是有可能发生变化的(事实上在目前开发的Cruise 2.0中这一细节的确发生变化了)。我们应该通过面向对象的封装特性来隐藏这一细节,而不应该把它直接暴露在外面

新的设计把artifact的内部实现细节封装给了一个ArtifactService类。其中findArtifact方法解决了上面的问题:

public File findArtifact(JobIdentifier jobId) {
  return new File(.....);
}

当系统中只有这一个方法了解artifact repository的内部结构时,再改动起来就非常容易了。

View Comments
30 Dec 2008

用脚本自动转换和发布blog

昨天我在一篇 blog 里提到了自己想做的一自动化转换和发布blog的想法。说干就干,在一番google之后我写了下面这段脚本来自动转换blog:


require 'rubygems'
require 'redcloth'

if ARGV.length < 1 then
  puts "please tell me which textile file to convert: bc.rb [FILE]"
else
  filename = ARGV[0].split('/').last.split('.').first
  blog = open(ARGV[0]) { |f| f.read}
  textile = RedCloth.new blog
  open('html/' + filename + '.html', 'w') {|f| f << textile.to_html }
end

这段代码会把输入的textile文件自动转移成html文件。为了区分文件的类型,我重构了本地的blog目录,把原有的blog文本文件从blog根目录挪到了textile目录下,同时在textile同一级创建了html目录,用于保存转换以后的结果。

格式转移搞定了,接下来就是自动发布。google了一番以后,我又写了下面这个脚本:


require 'xmlrpc/client.rb'

if ARGV.length < 1 then
  puts "please tell me which html file to post: bc.rb [FILE]"
else
  blog = ARGV[0]
  blog_body = open(blog) { |f| f.read }
  blog_content = {:title => 'test', :description => blog_body }

  proxy = XMLRPC::Client.new("dyang.wordpress.com.cn", "/xmlrpc.php", 80)
  begin
    puts "posting new blog:"
    resp = proxy.call("metaWeblog.newPost", "1", "myusername", "mypassword", blog_content, false)
    puts resp
  rescue XMLRPC::FaultException => e
    puts "ERROR: Code: #{e.faultCode}"
    puts "ERROR: Msg: #{e.faultString}"
  end
end


这段脚本非常好理解,它调用了Wordpress支持的metaWeblog API,通过传入适当的参数来实现blog的发布。但是目前我只能用它来发布英文内容,一旦标题或者正文里有中文字符就会返回一个id为-32700的格式错误。google了一下, 这里 提供了一种解决方案,但它要求修改服务器端的一个文件,而我的blog提供商是不允许这么做的。这让我很失望。看来,我需要再找一个地方托管我的blog了。

顺便提一下的是,同事 Tin 提醒我Wordpress也支持邮件发布。然而让我再次失望的是,我现在的blog提供商并没有开放这个功能,而且也没有办法自己上传或者加载plugin去支持。发邮件去问也没有回应,看来我也许真的要考虑迁出了。

View Comments

Older Posts

重构blog 29 Dec 2008 Comments