求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
在X++中使用IoC/DI模式应对不断变化的客户需求
 

2010-06-30 作者:陈晴阳 来源:陈晴阳的blog

 

IoC/DI(Inverse of Control/Dependency Injection,控制反转/依赖注入)模式是一种企业级架构模式,通过将应用程序控制权反转交移给框架,并以构造器注入、属性设置器注入等方式将类实体注入到特定应用层中,最终实现层与层之间的解耦,使得应用程序获得良好的扩展性和应变能力。

客户需求如下:需要向系统中添加两个窗体,Engineers和Analysts,分别显示工程师和分析师的ID、Name和Credit(积分)。在每个窗体右边有一个按钮,该按钮的作用是通过一种计算方式,算出工程师或者分析师的最终积分并显示在弹出窗体上。对于工程师,最终积分=积分(Credit)* 1.1;对于分析师,最终积分=积分(Credit)* 1.4。

劣质的设计

客户需求很简单,稍作分析,我们不难得出,无论从数据表结构还是窗体界面上,Engineers部分和Analysts部分都是非常相近的,只是在最终积分的计算方式上有所不同。很明显,为了具体化这两种算法,我们首先需要将其泛化,然后派生出两个不同算法的具体类:

在上图中,CreditCalculator_General类用于计算工程师的最终积分;CreditCalculator_Special用于计算分析师的最终积分;而CreditCalculator是泛化类。CreditCalculator_General和CreditCalculator_Special分别实现抽象类CreditCalculator的calculate方法,以实现具体的计算方式:


// CreditCalculator_General实现
public class CreditCalculator_General extends CreditCalculator
{
    Engineers       engineers;
}
public void new (Common _common)
{
    engineers = _common;
}
public Amount calculate()
{
    return engineers.Credit * 1.1;
}

// CreditCalculator_Special实现
public class CreditCalculator_Special extends CreditCalculator
{
    Analysts    analysts;
}
public void new (Common _common)
{
    analysts = _common;
}
public Amount calculate()
{
    return analysts.Credit * 1.4;
}

在计算最终积分的窗体的init方法中,使用下面的代码获得具体的算法类,然后在RealEdit中显示计算结果:

public void init()
{
    CreditCalculator calculator;
    ;

    super();

    if (!element.args().caller())
    {
        throw error("@SYS75311");
    }
     // 使用CreditCalculator的construct方法创建实例
    calculator = CreditCalculator::construct(
                element.args().parmEnum(), // 采用何种计算方式
                element.args().record());  // 具体记录
     // 计算并显示结果
     result.realValue(calculator.calculate());
}

其中的construct方法代码如下,参数_method是一个BaseEnum,它有两个元素,用于指定具体的计算方式,该参数在与按钮关联的Display MenuItem上指定,以便于显示结果的窗体在init的时候可以获得具体的参数值:

public static CreditCalculator construct(CalculationMethod _method, Common _common = null)
{
    CreditCalculator    calculator;
    switch (_method)
    {
        case CalculationMethod::General:
            calculator = new CreditCalculator_General(_common);
            break;
        case CalculationMethod::Special:
            calculator = new CreditCalculator_Special(_common);
            break;
        default:
            throw error(strFmt("@SYS22828", funcname()));
    }
    return calculator;
}

很明显,上面的construct方法其实就是一个工厂方法。那么这种设计方式是不是就可以应对不断变化的需求呢?现在我们来分析一下这种设计方式。

上面的设计思想可以使用下面的UML类图进行简要描述:

1、 CreditCalculator_General与数据表Engineers产生聚合(Aggregation)耦合;同理,CreditCalculator_Special与数据表Analysts产生聚合(Aggregation)耦合;在这种情况下,如果客户提出需求更改:Engineers也要采用与Analysts相同的积分计算方式,此时,如果仅仅修改Engineers窗体上按钮的MenuItem,使其采用CreditCalculator_Special的计算方式,那么将会由于Engineers窗体向积分计算窗体传送的数据记录类型(Engineers类型)与Analysts数据记录类型(也就是CreditCalculator_Special类中所必需的数据记录)不匹配而出现异常。

2、 由于两个具体类都分别与其业务相关的数据表产生聚合耦合,这导致CreditCalculator抽象类的construct静态方法也间接的与这两张数据表产生聚合耦合(在上面的UML图中以虚线的聚合关联表示);Engineers数据表和Analysts数据表可以看成是stereotype为table的类,它们是存在于数据表示层的,因此,construct静态方法会与其它两个类产生聚合耦合关联,这违背了面向对象设计中“层与层之间需要解耦”的设计思想。

3、 虽然construct方法采用了switch/case语句提供工厂模式中的工厂方法实现,但是仍然无法应对客户需求变化,例如,如果客户提出另外一个需求:需要增加一种新的计算方式Compound,此时您不得不新建一个继承于CreditCalculator的类:CreditCalculator_Compound,并修改construct方法,添加下面代码:。

