在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、 序列图
序列图如下所示(点击查看全图):
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的设计框架,还需要设计人员在项目开发过程中多多权衡。
在X++中使用IoC/DI模式应对不断变化的客户需求相关推荐
- Vue.js的IoC容器模式探索
IoC概念阐述 IoC(Inversion of Control),意为控制反转,不是什么技术,而是一种设计思想.==Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制==. ...
- 华为轮值CEO徐直军:应对快速变化的世界
在10月15日举办的2017中国管理.全球论坛暨金蝶用户大会,华为轮值CEO徐直军做了题为"应对快速变化的世界"主题演讲. 以下为徐直军演讲全文: 尊敬的来宾.女士们.先生们: 非 ...
- 工厂方法模式与IoC/DI
工厂方法模式与IoC/DI IoC--Inversion of Control 控制反转 DI--Dependency Injection 依赖注入 1:如何理解IoC/DI ...
- 用IDEA详解Spring中的IoC和DI(挺透彻的,点进来看看吧)
用IDEA详解Spring中的IoC和DI 一.Spring IoC的基本概念 控制反转(IoC)是一个比较抽象的概念,它主要用来消减计算机程序的耦合问题,是Spring框架的核心. 依赖注入(DI) ...
- 请简述什么是spring的ioc和di_Spring中的IoC与DI的理解
1.IoC是什么? IoC(Inversion of Control)控制反转,IoC是一种新的Java编程模式,目前很多轻量级容器都在广泛使用的模式. 2.IoC解决了什么问题? 在IoC出现以前, ...
- 请简述什么是spring的ioc和di_理解Spring中的IoC和DI
什么是IoC和DI IoC(Inversion of Control 控制反转):是一种面向对象编程中的一种设计原则,用来减低计算机代码之间的耦合度.其基本思想是:借助于"第三方" ...
- 关于RuoYi中Spring IOC、DI以及MVC不同注解的使用
1.什么是Spring IOC.DI? IOC(inverse of control)即"控制反转",DI(Dependence Injection)即"依赖注入&quo ...
- 手撸Spring系列2:IOC/DI 思想(源码篇-IOC)
说在前头: 笔者本人为大三在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正. ...
- java spring server_Java server框架之(1):spring中的IoC
为什么需要IoC? 一般的对象耦合是在编译时确定的,也就是说当我们写如下类: 1 public classStaticCoupling { 2 3 String s = new String(&quo ...
最新文章
- 如何使错误日志更加方便排查问题
- “{”: 未找到匹配令牌
- python自动测试u_自动化测试——Selenium+Python之下拉菜单的定位
- 11月技术考核:LINUX系统重新安装
- Python IDE 详细攻略,拿来吧你~
- 前端学习(1324):anysc关键字
- ieda ts文件报错_使用TS开发微信小程序(1):环境搭建——VSCode+TS
- HomeWindowsYesPlayMusic – 一个好看的第三方xx云音乐客户端 YesPlayMusic
- 录制完脚本怎么做接口自动化测试_快速构建轻量级接口自动化框架
- [网络安全自学篇] 二十五.Web安全学习路线及木马、病毒和防御初探
- 怎样永久关闭Win10自动更新_win10官网
- 千万千万别裸辞,否则你已经死了
- 360WiFi之愚见
- 如何用科学的方法,保障数据准确性
- 全球与中国无线充电芯片市场深度研究分析报告
- java基础-(六)-使用 Spring Initializr 创建springBoot项目
- appcan中的微信分享与qq分享
- 如何解决不能绘制网络模型,报错protobuf
- 我找到的马春鹏版PRML的错误
- 所见即所得编辑器_Froala所见即所得编辑器
热门文章
- 【MyBatis框架】mybatis和spring整合
- 会动的图解 | 既然IP层会分片,为什么TCP层也还要分段?
- 给大家介绍一下实现Go并发同步原语的基石
- [leetcode-347-Top K Frequent Elements]
- Classifier4J的中文支持
- 数据科学家:21世纪最性感的职业
- 操作系统(14)Linux最常用命令(能解决95%以上的问题)
- 程序员的算法课(8)-贪心算法:理解霍夫曼编码
- linux 背光驱动程序,Linux驱动工程师成长之路 LCD背光控制RT9379B
- 【消息队列之rabbitmq】学习RabbitMQ必备品之一