简介:在使用 演化架构和紧急设计 前几期描述的技术发现 代码中的紧急设计之后,下一步您需要一 种获取和利用这些设计元素的方法。本文介绍了两种用于获取惯用模式的方法:将模式作为 APIs 进行捕 捉;使用元程序设计方法。
本 系列 的前几期主要关注紧急设计中显而易见的第一步:发现 惯用模式。发现惯用模式之后,您要 用它做什么?该问题的答案就是本期重点,本文属于由多个部分组成的系列文章的第二部分。第 1 部分 —代码与设计的关系探讨— 介绍了一种观点的理论基础,这种观点就是软件中的设计真正是指解决方案 的整个源代码。一旦转换角度将所有 代码当做实际设计,您可以开始考虑在语言级别巩固设计元素,而 非仅在图表范围和其他设计辅助项目中。在这里我要讲一下在发掘出代码中的可重用设计之后应该做些什 么,介绍获取这些模式所用的方法。我首先将它们作为简单 APIs 获取,然后描述一种可将这些元素与其 他代码区分开来的获取方法。
将模式作为 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 语言中的初始化顺序
加载类之后,静态初始化器首先运行,紧接着运行的是 main 方法(也是静态的)。之后,Java 平台 汇集所有实例 初始化块并在构造函数之前执行它们,最后运行构造函数本身。实例初始化器允许您为一 个匿名内部类执行构造代码。事实上,它是惟一真实的初始化机制,因为要为一个匿名内部类编写一个构 造函数是不可能的 — 构造函数必须与类具有相同的名称,但是匿名内部类下面的类没有 名称。
通过使用一种本质上不太智慧的 Java 技巧,您可以避免重用要执行的一系列方法的主机名。但是, 这样做的代价就是,会有一个奇怪的语法令您的同事备受困扰。
负面效应
将 APIs 作为惯用模式进行提取是一种极其有效的方法,而且可能是利用您所发现的可重用 gems 最 常见的方式。该方法的缺点在于其常态:难以区分您提取的设计元素,因为它们看起来就像您的所有其他 代码。项目中您的接任人会很难理解您创建的 API 会与其周围的代码有所不同,因此您通过探测发现模 式的努力可能会付之一炬。不过,如果您可以将惯用模式从其他代码中凸显出来,这样就可以更容易地看 到它的不同。
使用元程序设计
元程序设计提供一种不错的方式将模式代码与实现代码区分开来,因为您使用关于 代码的代码来表达 您的模式。Java 语言提供的一种不错的方法就是属性。您可以通过定义属性来创建声明性元程序设计标 记。属性提供一种简明的方式来表达概念。您可以将大量功能装入一个小空间,方法就是将其定义为一个 属性并修饰相关的类。
这里有一个很好的示例。大多数项目中最常见的技术惯用模式是验证,它非常适用于声明性代码。如 果您将验证模式作为属性予以获取,可以用明确的验证约束标出您的代码,这不会影响代码的主旨。下面 看一下清单 4 中的代码:
清单 4. MaxLength 属性
public class Country { private List regions = new ArrayList(); 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 getRegions(){ return regions; }}
使用属性标记代码元素的能力揭示了您的意图,即让一些外部因素对后面的代码起作用。这反而更易 于区分模式部分和实现部分。您的验证代码很醒目,是因为它看起来 不像周围的其他代码。这种通过功 能划分代码的方式使我们更易识别特定职责、进行重构和维护工作。
MaxLength 验证程序规定 Country 名不能超过 10 个字符。属性声明本身出现在清单 5 中:
清单 5. MaxLength 属性声明
@Retention(RetentionPolicy.RUNTIME)public @interface MaxLength { int length() default 0;}
MaxLength 验证程序的实际功能存在于两个类中:名为 ValidaTor 的一个抽象类及其具体实现 MaxLengthValidaTor。ValidaTor 类出现在清单 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 therReturnValue = 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) endend
如果 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 endend
这是与测试相关的一种技术惯用模式,我可在多个上下文中轻松预见该测试的有用性。在一个 if 块 中包装方法声明为我的代码增加了复杂度,因为并非所有方法声明都使用相同的缩进。因此我将使用一个 属性捕捉该模式,如清单 13 所示:
清单 13. 在 Ruby 中声明一个属性
class TestCalculaTor < Test::Unit::TestCase extend TestDirectives acceptance_only def test_complex_calculation assert_equal(4, CalculaTor.new.complex_calculation) endend
该版本更清晰且易于读取。清单 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 endend
在 Ruby 中使用如此少的代码所能完成的工作令人惊叹。清单 14 声明了一个 module,它是 Ruby 的 混合版本。一个混合版本含有一个您可以包括(include)到类中的功能,从而将该功能添加到类中。您 可以将其作为一种接口,一种可包含代码的接口。该模块定义一个名为 acceptance_only 的方法,该方 法检查 BUILD 环境变量,确定哪个测试阶段处于执行中。一旦设置了这个标志,模块利用一个 hook 方 法。Ruby 中的 Hook 方法在解译时(而非运行时)执行,且每次向类添加新方法时该 hook 方法都会启 动。如果设置了 acceptance_build 标志,该方法在执行时会删除刚才定义的方法。然后将标记设置回 false。(否则,该属性会影响所有随后的方法声明,因为标记仍然为真。)如果您希望它影响包含诸多 方法的代码块,您可以删除标志的重新设置,让该行为一直保持到有其他因素(比如用户定义的 unit_test 属性)改变它时。(这些通俗地讲就叫做 sticky 属性。)
为阐述该机制的功能,Ruby 语言本身使用 sticky 属性来声明 private、protected 和 public 类作 用域修饰符。没错 — Ruby 中的类作用域界定不是关键词,它们仅仅是 sticky 属性。
结束语
在本期中,我们展示了如何使用 APIs 和属性作为获取惯用模式的方法。如果您能够设法将获取的模 式从其他代码中凸显出来,那么就更易于同时读取两种代码,因为它们不相互混杂。
在下一期中,我们将继续展示如何通过用于构建域特定语言的一系列方法获取惯用模式。
爱人,却不一定能够听懂。他们听见的,多是抱怨不休,心烦意乱。