case CalculationMethod::Compound:
            calculator = new CreditCalculator_Compound (_common);
            break;

在这种情况下,construct工厂方法根本没有解决设计中存在的问题,事实上,这种采用switch/case语句或者if/else语句实现的工厂模式是一种“伪工厂”模式。系统在发生变更的时候,仍然需要修改大量的代码,当然,您会说X++修改代码很方便,但这并不能作为不使用面向对象思想进行系统分析与设计的借口。

4、 代码需要依赖一个用于指定计算方式的BaseEnum:CalculationMethod,在添加新的计算方式的同时,还需要在BaseEnum中增加元素,代码应需求而变的情况没有得到任何改观。

综上所述,这种设计方式是劣质的,根本无法应对客户需求的变更,因此,我们需要重构,以改进现有设计。

第一次改进

针对上面设计的四个问题,我们对设计进行改进

首先需要解耦具体计算类与数据表实体的耦合关联,也就是让CreditCalculator_General以及CreditCalculator_Special类的具体实现不依赖于Engineers与Analysts数据表,当然,我们可以使用Common来表示一个数据记录,但是它不具备Engineers与Analysts数据表的抽象特性,就好像在.NET Framework中,object类并不具备TextReader与TextWriter的特性一样,这是一种过度泛化。

在常规设计模式中,我们需要定义一个接口,使得Engineers与Analysts数据表都继承于该接口,而在CreditCalculator_General以及CreditCalculator_Special类的具体实现中只对接口进行操作,此时,具体计算类已经与数据表实体实现解耦。然而不幸的是,X++中的数据表实现的是Active Record模式,从表面上看,我们无从定义这个接口。

不幸中的万幸,Data Dictionary下的Map为我们提供了解决方案。在此,我们需要新建一个Map,姑且命名为Staff,该Map只有一个数据字段:Credit(因为在我们的积分计算类中,只需要用到这个字段),在Map中添加两个数据表:Engineers和Analysts,并建立字段关联,使得这两个数据表的Credit字段分别与Map的Credit字段关联,如下图所示:

由此我们可以修改CreditCalculator、CreditCalculator_General、CreditCalculator_Special的代码如下(其它部分的代码暂时不需要变化):

// CreditCalculator的代码
public abstract class CreditCalculator
{
    Staff   staff;
}
public void new(Common _common)
{
    staff = _common;
}
// CreditCalculator_General的代码
public class CreditCalculator_General extends CreditCalculator
{
    // 我们已经不需要定义具体的Engineers数据表实体了
    // Engineers       engineers;
}
public Amount calculate()
{
    return staff.Credit * 1.1;
}
// CreditCalculator_Special的代码
public class CreditCalculator_Special extends CreditCalculator
{
    // 我们已经不需要定义具体的Analysts数据表实体了
    // Analysts    analysts;

}
public Amount calculate()
{
    return staff.Credit * 1.4;
}

至此,无论传递给CreditCalculator_Special类的数据记录是Engineers类型的,还是Analysts类型的,类的calculate方法都可以很好地计算出最终积分。具体的积分计算类已经与数据表实体解耦。这种设计方式可以用下面的UML类图进行描述(点击图片查看全图):

由UML图可以看出,CreditCalculator类已经与Staff表类(stereotype为table的类)产生耦合关联,同时解耦了具体表与其之间的耦合关联。

在此,IoC/DI设计模式的应用已经初见端倪:CreditCalculator类在new的时候,以及General和Special类在使用calculate方法进行积分计算的时候,它们并不知道数据表抽象类Staff(实际是一个Map)具体指代的是Engineers还是Analysts;数据表的具体实例是在CalculateCredit窗体创建General/Special类实例的时候,通过构造函数注入到类中的,这就是依赖注入(DI)的具体体现。

我们再来思考同样一个问题:现在的设计真的可以应对不断变化的客户需求吗?仍然不行!我们忽略了那个“伪工厂”方法和那个Base Enum。换句话说,如果客户需要添加一个新的最终积分计算方法,我们不得不去修改construct方法和这个Base Enum。

第二次改进

我们需要使用控制反转(IoC)及其容器实现来完成设计的第二次改进。所谓控制反转,就是将程序控制权由应用程序反转交给框架,例如支持插件系统的应用程序,应用程序是框架,应用程序的具体行为都在插件中体现,程序控制权在插件手中,Axapta本身就是一个控制反转的实例。为了解决第一次改进中遗留的问题,我们需要引入一种框架,在此我们简单地引入一个IoC容器,由容器来确定系统使用哪个积分计算类来实现积分计算。

1、 模式参与者

