演化架构和紧急设计: 利用可重用代码,第 2 部分

捕捉惯用模式

在使用 演化架构和紧急设计 前几期描述的技术发现 代码中的紧急设计之后,下一步您需要一种获取和利用这些设计元素的方法。本文介绍了两种用于获取惯用模式的方法:将模式作为 APIs 进行捕捉;使用元程序设计方法。

Neal Ford, 应用程序架构师, ThoughtWorks Inc.

Neal FordNeal Ford 是 ThoughtWorks 的应用程序架构师,这是一家全球性的 IT 咨询公司,专门研究端到端的软件开发和交付。他是 Developing with Delphi: Object-Oriented TechniquesJBuilder 3 UnleashedArt of Java Web Development 的作者。他的主要咨询重点是构建大规模企业应用程序。您可以通过 Neal 的 Web 站点 www.nealford.com 或电子邮件 nford@thoughtworks.com 与他联系。



2010 年 6 月 24 日

系列 的前几期主要关注紧急设计中显而易见的第一步:发现 惯用模式。发现惯用模式之后,您要用它做什么?该问题的答案就是本期重点,本文属于由多个部分组成的系列文章的第二部分。第 1 部分—代码与设计的关系探讨— 介绍了一种观点的理论基础,这种观点就是软件中的设计真正是指解决方案的整个源代码。一旦转换角度将所有 代码当做实际设计,您可以开始考虑在语言级别巩固设计元素,而非仅在图表范围和其他设计辅助项目中。在这里我要讲一下在发掘出代码中的可重用设计之后应该做些什么,介绍获取这些模式所用的方法。我首先将它们作为简单 APIs 获取,然后描述一种可将这些元素与其他代码区分开来的获取方法。

关于本系列

系列 旨在从全新的视角来介绍经常讨论但是又难以理解的软件架构和设计概念。通过具体示例,Neal Ford 将帮助您在演化架构紧急设计 的灵活实践中打下坚实的基础。通过将重要的架构和设计决定推迟到最后责任时刻,您可以防止不必要的复杂度降低软件项目的质量。

将模式作为 APIs 予以获取

捕捉惯用模式最简单的方式就是将它们作为自身的 API 或框架予以提取。您使用的大多数开源框架都是与解决特定问题相关的惯用模式集。例如,Web 框架包含您构建 Web 应用程序所需的所有 API 元素,它们预先从其他运行的 Web 应用程序中获得。例如,Spring 是用于处理依赖项注入和构建的技术惯用模式集合,Hibernate 为对象-关系映射封装模式(参阅 参考资料)。

当然,您可以在您的代码中做同样的工作。这是目前为止最简单的方法,因为您改变的仅是代码的结构(通常通过在您选择的 IDE 中重构支持)。这种方法的大量示例参见 第 1 部分 以及 “语言、表达性与设计:第 2 部分”, 该部分探讨了设计模式。

避免结构重复

APIs 偶尔会促进结构重复。使用 APIs 会很烦人,因为您必须频繁使用主机对象来调用 API。下面来看一下清单 1 中的示例(其中调用一个与有轨电车相关的 API):

清单 1. 访问 Car API
Car2 car = new CarImpl();
MarketingDescription desc = new MarketingDescriptionImpl();
desc.setType("Box");
desc.setSubType("Insulated");
desc.setAttribute("length", "50.5");
desc.setAttribute("ladder", "yes");
desc.setAttribute("lining type", "cork");
car.setDescription(desc);

强制用户输入主机对象(desc)会给代码增加不必要的干扰。大部分 APIs 包括主机对象并将其作为 API 的入口点,您必须携带它们才能访问 API。

目前有几个方法可缓减 APIs 中的这个问题。其中一种方法使用一个鲜为人知的 Java 语法,它允许您通过一个匿名内部类的作用域界定 “携带” 主机对象,如清单 2 所示:

清单 2. 使用一个匿名内部类携带主机对象
MarketingDescription desc = new MarketingDescriptionImpl() {{
    setType("Box");
    setSubType("Insulated");
    setAttribute("length", "50.5");
    setAttribute("ladder", "yes");
    setAttribute("lining type", "cork");

}};

