你在用什么思想编码:事务脚本 OR 面向对象?
最近在公司内部做技术交流的时候,说起技能提升的问题,调研大家想要培训什么,结果大出我意料,很多人想要培训:面向对象编码。于是我抛出一个问题:你觉得我们现在的代码是面向对象的吗?有人回答:是,有人回答否。我对这个问题的回答是:语法上,是了,但是架构上或者思想上,不是。我们现在的大部分代码,如果要死扣一个名词的话,那就是:事务脚本。
1:最开始的事务脚本
在 Martin Fowler 的书中,存在一个典型的 应用场景,即“收入确认”(Revenue Recognition)。该“收入确认”的描述:
一家软件公司有3种产品,其售价策略分别为,第一种:交全款才能卖给你;第二种,付三分之一,就给你,60天后,再给1/3,90天后给完全部;第三种,付1/3,就给你,30天后给1/3,60天后给完。
但是,关于这个描述,我打算多啰嗦几句,而且个人觉的这个啰嗦非常之紧要,因为它影响到了我们的设计。以下是啰嗦的部分:
“收入确认”,在概念上,确实是产品的入账策略,实际上,Martin 的代码,也是这么去实现的,不同的产品有不同的入账策略。不过,数据库实现,RevenueRecognition 这个表记录的是“产品的某个合同根据产品类型所计算出来的:应该执行的入账日及金额”,即策略是跟着合同走的,而不是跟着产品走的。这很有意思,如果你精读此部分,这种矛盾就会一直纠结在你心头。同时,我们又不得不时刻提醒自己存在的这个需求。
现在,关于这个场景,如果我们理解了 产品 合同 RevenueRecognition 之间的关系,我们就很能理解了数据库是被设计成这样的:
其概念模型为如下:
好了,现在我们来看看什么是事务脚本,对的,就用代码来说话。在原文中, Martin 举了两个例子,但是精读之后,我打算将其颠个倒,把原文中的示例2讲在前头。因为示例2,很好的表达了什么才是作者或者译者眼中的“收入确认”,以及我眼中的“收入策略”。
第一个要实现的功能,即第一个事务脚本描述如下:
根据合同 ID,找到该合同,并根据合同类型得到应该在哪天收入多少钱,并插入数据库。
从该描述中,我们知道,这个脚本最应该发生在签订合同时。因为合同一旦签订,就应该记录什么时候应该收到客户端多少钱。代码如下:
class RecognitionService
{
dynamic dal = null;
// 计算哪天该入账多少并插入
public void CalculateRevenueRecognitions(long contactNumber)
{
DataSet contractDs = dal.FindContract(contactNumber);
double totalRevenue = (double)contractDs.Tables[0].Rows[0]["ID"];
DateTime dateSigned = (DateTime)contractDs.Tables[0].Rows[0]["DateSigned"];
string type = (string)contractDs.Tables[0].Rows[0]["Type"];
if(type == "S") // 电子表格类
{
// the sql "INSERT INTO REVENUECONGNITIONS (CONTRACT,AMOUNT,RECOGNIZEDON) VALUES (?,?,?)"
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(90));
}else if(type == "W") // 文字处理
{
dal.InsertRecognition(contactNumber, totalRevenue, dateSigned);
}else if(type == "D") // 数据库
{
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(30));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
}
}
}
第二个需求是:计算某合同在某个日期前的应该有的入账。
class RecognitionService
{
dynamic dal = null;
// 得到哪天前入账了多少
public double RecognizedRevenue(long contractNumber, DateTime asOf)
{
// the sql "SELECT AMOUNT FROM REVENUECONGNITIONS WHERE CONTRACT=? AND RECOGNIZEDON <=?";
DataSet ds = dal.FindRecognitionsFor(contractNumber, asOf);
double r = 0.0;
foreach(DataRow dr in ds.Tables[0].Rows)
{
r += (double)dr["AMOUNT"];
}
return r;
}
}
从上面的代码,我们可以看出什么才是 事务脚本:
1:采用面向过程的方式组织业务逻辑;
2:没有或尽量少的实体类;
3:一个方法一件事情,故有大量业务类或方法;
4:能与行数据入口和表数据入口很好协作;
2:事务脚本之变体
也许上面的代码多多少少让大家嗤之以鼻,认为现在很少会这样来写代码了。那么,我们来看看下面这段代码:
class RecognitionBll
{
dynamic dal = null;
// 计算哪天该入账多少并插入
public void CalculateRevenueRecognitions(long contactNumber)
{
List<Contact> contracts = dal.FindContract(contactNumber);
double totalRevenue = (double)contracts[0].Id;
DateTime dateSigned = (DateTime)contracts[0].DateSigned;
string type = (string)dal.FindContractType(contactNumber);
// 上面这行代码你还可能会写成
// string type = (string)dal.contracts[0].ProductType;
// 或者
// string type = (string)dal.contracts[0].Product.Type;
if(type == "S") // 电子表格类
{
// the sql "INSERT INTO REVENUECONGNITIONS (CONTRACT,AMOUNT,RECOGNIZEDON) VALUES (?,?,?)"
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(90));
}else if(type == "W") // 文字处理
{
dal.InsertRecognition(contactNumber, totalRevenue, dateSigned);
}else if(type == "D") // 数据库
{
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(30));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
}
}
// 得到哪天前入账了多少
public double RecognizedRevenue(long contractNumber, DateTime asOf)
{
// the sql "SELECT AMOUNT FROM REVENUECONGNITIONS WHERE CONTRACT=? AND RECOGNIZEDON <=?";
List<RevenueRecognition> revenueRecognitions = dal.FindRecognitionsFor(contractNumber, asOf);
double r = 0.0;
foreach(RevenueRecognition rr in revenueRecognitions)
{
r += rr.Amount;
}
return r;
}
}public class Product
{
public long Id;
public string Name;
public string Type;
}public class Contact
{
public long Id;
public long ProductId;
public string ProductType;
public Product Product;
public double Revenue;
public DateTime DateSigned;
}public class RevenueRecognition
{
public long ContactId;
public double Amount;
public double RevenuedOn;
}
在这个事务脚本的变种中,我们看到了所有人写过代码的影子:
1:有了实体类了,所以看上去貌似是面向对象编码了;
2:看到了 “三层架构” 了,即:实体层、DAL层、业务逻辑层等;
但是,它仍旧是 事务脚本 的!唯一不同的是,它光鲜的把 DataSet 变成了 List<Model> 了!
3:什么是面向对象的?
那么,什么是面向对象的编码,面向对象的一个很重要的点就是:“把事情交给最适合的类去做”,并且“你得在一个个业务类之间跳转,才能找出他们如何交互”。这确实是个不那么简单的话题,而本文的主旨也仅在于指出,如果我们的代码中还没有 工作单元 映射 缓存 延迟加载 等等概念,即便我们编码再熟练,也仅仅是在熟练的 面向过程编码。
你在用什么思想编码:事务脚本 OR 面向对象?的更多相关文章
- .NET应用架构设计—表模块模式与事务脚本模式的代码编写
阅读目录: 1.背景介绍 2.简单介绍表模块模式.事务脚本模式 3.正确的编写表模块模式.事务脚本模式的代码 4.总结 1.背景介绍 要想正确的设计系统架构就必须能正确的搞懂每个架构模式的用意,而不是 ...
- 框架计划随笔 三.EntityFramework在传统事务脚本模式下的使用
某个朋友问为什么不推首页或者允许评论,我说一直没怎么写博客,也习惯了先随便乱画再开始写文档,担心公开后一些不经意的"呓语“中得出的错误的结论会给别人错误的观点,所以这个系列只是当做熟悉写博客 ...
- DDD与数据事务脚本
DDD与数据事务脚本 扯淡 相信点进来看这篇文章的同学,大部分是因为标题里面的"DDD"所吸引!DDD并不是一个新技术,如果你百度一下它的历史就会知道,实际上它诞生于2004年, ...
- 多边形节点编码python脚本
# -*- coding: cp936 -*-#本脚以最左边.Y值最大的点为起始点按顺时针为多边形节点编码,生成一个包含记录编码值和多边形FID字段的点要素类 #注意:#1.本脚本作为arcgis脚本 ...
- Oracle 查看 使用 UNDO 段的事务脚本
查看oracle undo segment段的信息: SELECT T1.USN, T2.NAME, T1.STATUS, T1.LATCH, T1.EXTENTS, T1.WRAPS, T1.EXT ...
- java编程思想笔记(一)——面向对象导论
1.1 抽象过程 1.所有编程语言都提供抽象编程机制. 2.人们所能够解决的问题的复杂性直接取决于抽象的类型(所抽象的是什么)和质量. 3."命令式"语言(basic,c等)都是对 ...
- 面向对象架构模式之:领域模型(Domain Model)
一:面向对象设计中最简单的部分与最难的部分 如果说事务脚本是 面向过程 的,那么领域模型就是 面向对象 的.面向对象的一个很重要的点就是:“把事情交给最适合的类去做”,即:“你得在一个个领域类之间跳转 ...
- 领域模型(Domain Model)
领域模型(Domain Model) 一:面向对象设计中最简单的部分与最难的部分 如果说事务脚本是 面向过程 的,那么领域模型就是 面向对象 的.面向对象的一个很重要的点就是:“把事情交给最适合的类去 ...
- python 面向对象设计思想发展史
这篇主要说的是程序设计思想发展历史,分为概述和详细发展历史 一,概述 1940年以前:面向机器 最早的程序设计都是采用机器语言来编写的,直接使用二进制码来表示机器能够识别和执行的 指令和数 据.简单来 ...
随机推荐
- TestLink安装全攻略
TestLink安装全攻略 此文章转自该链接--http://www.cnblogs.com/Tcorner/archive/2011/07/26/2117296.html 安装前准备 需要下载xam ...
- Javascript调用ActiveX示例
Javascript调用ActiveX示例 写一个ActiveX控件比如叫做MyNameSpace.SecreteInfo,安装在客户机器上,这样可以通过c++获取到机器的几乎任何信息. 在网 ...
- 关于UID和GID的创建、修改、删除;简要举例
用户.组和权限 安全3A资源分派 (authentication)认证 (authorization)授权 (accounting)审计 user( 用户) Linux用户:Username/UID ...
- UVa 10055 - Hashmat the Brave Warrior
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=94&page=s ...
- Linux 文件锁
当多个进程同时访问操作同一个文件时,我们怎么保证文件数据的正确性. linux通常采用的方法是文件上锁,来避免共享资源的产生竞争状态. 文件锁包括建议性锁和强制性的锁: 建议性的锁 :顾名思义,相对温 ...
- 使用VisualVM监控远程服务器JVM
VisualVM是JDK自带的一款全能型性能监控和故障分析工具,包括对CPU使用.JVM堆内存消耗.线程.类加载的实时监控,内存dump文件分析,垃圾回收运行情况的可视化分析等,对故障排查和性能调优很 ...
- HTML 透明、阴影,圆角等知识点
table两个属性:cellpadding:内容与单元格边框的距离,内部距离cellspacing:单元格之间的距离,外部距离 table合并边框线: border-collapse: co ...
- js中数组
- DIOCP之数据接收事件
一.不引用编码器与解码器的情况下(ECHO的DEMO) 类TIOCPtcpclient,接收服务器的数据事件:OnRecvBuffer 类TDiocpTcpServer,接收客户端数据事件:OnRec ...
- [python] python 中的" "和' '都是完全转义
dict = {"a" : "apple", "b" : "banana", "g" : " ...