架构的演化和不断新兴的设计

Evolutionary architecture and emergent design: Leveraging reusable code, Part 1

这是IBM的一篇文章,一旦你确认了惯用的模式,下一步你就进入收获期,理解设计和代码之间关系,能够发现可重用复用的代码,这篇文章阐述了代码与设计的关系,指出重要是用一个可表达有表现力的语言,然后重新不断思考抽象,挖掘潜在价值。

文章指出两个惯用模式:
1.技术模式,这些模式强调事务 安全和其他底层技术元素。
2.领域模式,这些包括解决业务应用的通用方案。

文章认为:设计就是代码。从表现力和抽象风格来谈论两者关系。完整的代码是设计工作的目标和结果。

理解这个点就不能理解为什么一些开发失败。比如MDA模型驱动架构中,试图直接从UML产生代码,这是失败的,因为UML语言不够表现力,不够丰富,不能表达细微差别。

要认识到设计是最昂贵的活动(banq:有些公司开发图形化的拖曳或快速开发软件方式,从方向上应该迟早会失败的,如果你能通过开发专门工具来解决这个最昂贵的活动,那么你就是上帝了)。

当然并不是反对你用UML,一旦代码出来后,你就要明白你进入了新的设计阶段,你就要迁移到这个阶段,这个阶段可读性设计是关键(banq:有些图形化开发工具开发或生成出来的代码几乎不能看,二次开发还需要通过开发工具去修改代码,不能直接阅读代码,这个方向也大大出错了,不过因为中国人不懂英语,所以中国软件公司总是试图阻止程序员直接读英文程序代码,而是搞图形化界面,结果越搞越复杂)。

而让代码变得可读,就要使用一些惯用的模式,这样就越容易修改维护拓展,凡是知道这些惯用模式的程序员都能读懂你的代码。(当然这个要求对程序员要高,不但要懂Java语言,还要懂模式)

banq总结:整篇文章虽然没有提DDD,但是和DDD或MDD领域驱动设计开发思想是一致的。
[该贴被banq于2010-07-23 18:43修改过]
[该贴被banq于2010-07-23 18:44修改过]

文章在语言表达力上使用Java Groovy和Ruby来进行了比较,暗指这些语言在细节表达力上要强于UML或其他图形化表达手段。

下面是一段增加订单的业务操作和技术事务保证操作混合在一起的代码:


public void addOrderFrom(ShoppingCart cart, String userName,
Order order) throws SQLException {
setupDataInfrastructure();//底层技术
try {
add(order, userKeyBasedOn(userName));
//业务操作
addLineItemsFrom(cart, order.getOrderKey());
//业务操作
completeTransaction();
//底层技术事务
} catch (SQLException sqlx) {
rollbackTransaction();
throw sqlx;
} finally {
cleanUp();
}
}

很显然,这段代码违背职责单一原则,我们需要对其分离,分离后可再组合应用。使用command模式:


public void wrapInTransaction(Command c) {
setupDataInfrastructure();
try {
c.execute();
completeTransaction();
} catch (RuntimeException ex) {
rollbackTransaction();
throw ex;
} finally {
cleanUp();
}
}

public void addOrderFrom(final ShoppingCart cart, final String userName,
final Order order) throws SQLException {
wrapInTransaction(new Command() {
public void execute() {
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
}
});
}

熟悉Spring+hibernate可以知道,其HibernateTemplate中就是如此实现的。

如果使用Groovy来实现,则会更加具备可读性:


public class OrderDbClosure {
def wrapInTransaction(command) {
setupDataInfrastructure()
try {
command() //关键一句 直接运行方法体内代码
completeTransaction()
} catch (RuntimeException ex) {
rollbackTransaction()
throw ex
} finally {
cleanUp()
}
}

def addOrderFrom(cart, userName, order) {
wrapInTransaction {
add order, userKeyBasedOn(userName)
addLineItemsFrom cart, order.getOrderKey()
}
}
}

由于Groovy语言内置了command模式,所以这里实现起来更加简单直接。

使用ruby实现:


def wrap_in_transaction
setup_data_infrastructure
begin
yield //关键一句
complete_transaction
rescue
rollback_transaction
throw
ensure
cleanup
end
end

def add_order_from
wrap_in_transaction do
add order, user_key_based_on(user_name)
add_line_items_from cart, order.order_key
end
end