为了便于您理解清单 2,我必须深入探究一个小问题,即 Java 语言如何处理初始化。请看一下清单 3 中的代码:

清单 3. Java 语言中的初始化设置
public class InitializerDemo {
    public InitializerDemo() {
        out.println("in constructor");
    }

    static {
        out.println("in static initializer");
    }

    {
        out.println("in instance initializer");
    }

    public static void main(String[] args) {
        out.println("in main() method");
        new InitializerDemo();
    }
}

清单 3 中的示例展示了 Java 语言中的 4 种不同的初始化方法:

  • main() 方法中
  • 在构造函数中
  • 在一个静态 初始化块中,在加载类时执行
  • 在一个初始化块中,仅在构造函数之前执行

执行顺序如图 1 所示:

图 1. Java 语言中的初始化顺序
Java 中的初始化顺序图解

加载类之后,静态初始化器首先运行,紧接着运行的是 main 方法(也是静态的)。之后,Java 平台汇集所有实例 初始化块并在构造函数之前执行它们,最后运行构造函数本身。实例初始化器允许您为一个匿名内部类执行构造代码。事实上,它是惟一真实的初始化机制,因为要为一个匿名内部类编写一个构造函数是不可能的 — 构造函数必须与类具有相同的名称,但是匿名内部类下面的类没有 名称。

通过使用一种本质上不太智慧的 Java 技巧,您可以避免重用要执行的一系列方法的主机名。但是,这样做的代价就是,会有一个奇怪的语法令您的同事备受困扰。

负面效应

将 APIs 作为惯用模式进行提取是一种极其有效的方法,而且可能是利用您所发现的可重用 gems 最常见的方式。该方法的缺点在于其常态:难以区分您提取的设计元素,因为它们看起来就像您的所有其他代码。项目中您的接任人会很难理解您创建的 API 会与其周围的代码有所不同,因此您通过探测发现模式的努力可能会付之一炬。不过,如果您可以将惯用模式从其他代码中凸显出来,这样就可以更容易地看到它的不同。


使用元程序设计

元程序设计提供一种不错的方式将模式代码与实现代码区分开来,因为您使用关于 代码的代码来表达您的模式。Java 语言提供的一种不错的方法就是属性。您可以通过定义属性来创建声明性元程序设计标记。属性提供一种简明的方式来表达概念。您可以将大量功能装入一个小空间,方法就是将其定义为一个属性并修饰相关的类。

这里有一个很好的示例。大多数项目中最常见的技术惯用模式是验证,它非常适用于声明性代码。如果您将验证模式作为属性予以获取,可以用明确的验证约束标出您的代码,这不会影响代码的主旨。下面看一下清单 4 中的代码:

清单 4. MaxLength 属性
public class Country {
	private List<Region> regions = new ArrayList<Region>();
	private String name;
	
	public Country(String name){
		this.name = name;
	}
	
	@MaxLength(length = 10)
	public String getName(){
		return name;
	}
	
	public void addRegion(Region region){
		regions.add(region);
	}
	
	public List<Region> getRegions(){
		return regions;
	}
}

使用属性标记代码元素的能力揭示了您的意图,即让一些外部因素对后面的代码起作用。这反而更易于区分模式部分和实现部分。您的验证代码很醒目,是因为它看起来 不像周围的其他代码。这种通过功能划分代码的方式使我们更易识别特定职责、进行重构和维护工作。

MaxLength 验证程序规定 Country 名不能超过 10 个字符。属性声明本身出现在清单 5 中:

清单 5. MaxLength 属性声明
@Retention(RetentionPolicy.RUNTIME)
public @interface MaxLength {
	int length() default 0;
}

MaxLength 验证程序的实际功能存在于两个类中:名为 Validator 的一个抽象类及其具体实现 MaxLengthValidatorValidator 类出现在清单 6 中:

清单 6. 提取基于属性的 Validator
public abstract class Validator {

