演化架构和紧急设计: 设计的环境因素,第 2 部分

重构和企业架构的影响

企业软件项目并不是真空存在的。环境因素有很大影响,甚至对纯技术决策也有影响。本期 演化架构和紧急设计 继续讨论这些环境因素,特别是重构和架构与设计之间的交叉。

Neal Ford, 软件架构师, ThoughtWorks Inc.

Neal Ford 是一家全球性 IT 咨询公司 ThoughtWorks 的软件架构师和 Meme Wrangler。他的工作还包括设计和开发应用程序、教材、杂志文章、课件和视频/DVD 演示,而且他是各种技术书籍的作者或编辑,包括最近的新书 The Productive Programmer。他主要的工作重心是设计和构建大型企业应用程序。他还是全球开发人员会议上的国际知名演说家。请访问他的 Web 站点



2011 年 3 月 28 日

关于本系列

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

上一期 中,我们讨论了软件设计的环境因素。企业软件开发绝不是真空存在的;明确的技术决策会被政治和其他外部因素复杂化。这一期,继续介绍重构和隔离架构更改。

智能重构

重构,由 Martin Fowler 在关于该课题的重要著作中(见 参考资料)定义为一个常用的、易理解的用于提高代码质量的技术。紧急设计的一个关键性促进因素是捕获和使用您发现的惯用模式的能力。就机制而言,这意味着重构。但是重构也包含项目的环境因素。所有主要 IDE 现在支持重构 — 但是您不能依赖一个工具来执行智能 重构,只能执行正确的 重构。

在该系列的 第 4 期 中,我讨论了 Single Level of Abstraction Principle (SLAP),使用样例电子商务网站中的一个方法作为重构目标来提高其清晰度,该方法在清单 1 中显示:

清单 1. 一个 addOrder() 方法,来自样例电子商务网站
public void addOrder(ShoppingCart cart, String userName,
                     Order order) throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;
    Statement s = null;
    ResultSet rs = null;
    boolean transactionState = false;
    try {
        s = c.createStatement();
        transactionState = c.getAutoCommit();
        int userKey = getUserKey(userName, c, ps, rs);
        c.setAutoCommit(false);
        addSingleOrder(order, c, ps, userKey);
        int orderKey = getOrderKey(s, rs);
        addLineItems(cart, c, orderKey);
        c.commit();
        order.setOrderKeyFrom(orderKey);
    } catch (SQLException sqlx) {
        s = c.createStatement();
        c.rollback();
        throw sqlx;
    } finally {
        try {
            c.setAutoCommit(transactionState);
            dbPool.release(c);
            if (s != null)
                s.close();
            if (ps != null)
                ps.close();
            if (rs != null)
                rs.close();
        } catch (SQLException ignored) {
        }
    }
}

在 SLAP 那一期中,我说明了阅读从一个抽象层跳到另一个抽象层的代码有多么的困难,重构了 清单 1 中的代码来提高可读性。然而,在那一期中,我为您展示的是重构的最终结果,而没有展示中间过程,在清单 2 中我将向您展示:

清单 2. addOrder() 方法中间的重构阶段
public void addOrder(ShoppingCart cart, String userName,
                     Order order) throws SQLException {
    Connection connection = null;
    PreparedStatement ps = null;
    Statement statement = null;
    ResultSet rs = null;
    boolean transactionState = false;
    try {
        connection = dbPool.getConnection();
        statement = connection.createStatement();
        transactionState =
                setupTransactionStateFor(connection,
                        transactionState);
        addSingleOrder(order, connection,
                ps, userKeyFor(userName, connection));
        order.setOrderKeyFrom(generateOrderKey(statement, rs));
        addLineItems(cart, connection, order.getOrderKey());
        completeTransaction(connection);
    } catch (SQLException sqlx) {
        rollbackTransactionFor(connection);
        throw sqlx;
    } finally {
        cleanUpDatabaseResources(connection,
                transactionState, statement, ps, rs);
    }
}

清单 2 中的代码展示了一些自动化重构工具的固有缺点。工具必须严格遵合同:生成的代码必须能够和以前一样工作。例如,当您执行一个抽象方法 重构时,该工具必须确保抽象方法所需的所有变量仍然存在 — 但是只能通过参数传递确保它们存在。解决这个问题的另一个方法是将共享变量上移到类级别,使其成为类的字段。重构工具做不到这一点,因为它不能考虑到这样一个决策细节的严重影响 — 比如线程问题、命名和在其他方法中的可用性。不过,一个开发人员可以决定采取一系列考虑这些问题的手工重构。addOrder() 的手工重构结果如清单 3 所示:

清单 3. 手工重构,极大地改善了 addOrder() 代码
public void addOrderFrom(ShoppingCart cart, String userName,
                     Order order) throws Exception {
    setupDataInfrastructure();
    try {
        add(order, userKeyBasedOn(userName));
        addLineItemsFrom(cart, order.getOrderKey());
        completeTransaction();
    } catch (Exception condition) {
        rollbackTransaction();
        throw condition;
    } finally {
        cleanUp();
    }
}