通过Java以及Java的框架 以及groovy与Ruby比较,展现了语言表达力的魅力,这里体现了模式和语言的关系:如果你为汇编语言找一套惯用的模式是非常困难的,因为语言本身增加了很多不透明因素让你无法看到真正的设计。

由于设计就是代码,代码就是设计,你必须选择最能表达你设计的语言,凭借语言的表现力可以跟容易看到你惯用的模式,也就更容易看到你的设计。

文章认为设计或编码是一项精益活动,精益活动有一个原则:你等待时间越长,你就越有机会寻找到合适的设计。

个人观点:个人体会,不愿意做项目期限很紧的软件,愿意一直精益做好一个软件,比如JiveJdon;有新项目时,也不是急于立即下手,而是让自己等待。越长时间进行设计决定,你就拥有越多的设计语境,对设计的场景背景就越多理解,就能越多寻找到合适的设计。

当你在一个语言上工作很久,会陷入一种具体战术性细节,而忽视抽象战略思想,当你拥有一把锤子,每个问题看起来都是钉子,每个问题都可以使用你熟悉的语言来抽象解决。

如果你熟悉java语言,再使用面向函数语言如scala等,你的思维方式就会不一样,你就不会再用你熟悉的锤子从一个角度解决问题,而是更换了另外一个角度,比如Java中数据结构缺省是可变的,这在一个多线程环境中是危险的,你可以使用很多代码和模式在Java中实现不可变;但是Scala这些语言数据结构缺省是不可变的,那么在多线程环境中就显然更加干净。

OOPSLA 2006曾经提出反对象概念:"Collaborative Diffusion: Programming Antiobjects" , 引入一个叫反对象antiobject, 它也是一个对象,但是它是我们通常认为应该是什么样的反面。这实际是用来扩展我们的思维的一种方法,否定之否定,从另外一个角度重新思考需求,重新思考问题。

到现在才有这种感觉。熟能生巧,当你的代码写的多了,一个会思考的人,自然会有对模式的需求。

确实这样 需要仔细琢磨

受教了。不过,这里有一个疑惑。
就是Java内部类只接收外部的final型的数据,所以也就不能修改外部的数据了。
比如查询操作,从内部类返回数据给外部,需要对外部数据进行修改,banq是如何解决的?
可以想到的主要有两种,第一种是将内部类拉到外面来,不过这样类的数量会膨胀得惊人,就不采用了;第二种,就是定义返回数据的引用的容器,这有点不太自然,原来只需要定义个引用,指向返回的结果就可以了。

返回单个数据, 还有更自然的方法吗?


public Order getOrder(final int orderId) throws SQLException {
final Order[] orders = new Order[1];
wrapInTransaction(new Command() {
public void execute() {
orders[0] = /* select * from order where id = orderId */
}
});
return order[0];
}

返回列表数据, 看起来似乎好一些, 不过还是new一个列表出来,显得多余,但好像也没有办法。


public List<Order> getOrders() throws SQLException {
final List<Order> orders = new ArrayList<Order>();
wrapInTransaction(new Command() {
public void execute() {
orders.addAll(/* select * from order */)
}
});
return orders;
}

banq没空回复呀:(
无意看到CQRS(http://www.jdon.com/jivejdon/thread/37891),明白了。
应该使用第三种方案,即CQRS(Command Query Responsibility Segregation )解决方案。
对于修改性操作使用命令模式,对于非修改性(查询)操作使用门面模式,平时对四种操作都是使用门面模式。
这样可以在门面下添加缓存、分页等机制,对查询进行优化,确实不错,我尝试使用的。

不过,又添加了一个疑惑。
对于增删改查的操作的分类角度似乎不止这一个:即分为修改性与非修改性操作,进行区分处理。
记得您好像也曾在某个帖子说过,可以分为:内部操作(update/delete)和外部操作(retrieve/create),且在REST风格的解决方案中(http://www.ibm.com/developerworks/cn/web/wa-aj-tomcat/),似乎就是这么划分的。

这两种划分方法,孰优孰劣,也许要看在各种场景下,谁更为自然。也许在处理Domain与DB的边界采用将“修改性与非修改性"的操作进行分离更为合适,但也许在处理Domain与Web UI的边界采用“内部与外部”的操作进行分离更为合适?

以上是我的一些想法,本人刚刚从事J2EE的开发,盼望能得到banq大师的指点一二。:)