    public void validate(Object obj) throws ValidationException {
        Class clss = obj.getClass();
        for(Method method : clss.getMethods())
            if (method.isAnnotationPresent(getAnnotationType()))
                validateMethod(obj, method, method.getAnnotation(getAnnotationType()));
    }

    protected abstract Class getAnnotationType();
    protected abstract void validateMethod(
        Object obj, Method method, Annotation annotation);
}

该类通过查看 getAnnotationType() 来迭代类中的方法,以确定这些方法是否修饰有特定属性;当它找到一个方法时,就执行 validateMethod() 方法。MaxLengthValidator 类的实现见清单 7:

清单 7. MaxLengthValidator
public class MaxLengthValidator extends Validator {

    protected void validateMethod(Object obj, Method method, Annotation annotation) {
        try {
            if (method.getName().startsWith("get")) {
                MaxLength length = (MaxLength)annotation;
                String value = (String)method.invoke(obj, new Object[0]);
                if ((value != null) && (length.length() < value.length())) {
                    String string = method.getName() + " is too long." + 
                        "Its length is " + value.length() + 
                        " but should be no longer than " + length.length();
                    throw new ValidationException(string);
                }
            }
        } catch (Exception e) {
            throw new ValidationException(e.getMessage());

        }
    }

    @Override
    protected Class getAnnotationType() {
        return MaxLength.class;
    }
}

该类从 get 开始检查方法是否经过潜在验证,然后获取注释中的元数据,最后检查属性相对于所声明长度的 length 字段值,在出现违规时抛出验证错误。

属性可以完成很高级的工作。请看下面清单 8 中的例子:

清单 8. 带惟一性验证的类
public class Region {
    private String name = "";
    private Country country = null;
    
    public Region(String name, Country country) {
        this.name = name;
        this.country = country;
        this.country.addRegion(this);
    }

    public void setName(String name){
        this.name = name;
    }
    
    @Unique(scope = Country.class)
    public String getName(){
        return this.name;
    }
    
    public Country getCountry(){
        return country;
    }
}

要声明 Unique 属性很简单,如清单 9 所示:

清单 9. Unique 属性
@Retention(RetentionPolicy.RUNTIME)
public @interface Unique {
	Class scope() default Unique.class;
}

Unique 属性实现类扩展了 清单 6 中所示的 Validator 抽象类。如清单 10 所示:

清单 10. 惟一验证程序实现
public class UniqueValidator extends Validator{

  @Override
  protected void validateMethod(Object obj, Method method, Annotation annotation) {
    Unique unique = (Unique) annotation;
    try {
      Method scopeMethod = obj.getClass().getMethod("get" + 
          unique.scope().getSimpleName());
      Object scopeObj = scopeMethod.invoke(obj, new Object[0]);
      
      Method collectionMethod = scopeObj.getClass().getMethod(
          "get" + obj.getClass().getSimpleName() + "s");
      List collection = (List)collectionMethod.invoke(scopeObj, new Object[0]);
      Object returnValue = method.invoke(obj, new Object[0]);
      for(Object otherObj: collection){
        Object otherReturnValue = otherObj.getClass().
            getMethod(method.getName()).invoke(otherObj, new Object[0]);
        if (!otherObj.equals(obj) && otherReturnValue.equals(returnValue))
          throw new ValidationException(method.getName() + " on " + 
            obj.getClass().getSimpleName() + " should be unique but is not since");
      }
    } catch (Exception e) {
      System.out.println(e.getMessage());
      throw new ValidationException(e.getMessage());
    }
  }

  @Override
  protected Class getAnnotationType() {
    return Unique.class;
  }
}

该类必须执行相当数量的工作来确保一个国家名的值是惟一的,不过它也展示了属性在 Java 编程中的强大功能。

属性是 Java 语言中备受青睐的一部分。您可以通过它们精确地定义有较广影响而在目标类中有较少语法残留的行为。但是,与 JRuby 等 JVM 上更具表达性的语言相比,它们所做的工作仍然很有限。

使用 JRuby 的 sticky 属性