这就是在 “利用可重用代码,第 1 部分” 中我最终重构到它的惯用模式中的代码。它比原来的代码好多了,因为您可以看到它正在做什么,可从重用部分获益。

您不能一味地期望一个工具可为您制定出好的决策。工具只能确保代码正确,但不一定是最优的。要有效地发现可重用资产,您必须经常超越自动化工具所提供的。而且必须利用团队其他成员的集体智慧。

集体代码所有权

破窗理论

The Pragmatic Programmer(见 参考资料)中,Dave Thomas 和 Andy Hunt 借用了废弃建筑研究中的破窗理论 概念。废弃的建筑如果完好无损则通常不会被破坏,但是如果有一扇窗户被打破,则很快会被破坏。首先,打破窗户暗示这个财物没有人关心,此后,整体失修且滥用就会逐渐加速。

破窗理论也会出现在软件开发中,如果您看到一些从技术上来说不是一个 bug 但从设计角色来说不太合适的代码,那么您就有了一个 “破损窗户”。集体代码所有权意味着您必须修复那个代码。软件工程随着时间逐渐脆弱的部分原因就是存在几百个(或者几千个)“破损窗户”。如果您对它们定期进行修复,您的代码将会随时间的推移越来越强壮,而不是脆弱。

我的项目总是使用结对编程,我们总是寻找 “破损窗户”。但是我们不会一发现问题,就马上自动放下手头的工作去解决。当我的搭档和我发现一个错误时,我们估计修复它所需的时间,如果少于 15 分钟,我们继续前进,随后使用正在运行的其他案例进行联机修复。如果更改比较复杂,我们将它添加到一个技术债务 backlog 中。我的所有项目都有一个技术债务 backlog,由技术主管维护。当我们的项目开发时间充裕时,技术主管将分配这个 backlog 中的案例,逐步解决积累的技术债务。

从整个系列中我们得到了这样一个论点,那就是在软件开发中设计不能脱离编码。完整源代码是唯一真正精确的设计工件,这表明一群人合作开发一个软件项目就是一个协同设计实践。将软件创建看作协同设计使一些令人困惑的软件开发突然变得容易理解。例如,业界很久之前就认为信息交流在软件开发中至关重要。(事实上,许多敏捷方法认为这是成功的必要条件。)如果将编写软件比作制造业,那您需要的通信开销少了很多。协同设计需要成员之间的交流。

协同设计也要求开发人员对他们所创建的整体应用程序的正确性和质量负责。正确性 有两个方面:遵守业务需求和技术适用性。业务需求的正确性由您公司生态系统目前已到位的任何核查机制确定,该核查机制用于判断软件对其要解决的问题的适用性。技术正确性是留给开发团队的。

各个团队将他们自己的流程和机制落实到位来确保技术质量,比如代码审查和通过持续集成运行的自动化度量工具。许多敏捷团队采用的一个实践是集体代码所有权,这是紧急设计的一个关键性促进因素,这意味着,不仅仅是代码的编写者,项目中的每个人都对所有代码有所有权责任。

集体代码所有权需要对项目的代码质量有一定的关注,特别是要带着追踪和修复逐渐废除的抽象概念和 破损窗户 的目的来关注。更具体得说,它要求:

  • 频繁地进行代码审查(或者实时代码审查,比如结对编程),确保每个人都使用相同的惯用模式和团队所发现的其他有用设计。
  • 项目中的每个人至少了解该项目中所有组件的部分详情。
  • 项目中的每个人都要自愿参与修复 “破损窗户”,不管代码的原作者是谁。

机制和环境两方面的重构因素都会影响紧急设计决策,紧急设计效能的另一个环境因素涉及到隔离架构更改。


隔离架构更改

在本系列中,我已经将设计问题从架构问题中分离出来了,但是当然在真实生活中分离是相当困难的。您的架构是您所有设计决策的基础,而且架构因素可能会影响您使用我之前介绍的那些紧急设计技术的能力。

过度的一般性(Rampant genericness)

在软件中,一个常见的架构通病是过度的一般性,其蕴藏的思想是:如果您添加许多用于扩展的层,后期便可轻松在其上构建更多的层。不可否认早期将扩展机制落实到位可以使后期的扩展更为容易。但是复杂度的增加是从添加这些层开始的,而不是始于使用这些层时。额外开销是由偶发复杂性形成的,直至它开始显示项目的实际获益。

一部分紧急设计能够从现有代码中查看和捕获惯用模式。如果您的所有代码都在一个物理层中,这个技术对于开发有一定难度,而且当您开始使用那些逻辑上是一个整体但是由于架构决策而跨层分离的工件时,将会变得更难。例如,我们假设您有一个 Country 类,该类有一个验证地区名长度的验证规则。理想情况下,您想利用地区优势,然后将验证代码放置到尽可能靠近域类的地方,也许是在一个注解中(该场景的具体示例见 “利用可重用代码,第 2 部分”),如清单 4 所示:

清单 4. Country 类的一个 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 void setName(String name){
		this.name = name;
	}
	
	public void addRegion(Region region){
		regions.add(region);
	}
	
	public List<Region> getRegions(){
		return regions;
	}
}

