本系列的前几期文章重点介绍了紧急设计中显而易见的第一步: 发现惯用模式。 找到一个后,您将如何处理? 这个问题的答案是本期的重点,这是多部分文章的第二部分。 第1部分 (讨论代码与设计之间的关系)涵盖了软件设计实际上是指解决方案的整个源代码这一观点的理论基础。 一旦将您的观点转变为将所有代码视为真实的设计,就可以开始考虑在语言级别上整合设计元素,而不仅仅是在图和其他设计辅助工件领域。 在这里,我将讨论一旦您在代码中发现了可重用的设计(包括收获这些模式的技术)后该怎么做。 我将以简单的API来收集它们,然后说明一种收集技术,使您能够将这些元素与其他代码区分开。
收集模式作为API
捕获惯用模式的最简单方法是将它们提取为自己的API或框架。 您使用的大多数开源框架都是与解决特定问题有关的惯用模式集。 例如,Web框架包含构建Web应用程序所需的所有API元素,这些API元素是从其他工作的Web应用程序中预先获取的。 例如,Spring是用于处理依赖项注入和构造的技术惯用模式的集合,而Hibernate封装了用于对象关系映射的模式(请参阅参考资料 )。
当然,您可以在代码中执行相同的操作。 到目前为止,这是最简单的技术,因为您只是在更改代码的结构(通常通过选择的IDE中的重构支持)。 在第1部分以及讨论设计模式的“ 语言,表现力和设计,第2部分 ”中,可以找到该技术的许多示例。
避免结构重复
API偶然会鼓励结构重复。 使用API可能会很吵,因为您经常必须使用主机对象来调用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
)将不必要的噪音添加到代码中。 大多数API都包含主机对象作为API的入口点,并且您必须随身携带它们才能访问API。
存在一些缓解API中此问题的技术。 一种使用鲜为人知的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语言中的四种不同的初始化技术:
- 在
main()
方法中 - 在构造函数中
- 在静态初始化程序块中,该类在加载类时执行
- 在初始化程序块中,该程序块在构造函数之前执行
此执行顺序如图1所示:
图1. Java语言的初始化顺序
静态初始化程序首先在加载类时运行,然后运行main方法(也是静态的)。 之后,Java平台收集所有实例初始化程序块,并在构造函数之前执行它们,最后是构造函数本身。 实例初始化器使您可以执行匿名内部类的构造代码。 事实上,它是唯一真正的初始化机制,因为它是不可能写出一个构造一个匿名内部类-构造必须具有相同的名称作为类,但一个匿名内部类的类没有名字。
通过使用本质上是愚蠢的Java技巧,您可以避免重用要执行的一系列方法的主机名。 但是,这是以一种奇怪的语法为代价的,该语法可能会让您的同事挠头。
不足之处
将惯用模式提取为API是一种完全有效的技术,并且可能是利用您发现的可重用宝石的最常用方法。 这种方法的缺点在于它的常规性:很难区分您提取的设计元素,因为它们看起来就像您的所有其他代码一样。 您的项目后继者将很难理解您创建的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
的抽象类,以及一个名为MaxLengthValidator
的具体实现。 清单6中显示了Validator
类:
清单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()
方法。 清单7中显示了MaxLengthValidator
类的实现:
清单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
开头,然后从注释中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;
}
}
清单9中所示的Unique
属性的声明非常简单:
清单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语言的一个受欢迎的补充。 它们使您可以简洁地定义在目标类中几乎没有语法宿醉影响的行为。 但是,与您可以在JVM等更具表达能力的语言(例如JRuby)中进行的工作相比,它们仍然受到限制。
使用JRuby的粘性属性
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
环境变量以查看正在执行哪个测试阶段。 设置此标志后,模块将利用挂钩方法。 Ruby中的挂钩方法在解释时(而不是运行时)执行,并且每次向类添加新方法时都会触发此特定的挂钩方法。 执行此方法时,如果设置了acceptance_build
标志,它将删除刚刚定义的方法。 然后,将标志设置回false
。 (否则,此属性将影响所有后续方法声明,因为该标志将保持为真。)如果您希望它影响包含多个方法的代码块,则可以删除该标志的重置,从而使此行为一直保持到其他状态。 (例如用户定义的unit_test
属性)对其进行更改。 (这些非正式地称为粘性属性 。)
为了说明这种机制的强大功能,Ruby语言本身使用粘性属性来声明private
, protected
和public
class-scope修饰符。 没错-Ruby中的类作用域指定不是关键字,它们只是粘性属性。
结论
在本期中,我演示了如何使用API和属性作为收集惯用模式的技术。 如果您找到一种使采摘的模式从其他代码中脱颖而出的方法,那么阅读这两种代码会更容易,因为它们不会互相污染。
在下一部分中,我将继续展示如何收集惯用模式,以及通常用于构建特定于域的语言的一系列技术。
翻译自: https://www.ibm.com/developerworks/java/library/j-eaed12/index.html