• 你在用什么思想编码:事务脚本 OR 面向对象?


    最近在公司内部做技术交流的时候,说起技能提升的问题,调研大家想要培训什么,结果大出我意料,很多人想要培训:面向对象编码。于是我抛出一个问题:你觉得我们现在的代码是面向对象的吗?有人回答:是,有人回答否。我对这个问题的回答是:语法上,是了,但是架构上或者思想上,不是。我们现在的大部分代码,如果要死扣一个名词的话,那就是:事务脚本。

    1:最开始的事务脚本

    在 Martin Fowler 的书中,存在一个典型的 应用场景,即“收入确认”(Revenue Recognition)。该“收入确认”的描述:

    一家软件公司有3种产品,其售价策略分别为,第一种:交全款才能卖给你;第二种,付三分之一,就给你,60天后,再给1/3,90天后给完全部;第三种,付1/3,就给你,30天后给1/3,60天后给完。

    但是,关于这个描述,我打算多啰嗦几句,而且个人觉的这个啰嗦非常之紧要,因为它影响到了我们的设计。以下是啰嗦的部分:

    “收入确认”,在概念上,确实是产品的入账策略,实际上,Martin 的代码,也是这么去实现的,不同的产品有不同的入账策略。不过,数据库实现,RevenueRecognition 这个表记录的是“产品的某个合同根据产品类型所计算出来的:应该执行的入账日及金额”,即策略是跟着合同走的,而不是跟着产品走的。这很有意思,如果你精读此部分,这种矛盾就会一直纠结在你心头。同时,我们又不得不时刻提醒自己存在的这个需求。

    现在,关于这个场景,如果我们理解了 产品 合同 RevenueRecognition 之间的关系,我们就很能理解了数据库是被设计成这样的:

    image

    其概念模型为如下:

    image

    好了,现在我们来看看什么是事务脚本,对的,就用代码来说话。在原文中, 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:什么是面向对象的?

    那么,什么是面向对象的编码,面向对象的一个很重要的点就是:“把事情交给最适合的类去做”,并且“你得在一个个业务类之间跳转,才能找出他们如何交互”。这确实是个不那么简单的话题,而本文的主旨也仅在于指出,如果我们的代码中还没有 工作单元 映射 缓存 延迟加载 等等概念,即便我们编码再熟练,也仅仅是在熟练的 面向过程编码。

  • 相关阅读:
    Java实现 LeetCode 421 数组中两个数的最大异或值
    Java实现 LeetCode 421 数组中两个数的最大异或值
    Java实现 LeetCode 421 数组中两个数的最大异或值
    Java实现 LeetCode 420 强密码检验器
    Java实现 LeetCode 420 强密码检验器
    Linux系统Wpa_supplicant用法小结
    wpa_supplicant 和 802.11g WPA 认证的配置
    wpa_supplicant无线网络配置
    LINUX系统中动态链接库的创建与使用{补充}
    LINUX系统中动态链接库的创建与使用
  • 原文地址:https://www.cnblogs.com/luminji/p/3619748.html
Copyright © 2020-2023  润新知