本文内容包括:
控制反转(IoC)模式通常用于组件。本文描述了如何对方法签名使用该模式,以减少组件间的耦合并改善性能。IBM Global Business Services 顾问 André Fachat 用两个例子展示这种方法的灵活性。
控制反转(IoC)和依赖项注入(DI)是两种引起极大关注的模式(参见 参考资料)。它们主要用在所谓的 IoC 容器中,这些容器以其他组件的形式将依赖项注入到一个组件中。然而,这两种模式并未定义这些依赖项组件方法的设计方式。在经典的设计中,这些方法中的值对象或数据传输对象用作方法参数并在需要复杂对象时返回值。
本文向您展示还可以对方法签名使用 IoC,从而使方法与值对象解耦。为此,要把方法签名中的值对象替换成接口。我会介绍该方法的一些应用场景。我经常使用这种模式,并发现借助它可以更好地分离组件之间的关注点。并且在运行时,它能减少对象创建和复制工作。
使用值对象作为方法参数
关于 IoC 已经有很多描述,所以此处只阐述其总体原则:组件将其使用的组件配置、本地化和生命周期方面 “外包” 出去。例如,数据访问 bean 直接从某处 “获取” JDBC 连接并简单地使用该连接,而不是寻找一个 JDBC 数据源(配置和本地化),也许还要自己处理连接池(生命周期)。在 IoC 设置中,这些方面通常由一个 IoC 容器处理,比如,该容器通过调用 setter 方法将这些依赖项注入组件。
IoC 主要处理组件的生命周期。本文并不关注组件,而是关注组件提供的操作的方法参数。
请看下图,这是典型的组件设置的依赖关系图,其中有两个依赖关系并使用了方法参数(参见图 1)。这些方法参数定义成值对象,即不含逻辑只含数据值的对象。
图 1. 使用值对象作为方法参数的组件依赖关系图
在图 1 中,Component1 依赖于 Component2 和 Component3 (根据 IoC),并分别调用 method2 和 method3。如果 Component1 直接 “了解” Component2 或 Component3 或只使用 Component2 和 Component3 实现的接口,那么这与我们的讨论不相关。然而,通常方法参数基本都是对象而不是接口。
在这个设置中,当 Component1 调用 method2 时,它必须实例化 Value Object 2 并为其赋值。同样,如果 Component1 调用 method3,它必须实例化 Value Object 3 并为其赋值。
现在假设 Component1 需要用相同的输入数据调用 method2 和 method3 来获取不同的输出数据。例如,Component1 可以是订单准备组件,method2 可以是决定交货期的方法,method3 可以是决定价格的方法。这两种方法需要相同的输入并提供不同的输出。
在本例中,使用值对象作为方法参数需要 Component1 为每个方法调用创建值对象,并主动地将所需值复制到值对象中。同样,必须实例化这每个值对象,尽管这已经不像在 Java? 早期的版本中那么耗费资源,但仍然需要资源。这些行为都降低了性能。下面几节将介绍如何优化性能。
使用接口改善这种情况
目标是防止在不同的值对象间复制值。为此,可以将方法参数定义为接口。这样,只要对象实现该接口,调用组件就可以将其想使用的任何对象用作方法参数。
图 2 显示了新的依赖关系:
图 2. 使用接口作为方法参数的组件依赖关系图
依赖项方法将方法参数定义成接口。调用组件将实现这些接口的对象(故意不称之为值对象)实例化,并在这两个方法调用中将该对象用作方法参数。
以下例子突出显示了该方法的一些优势。
例子:定价和交货期
再次假设 method1 决定订单的交货期,method2 决定价格。清单 1 是这些组件和方法的简单定义:
清单 1. 使用接口作为方法参数的组件样例
interface LeadtimeComponent { void getLeadtimes(List<LeadtimeItem> items) throws LeadtimeException; } interface LeadtimeItem { Long getArticleId(); BigDecimal getQuantity(); String getQuantityUnit();
void setLeadtimeInDays(Integer leadtime); }
interface PricingComponent { void getPrices(List<PriceItem> items) throws PricingException; } interface PriceItem { Long getArticleId(); BigDecimal getQuantity(); String getQuantityUnit();
void setPrice(BigDecimal price); void setPriceUnit(String currency); } |
请注意这两个接口为检索商品数据的方法定义了极为相似的方法签名:getArticleId()、getQuantity() 和 getQuantityUnit()。还要注意组件方法没有返回值;它们通过对参数对象(即接口)调用 setter 方法来修改提供的 “现成” 对象,以设置价格和交货期。
这个方法简化了管道模式 (参见 参考资料)的实现,在该模式下,数据通过 “管道” 从一个组件输送到下一个组件,管道的一个阶段(组件)使用该管道在前面的步骤中提供的数据。图 3 显示了使用管道模式的一个序列图:
图 3. 使用管道模式准备订单的序列图 在本例中,订单准备过程首先从购物车中读取商品 ID。然后从分类数据库添加更多信息、检索交货期和价格(其中的价格依赖于交货期)并存储购物车中的其他信息,以便使用这些信息决定最终价格。如果读取购物车所返回的条目对象实现了分类、交货期和价格方法所要求的接口,则该过程不需要进行任何复制。
工厂方法
您可能想了解图 3 中的 OrderDB 组件及其 readCart() 方法。确实,这是一个特殊情况。在前面的例子中,依赖项方法(如 getPrices(...))修改过的所有对象均已作为方法参数进行传递。当组件正在从数据库中读取数据时这是不可能的,因为在本例中,购物车中的条目数量在读取前是未知的。
这里的解决方案是为要读取的条目提供一个带工厂方法的方法参数,如清单 2 所示:
清单 2:在方法参数中使用工厂方法
interface OrderDBComponent { void readCart(Cart cart) throws OrderDBException; } interface Cart { Long getCartId();
CartItem newItem(); void addItem(CartItem item); } interface CartItem { void setArticleId(Long articleId); ... } |
借助这个定义,OrderDB 组件从数据库中读取条目,并且对于读取的每个条目,都使用 newItem() 方法从 Cart 对象中获取新的条目对象(CartItem)。填充了从数据库中读取的值后,通过 addItem() 方法把 CartItem 添加到购物车中。请注意,向条目填充值后再将该条目添加到购物车中,能够使购物车总是保持一致。
接口和方法不匹配
这个方法适合以下情况,即多个依赖项组件定义的参数接口是兼容的,从而能够由相同的对象实现。在某些情况下可能出现不兼容,例如两个接口使用不同的返回值类型定义同一个方法。必须小心设计这些方法,才能不引入这类不兼容情况。同样,在设计这些方法时,还应确保当不同的接口有相同的方法签名时,方法参数接口所定义的方法具有相同的语义。
然而即使存在不兼容,所有数据也并未丢失!适配器对象能够将对象转换为实现所需接口的对象,而不必复制涉及到的数据。尽管必须使用该方式来实例化适配器对象,但这还是避免了复制数据值。
另一个例子
还有一个例子能显示此方法的灵活性。我编写了一个针对特定对象模型的编辑器,但希望将模型实现和编辑器实现分离开来。因此,让编辑器为可以编辑的模型定义接口。然后由真实的模型实现来实现清单 3 中的接口:
清单 3. 模型编辑器接口样例
interface ModelEditor { void edit(Model model); } interface Model { ModelElement newElement(); ModelElement addElement(ModelElement element); } |
在这个(相当)精简的定义中,可以看到模型的 addElement() 方法不仅把 ModelElement 当作参数,还返回一个 ModelElement 实例。返回的 ModelElement 是新添加的模型元素所替换的模型元素,如果没有替换任何元素,则为 NULL。然后,将返回值存储到一个撤销命令中,这样就能通过再次调用 addElement() 轻松地恢复该模型。同样,addElement() 方法实现模型一致性检验并拒绝无效更改。
结束语
本文展示了 IoC 的一种具体形式,即应用于组件方法的参数而非组件。使用接口作为方法参数是上下文 IoC 的一种形式(这是 IoC 术语),应用于调用程序的依赖项。就像将依赖项组件(如 PriceComponent)注入到调用程序的组件(如 OrderPrepareComponent)中一样,调用程序组件也将其依赖项对象(方法参数接口的实现)注入到依赖项组件的方法中。由于被调用的组件仅限于在参数接口中定义的方法,所以接口能够确保作为参数提供的对象是一致的。小心地减少功能上所必需的方法的接口,就会降低组件之间的耦合。 |