zoukankan      html  css  js  c++  java
  • 在X++中使用IoC/DI模式应对不断变化的客户需求

        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、 序列图

        序列图如下所示(点击查看全图):

    513609432

    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的设计框架,还需要设计人员在项目开发过程中多多权衡。

  • 相关阅读:
    poj 3321 Apple Tree
    hdu 1520 Anniversary party
    Light OJ 1089 Points in Segments (II)
    Timus 1018 Binary Apple Tree
    zoj 3299 Fall the Brick
    HFUT 1287 法默尔的农场
    Codeforces 159C String Manipulation 1.0
    GraphQL + React Apollo + React Hook 大型项目实战(32 个视频)
    使用 TypeScript & mocha & chai 写测试代码实战(17 个视频)
    GraphQL + React Apollo + React Hook + Express + Mongodb 大型前后端分离项目实战之后端(19 个视频)
  • 原文地址:https://www.cnblogs.com/daxnet/p/1686993.html
Copyright © 2011-2022 走看看