Ruby 语言也有属性(不过它们不像 “属性” 一样有特定名称 — 它们是 Ruby 提供的其中一种元程序设计方法)。这里有一个例子。请看清单 11 中的测试类:

清单 11. 测试一个复杂的运算
class TestCalculator < Test::Unit::TestCase
  def test_complex_calculation
    assert_equal(4, Calculator.new.complex_calculation)
  end
end

如果 complex_calculation 方法运行时间较长,您只想在执行验收测试时运行它,而不想在单元测试期间运行它。进行该限制的一种方式见清单 12:

清单 12. 限制测试范围
class TestCalculator < Test::Unit::TestCase

  if ENV['BUILD'] == 'ACCEPTANCE'
    def test_complex_calculation
       assert_equal(4, Calculator.new.complex_calculation)    
    end
  end
  
end

这是与测试相关的一种技术惯用模式,我可在多个上下文中轻松预见该测试的有用性。在一个 if 块中包装方法声明为我的代码增加了复杂度,因为并非所有方法声明都使用相同的缩进。因此我将使用一个属性捕捉该模式,如清单 13 所示:

清单 13. 在 Ruby 中声明一个属性
class TestCalculator < Test::Unit::TestCase  
  extend TestDirectives 
  
  acceptance_only
  def test_complex_calculation
    assert_equal(4, Calculator.new.complex_calculation)        
  end
end

该版本更清晰且易于读取。清单 14 中所示的实现无关紧要:

清单 14. 属性声明
module TestDirectives
  def acceptance_only
    @acceptance_build = ENV['BUILD'] == 'ACCEPTANCE'
  end
  
  def method_added(method_name)
    remove_method(method_name) unless @acceptance_build
    @acceptance_build = false
  end
end

在 Ruby 中使用如此少的代码所能完成的工作令人惊叹。清单 14 声明了一个 module,它是 Ruby 的混合版本。一个混合版本含有一个您可以包括(include)到类中的功能,从而将该功能添加到类中。您可以将其作为一种接口,一种可包含代码的接口。该模块定义一个名为 acceptance_only 的方法,该方法检查 BUILD 环境变量,确定哪个测试阶段处于执行中。一旦设置了这个标志,模块利用一个 hook 方法。Ruby 中的 Hook 方法在解译时(而非运行时)执行,且每次向类添加新方法时该 hook 方法都会启动。如果设置了 acceptance_build 标志,该方法在执行时会删除刚才定义的方法。然后将标记设置回 false。(否则,该属性会影响所有随后的方法声明,因为标记仍然为真。)如果您希望它影响包含诸多方法的代码块,您可以删除标志的重新设置,让该行为一直保持到有其他因素(比如用户定义的 unit_test 属性)改变它时。(这些通俗地讲就叫做 sticky 属性。)

为阐述该机制的功能,Ruby 语言本身使用 sticky 属性来声明 privateprotectedpublic 类作用域修饰符。没错 — Ruby 中的类作用域界定不是关键词,它们仅仅是 sticky 属性。


结束语

在本期中,我们展示了如何使用 APIs 和属性作为获取惯用模式的方法。如果您能够设法将获取的模式从其他代码中凸显出来,那么就更易于同时读取两种代码,因为它们不相互混杂。

在下一期中,我们将继续展示如何通过用于构建域特定语言的一系列方法获取惯用模式。

参考资料

学习

  • The Productive Programmer(Neal Ford,O'Reilly Media,2008):Neal Ford 最近撰写的这本书对本系列中的很多话题作了详细的阐述。
  • 脱离 Rails 看 Ruby”(Andrew Glover,developerWorks,2005 年 12 月):从 Java 开发人员的角度认识 Ruby。
  • Hibernate:这个流行的开放源代码对象-关系映射框架封装了很多便利的惯用模式。
  • Spring:Spring 框架被认为是 Java 领域中最有用的框架之一。
  • 浏览 技术书店,找到关于这些技术专题和其他技术专题的书籍。
  • developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=496863
ArticleTitle=演化架构和紧急设计: 利用可重用代码,第 2 部分
publish-date=06242010