只要您的代码是位于同一个逻辑层中,清单 4 中的代码效果很不错。但是,如果架构坚持执行验证的代码必须来自一个服务层,那么会发生什么呢?现在您只剩下一个无法取胜的决策:您可以让属性理解您应用程序的架构层并通过服务层调用来执行验证吗?或者您可以将所有验证代码移入到服务层,然后从应当是该知识所有者的域类中删除它吗?

层使得设计决策更困难。我不认为您应该废除分层架构(即使是那些打乱逻辑域层的架构)。相反,我认为分层架构提供的优势是有代价的,您应该在添加架构元素(比如,一个服务层)之前评估成本。这又会涉及到另一个问题:您应该何时添加一个可能会影响您设计的架构元素?

架构 vs. 设计

贯穿整个系列,我一直依赖最简单而精确的软件架构定义:“架构是后期很难更改的东西。” 当您查看项目某部分时,您可以通过询问 “它在后期是难以更改的吗?” 来识别架构元素。根据这一定义,层(物理的或逻辑的)是架构性质的,因为在后期很难更改(或删除)它们。

一些层使得开发(和设计)变得更加容易。使用像 Gang of Four(见 参考资料)一书中的 Model-View-Controller 这类模式分离技术责任,可使隔离路由、视图和域逻辑变得更为简单。然而,一些架构进一步分离域层、创建层来支持代码的集中化。例如,面向服务架构创建不同类型的层来更改您应用程序的拓扑结构。太多的架构分层(特别是在需要之前)使得设计更为困难,因为您需要转换设计必须依赖的基础。如果在一个虚拟机内运行的代码中难以查看一个可捕获的惯用模式,想想同时识别功能并弄清楚如何跨层捕获它以便重用会有多难。

至少部分这类问题会出现,因为我们不当地鼓励架构师。架构师的一个很重要的动机是确保整个企业生态系统可以正常工作,即使面对不断变化的需求和功能。在软件开发中,我们会遭遇美国前任国防部长唐纳德·拉姆斯菲尔所说的 “未知的未知数”。软件开发充满了尚未意识到的未知数 — 通常会有这种情况,您认为您在开始的时候已经理解了,却发现问题不同于您的第一印象(通常更复杂)。您如何应对架构中的这些未知的未知数呢?您试图过度设计一切内容。我相信在 2000 年初很多项目包含 Enterprise Java™Beans (EJB) 技术,因为经常遇到这种问题 “我们会永远需要声明性、分布式事务吗?” 答案是 “我们也不知道。” 最安全的方法是将 EJB 包含在项目中,以防日后需要使用。然而这使得项目从一开始就比较复杂。但是有讽刺意味的是,许多针对可扩展性落实到位的机制从一开始就将项目置于危险之中,因为额外的复杂性会迫使项目审查时间和预算。

这有一个示例,是我曾经做过的一个项目。这个项目最难的一个需求是国际化 — 在项目的版本 2 中。项目首先通过代码处理国际化,因为它最初被看成是一个架构决策:它太过普遍,以至于后期很难更改。这个特性主要体现为偶发复杂性:为了满足这种需求,我们甚至将简单的案例复杂化。花费的时间太长以至于版本 1 的交付日期被延迟。当意识到如果我们不交付版本 1 那么在版本 2 中将不会需要这一特性,技术主管决定从项目中分离出所有的国际化代码!最后,当要使用版本 2 时,一个开发人员想出了一个聪明的办法,使用元编程设计将国际化代码植入代码库中。换句话说,他设法将一个架构元素转换成了一个设计元素,使得后期更改不至于很难。预先将一些内容设计为一个架构元素(后来很难更改)有时会让您蒙蔽,想不到它作为一个轻量级设计元素时会是什么样子。

在架构与设计之间存在着一种令人不安的张力。从理论上说,您可以延迟设计决策到最后责任时刻。然而,机构决策将影响您可以制定的各种设计决策。记得架构定义的第 2 部分是:“这类东西越少越好。” 尝试延迟架构决策直至最后责任时刻,即使它们有着更大的潜在影响。您会惊奇地发现一些似乎难以改进的事情并不是很糟糕。


结束语

在这一期和上一期,我介绍了环境因素及其对紧急设计的影响。重构显然是紧急设计的一个重要工具 — 同时考虑机制和环境因素。不仅仅依赖自动重构工具,确保您可以进行智能重构。建立您的项目环境来支持集体代码所有权,利用最佳人选完成项目。

架构也会影响您的设计选择 。与设计元素一样,试着延迟架构决策直至最后责任时刻。架构元素落实到位使清单在将来作为偶发复杂性来扩展变得很容易,直至您开始使用这些元素。

下一期 演化架构和紧急设计 将是本系列的最后一篇。在其中我将总结概念,从将近两年时间内所编写和提供的紧急设计中得出结论。

参考资料

学习

讨论

  • 加入 My developerWorks 社区。查看开发人员推动的博客、论坛、组和 wikis,并与其他 developerWorks 用户交流。

条评论

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=643396
ArticleTitle=演化架构和紧急设计: 设计的环境因素,第 2 部分
publish-date=03282011