a) 配置数据表

配置数据表是对IoC容器配置的描述,一般情况下是一个键值对集合,用于表述在某个特定的环境中,使用哪个类来实现依赖注入,在Spring和Spring.NET框架中表现为XML文件

b) IoC容器

IoC容器用于注册环境特征与类类型的对应关系,并为应用程序提供用于依赖注入的具体实例。在本范例中,我们使用一张数据表来保存配置,因此在系统启动的时候,我们无须进行类型注册

注:上文中提到的其它参与者在此不一一列举

2、 动态特性

a) 用户在Engineers(或者Analysts)窗体上按下“Credit calculation”按钮,由此调用CalculateCredit窗体

b) CalculateCredit窗体调用IoC容器的GetClassFromContainer方法,以便获得具体的计算实例,以便进行最终积分的计算和输出

c) 当CalculateCredit窗体调用IoC容器的GetClassFromContainer方法时,应用程序控制权交给了IoC容器,此时容器会根据调用者的MenuItem名称,通过查询配置数据表来获得对应的类标识(ClassId),进而产生类的实例并返回给调用者

d) 调用者(CalculateCredit窗体)获得类实例后,调用实例的calculate方法计算出最终积分,并显示在窗体上

3、 序列图

序列图如下所示:

4、 UML类图

在采用了这种设计模式以后,我们的代码需要做如下修改:

1、 添加数据表Configuration,其中有两个字段ClassId和MenuItemName

2、 添加IoCContainer类,类中方法定义如下:

public static CreditCalculator GetClassFromContainer(str 30 _menuItemName)
{
    str menuItemStr;
    int classId;
    CreditCalculator calculator;
    ;
    classId = Configuration::find(_menuItemName).ClassId;
    calculator = classFactory.createClass(classId, true);
    return calculator;
}

3、 在抽象类CreditCalculator中添加parmStaff方法如下,同时删去原有的new方法(或者使用默认的new方法):

public Staff parmStaff(Staff _staff = staff)
{
    ;
    staff = _staff;
    return staff;
}

之所以要添加这个方法,是因为我们在使用classFactory创建类实例的时候,没有办法制定构造函数的参数,因此无法使用构造器注入方式来实现依赖注入,我们只能够使用属性设置器注入方式

4、 修改CalculateCredit窗体的init方法如下:

public void init()
{
    CreditCalculator calculator;
    ;

    super();

    if (!element.args().caller())
    {
        throw error("@SYS75311");
    }
    calculator = IoCContainer::GetClassFromContainer(
                 element.args().menuItemName());
    calculator.parmStaff(element.args().record());
    result.realValue(calculator.calculate());
}

通过第二次改进,我们的系统已经可以应对变化的客户需求了。当客户要求Engineers的最终积分计算方式要与Analysts的相同时,我们只要在Engineers窗体的按钮上,将其关联的MenuItem设置为Analysts中对应按钮的MenuItem即可;当客户要求添加一种新的最终积分计算方式时,我们只需要新添加一个继承于CreditCalculator的类,同时添加一个MenuItem,并在配置数据表Configuration中设置两者的关联即可,完全不需要更改现有代码;当客户需要添加并处理一个与Engineers/Analysts结构相同的数据表记录时,我们只需要创建数据表,并将其添加到Map中即可。

由此可见,IoC/DI模式给我们带来了应用程序的可扩展性,它使得我们的应用程序能够应对不断变化的客户需求,这也使我们了解到,要应对客户需求变更,不仅可以在开发模式上下手,同时也应该在系统设计的过程中多下功夫,只有这样,我们才能够真正的打造出具有良好构架和优秀质量的应用程序。

最后补充一点,在本例中,需要引入一个与架构相关但与业务无关的数据表,如果客户在这方面有较高要求或限制的话,比如,不能随便添加与业务无关的对象时,IoC/DI设计模式的使用会受到阻拦,因此我们需要“随需应变”,尽量不与需求相冲突,虽然Axapta本身是一个IoC/DI的具体实例,但它并没有提供IoC/DI的设计框架,还需要设计人员在项目开发过程中多多权衡。



如何向妻子解释OOD
OOAD与UML笔记
UML类图与类的关系详解
UML统一建模语言初学
总结一下领域模型的验证
基于 UML 的业务建模


面向对象的分析设计
基于UML的面向对象分析设计
UML + 嵌入式系统分析设计
关系数据库面向OOAD设计
业务建模与业务架构
使用用例进行需求管理


某航空IT部门 业务分析与业务建模
联想 业务需求分析与建模
北京航管科技 EA工具与架构设计
使用EA和UML进行嵌入式系统分析
全球最大的茶业集团 UML系统分析
华为 基于EA的嵌入式系统建模
水资源服务商 基于EA进行UML建模
更多...