域对象 & 面向对象 & 结构化编程
程序员文章站
2022-07-03 23:03:49
...
本来我尽量避免关于方法论方面的主义之争,但一些话如骨鲠在喉,不吐不快。
软件领域方法论大师的著作发人深省,通常代表着软件开发的未来模式。当然,我们在读大师之后,掩卷沉思之余,最好也保持自己的独立意见。
希望本文能够唤起一些对基本概念和基本功的重视(追逐新潮概念之余,同时也固本培源)。
1. Domain Object的重新提出的背景
Domain Object并不是一个全新的概念,而是继承以前的纯面向对象开发的思路。
由于当前O/R Mapping, DAO开发结构的层次划分,导致出现了大量的纯粹数据对象。这些数据对象只带有getter, setter属性,而不具有属于自己的方法,起着Data Transfer Object的作用。
Domain Object 则是重新提出并进一步探讨纯面向对象编程的概念:对象不仅应该具有数据,而且应该具有自己的方法。
这个过程和Spring的出现过程很像。
EJB时代之前,大家本来就是采用着轻量编程模型,只是那个时候,轻量编程架构还不成体系。EJB时代中,还坚持轻量编程模型,难免被看作顽固保守。EJB时代之末,轻量编程架构Spring大获成功。
2. Domain Object的划分准则
Domain Object是纯粹的OO对象(这话说起来有些别扭 :-)。
Domain Object的划分就是Object属性和方法的划分。这个划分有没有准则?我的看法是,没有准则。
有这样的说法,面向对象的业务划分,就是根据实际生活中的具体事物进行划分。这可以作为一个大的指导原则,但没有具体的可操作性。
让程序中的Object完全映射实际生活中的Object,是人类的一个伟大理想,是人工智能,是虚拟现实。
下面举个例子。比如,withdraw, deposit两个方法,是放在Account类的里面还是外面?
方案一:
Account代表我的账户,代表我的一个身份,那么当然是一个主动的对象。
Account取钱,存钱是里所当然的。withdraw, deposit两个方法应该是Account的方法。调用方法:
account.withdraw(money)
account.deposit(money)
方案二:
Account就是户头,就是一个被动的金额数据记录。
User每次申请银行管理机构(出纳),AccountManager来操作这个Account。
调用方法:
AccountManager.withdraw(account, money)
AccountManager.deposit(account, money)
3. 面向对象的真正意义 – 多态
面向对象的真正意义并不是为了能够方便的把数据和操作封装起来,映射一个实际业务中的对象。如果是为了这个目的,我们永远找不到一个可操作的准则。
比如,上面的两种划分方法,在实际的类结构设计中都存在,都有一定的道理。而且还存在言之成理的更多的其他划分方法。
面向对象的真正意义,在于处理多态。
上面的两种划分方法,如果只存在一种Account类型的情况下(比如只有银行柜台账户),那么编程上没有根本的区别。只是这个加钱、减钱的动作的位置不同 -- 是在Account里面做,还是在AccountManager里面做?
在有多个Account类型的情况下,那情况就大不一样了。
比如,有电子信用卡远程帐户 ECardAccount,有柜台存折账户PaperAccount。这两种账户的业务规则都是不同的。
比如,电子信用卡远程帐户的取钱,要收取一定比率的手续费,而柜台存折账户就不需要。
这个时候,两种划分方法的编程上的优劣,就体现出来了。
按照第一种划法方法(姑且成为Domain Object法),只要为相同的Account接口,实现两个不同的类,ECardAccount,PaperAccount,分别实现不同的withdraw,deposit就可以了。
第二种方法,就有些麻烦了。需要在AccountManager里面用一个 if else,或者switch来判断Account的类型,是ECard信用卡,还是Paper存折。
没错,多态就是用来消除if else, switch的。把接口从具体实现抽取出来的目的,就是为了实现上的多态。这个例子也很简单,属于所有OOP课本的第一个入门例子的级别。
这里不厌其烦地举出这个基本例子,就是为了说明:如果有多态的需求,那么应该使用Domain Object,如果没有多态的需求,那么随便,怎么样方便痛快,就怎么设计。毕竟,我们追求的最终目标是清晰、明快、简洁的代码,而不是为了符合某种经典结构。
4. 系统分层分包 & 类、包之间的交叉循环引用
我们还是看上面的例子,假设只有一个Account类型。
按照第一种划分方法,withdraw和deposit两个方法都在Account类里面。
假设withdraw方法需要根据金额大小,去查另一个数据表Fee费用表的费率,以便计算手续费。account.withdraw()方法还需要调用FeeDAO的方法,或者由一个代理调用。不管是采用什么方式,account和其他类之间的关系就复杂起来。层次调用关系也复杂起来。account同时是数据对象,也是业务对象。
account的获取和使用过程如下:
Account account = AccountDAO.getAccount(...);// DAO引用了Account
account.withdraw(...);// 其中调用了FeeDAO, Account引用了DAO
假设account处于business层。我们看到,business层和DAO层之间出现了循环关联引用。DAO -> business -> DAO
当然,Account, AccountDAO,FeeDAO都可以是接口。而接口之间的交叉或循环引用,在面向对象设计中,是无可厚非的。比如,著名的Observer模式的Observer和Observable接口之间就是典型的交叉引用。
不过,我的本人习惯是,这种类之间、包之间、Jar之间的交叉循环引用,应该尽量避免。不为别的,就为了所谓的Unit Test,类、包的裙带关系也是越少越好。
我们再来看,按照第二种划分方法的情况,withdraw和deposit两个方法都在AccountManager类里面。
Account属于Data Transfer Object层,AccountManager属于business层。
我们来看Account的获取和使用过程。
Account account = AccountDAO.getAccount(...); // DAO引用account
AccountManager.withdraw(account, ...); //里面调用FeeDAO,
我们看到,business -> DAO -> DTO。层次之间没有交叉循环引用的情况。
5. 面向对象 vs 面向过程
面向对象,还是面向过程,这是个典型的关于方法论的争论话题。
有这样的观点,如果一个程序员从一开始就是用Small Talk这样的纯面向对象语言,而不是从C这样的过程语言转过去,那么就能够建立良好的面向对象思维。
我想,这也许是对的。但这里似乎有一种隐含的意思,好像面向过程的思维习惯是一个根深蒂固的痼疾,是阻碍面向对象方针贯彻的万恶之首。
以至于有这样的趋势,全面否认了面向过程编程的经典设计思想和丰富遗产。
我觉得,这对面向过程编程来说,是不公平的。至少对于C++, Java这种半面向对象语言来说,面向过程编程的基本功也是很重要的。很多情况下,OO用不好的原因,恰恰是因为面向过程编程的基本功不过关。
其实,系统分层这个思路,就是来自于面向过程编程的最基本原则 – 库函数的设计要上层调用下层,层与层之间不能交叉调用依赖。比如,操作系统内核,系统函数库,应用函数库的设计。
基本功这个东西,一点都不酷,一点都不时髦,但这是立身之本。
祝大家新的一年,与时俱进,固本培元。:-)
软件领域方法论大师的著作发人深省,通常代表着软件开发的未来模式。当然,我们在读大师之后,掩卷沉思之余,最好也保持自己的独立意见。
希望本文能够唤起一些对基本概念和基本功的重视(追逐新潮概念之余,同时也固本培源)。
1. Domain Object的重新提出的背景
Domain Object并不是一个全新的概念,而是继承以前的纯面向对象开发的思路。
由于当前O/R Mapping, DAO开发结构的层次划分,导致出现了大量的纯粹数据对象。这些数据对象只带有getter, setter属性,而不具有属于自己的方法,起着Data Transfer Object的作用。
Domain Object 则是重新提出并进一步探讨纯面向对象编程的概念:对象不仅应该具有数据,而且应该具有自己的方法。
这个过程和Spring的出现过程很像。
EJB时代之前,大家本来就是采用着轻量编程模型,只是那个时候,轻量编程架构还不成体系。EJB时代中,还坚持轻量编程模型,难免被看作顽固保守。EJB时代之末,轻量编程架构Spring大获成功。
2. Domain Object的划分准则
Domain Object是纯粹的OO对象(这话说起来有些别扭 :-)。
Domain Object的划分就是Object属性和方法的划分。这个划分有没有准则?我的看法是,没有准则。
有这样的说法,面向对象的业务划分,就是根据实际生活中的具体事物进行划分。这可以作为一个大的指导原则,但没有具体的可操作性。
让程序中的Object完全映射实际生活中的Object,是人类的一个伟大理想,是人工智能,是虚拟现实。
下面举个例子。比如,withdraw, deposit两个方法,是放在Account类的里面还是外面?
方案一:
Account代表我的账户,代表我的一个身份,那么当然是一个主动的对象。
Account取钱,存钱是里所当然的。withdraw, deposit两个方法应该是Account的方法。调用方法:
account.withdraw(money)
account.deposit(money)
方案二:
Account就是户头,就是一个被动的金额数据记录。
User每次申请银行管理机构(出纳),AccountManager来操作这个Account。
调用方法:
AccountManager.withdraw(account, money)
AccountManager.deposit(account, money)
3. 面向对象的真正意义 – 多态
面向对象的真正意义并不是为了能够方便的把数据和操作封装起来,映射一个实际业务中的对象。如果是为了这个目的,我们永远找不到一个可操作的准则。
比如,上面的两种划分方法,在实际的类结构设计中都存在,都有一定的道理。而且还存在言之成理的更多的其他划分方法。
面向对象的真正意义,在于处理多态。
上面的两种划分方法,如果只存在一种Account类型的情况下(比如只有银行柜台账户),那么编程上没有根本的区别。只是这个加钱、减钱的动作的位置不同 -- 是在Account里面做,还是在AccountManager里面做?
在有多个Account类型的情况下,那情况就大不一样了。
比如,有电子信用卡远程帐户 ECardAccount,有柜台存折账户PaperAccount。这两种账户的业务规则都是不同的。
比如,电子信用卡远程帐户的取钱,要收取一定比率的手续费,而柜台存折账户就不需要。
这个时候,两种划分方法的编程上的优劣,就体现出来了。
按照第一种划法方法(姑且成为Domain Object法),只要为相同的Account接口,实现两个不同的类,ECardAccount,PaperAccount,分别实现不同的withdraw,deposit就可以了。
第二种方法,就有些麻烦了。需要在AccountManager里面用一个 if else,或者switch来判断Account的类型,是ECard信用卡,还是Paper存折。
没错,多态就是用来消除if else, switch的。把接口从具体实现抽取出来的目的,就是为了实现上的多态。这个例子也很简单,属于所有OOP课本的第一个入门例子的级别。
这里不厌其烦地举出这个基本例子,就是为了说明:如果有多态的需求,那么应该使用Domain Object,如果没有多态的需求,那么随便,怎么样方便痛快,就怎么设计。毕竟,我们追求的最终目标是清晰、明快、简洁的代码,而不是为了符合某种经典结构。
4. 系统分层分包 & 类、包之间的交叉循环引用
我们还是看上面的例子,假设只有一个Account类型。
按照第一种划分方法,withdraw和deposit两个方法都在Account类里面。
假设withdraw方法需要根据金额大小,去查另一个数据表Fee费用表的费率,以便计算手续费。account.withdraw()方法还需要调用FeeDAO的方法,或者由一个代理调用。不管是采用什么方式,account和其他类之间的关系就复杂起来。层次调用关系也复杂起来。account同时是数据对象,也是业务对象。
account的获取和使用过程如下:
Account account = AccountDAO.getAccount(...);// DAO引用了Account
account.withdraw(...);// 其中调用了FeeDAO, Account引用了DAO
假设account处于business层。我们看到,business层和DAO层之间出现了循环关联引用。DAO -> business -> DAO
当然,Account, AccountDAO,FeeDAO都可以是接口。而接口之间的交叉或循环引用,在面向对象设计中,是无可厚非的。比如,著名的Observer模式的Observer和Observable接口之间就是典型的交叉引用。
不过,我的本人习惯是,这种类之间、包之间、Jar之间的交叉循环引用,应该尽量避免。不为别的,就为了所谓的Unit Test,类、包的裙带关系也是越少越好。
我们再来看,按照第二种划分方法的情况,withdraw和deposit两个方法都在AccountManager类里面。
Account属于Data Transfer Object层,AccountManager属于business层。
我们来看Account的获取和使用过程。
Account account = AccountDAO.getAccount(...); // DAO引用account
AccountManager.withdraw(account, ...); //里面调用FeeDAO,
我们看到,business -> DAO -> DTO。层次之间没有交叉循环引用的情况。
5. 面向对象 vs 面向过程
面向对象,还是面向过程,这是个典型的关于方法论的争论话题。
有这样的观点,如果一个程序员从一开始就是用Small Talk这样的纯面向对象语言,而不是从C这样的过程语言转过去,那么就能够建立良好的面向对象思维。
我想,这也许是对的。但这里似乎有一种隐含的意思,好像面向过程的思维习惯是一个根深蒂固的痼疾,是阻碍面向对象方针贯彻的万恶之首。
以至于有这样的趋势,全面否认了面向过程编程的经典设计思想和丰富遗产。
我觉得,这对面向过程编程来说,是不公平的。至少对于C++, Java这种半面向对象语言来说,面向过程编程的基本功也是很重要的。很多情况下,OO用不好的原因,恰恰是因为面向过程编程的基本功不过关。
其实,系统分层这个思路,就是来自于面向过程编程的最基本原则 – 库函数的设计要上层调用下层,层与层之间不能交叉调用依赖。比如,操作系统内核,系统函数库,应用函数库的设计。
基本功这个东西,一点都不酷,一点都不时髦,但这是立身之本。
祝大家新的一年,与时俱进,固本培元。:-)
上一篇: 生产者-消费者模式实现
下一篇: 宇文邕是如何稳固政权的?他的死因是什么?