演化架构和紧急设计

设计的环境因素,第 2 部分

重构和企业架构的影响

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 演化架构和紧急设计

敬请期待该系列的后续内容。

此内容是该系列的一部分:演化架构和紧急设计

敬请期待该系列的后续内容。

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

智能重构

重构,由 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 部分” 中我最终重构到它的惯用模式中的代码。它比原来的代码好多了,因为您可以看到它正在做什么,可从重用部分获益。

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

集体代码所有权

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

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

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

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

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

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

隔离架构更改

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

过度的一般性(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 部分是:“这类东西越少越好。” 尝试延迟架构决策直至最后责任时刻,即使它们有着更大的潜在影响。您会惊奇地发现一些似乎难以改进的事情并不是很糟糕。

结束语

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

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

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


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=643396
ArticleTitle=演化架构和紧急设计: 设计的环境因素,第 2 部分
publish-date=03282011