Introduce Null Object(引入Null对象)

Introduce Null Object(引入Null对象)

你需要再三检查某对象是否为null。

将null值替换为null对象。

  1. if (customer == null) plan = BillingPlan.basic();  
  2. else plan = customer.getPlan(); 
 
 

动机

多态的最根本好处在于:你不必再向对象询问"你是什么类型"而后根据得到的答案调用对象的某个行为--你只管调用该行为就是了,其他的一切多态机制会为你安排妥当。当某个字段内容是null 时,多态可扮演另一个较不直观(亦较不为人所知)的用途。让我们先听听Ron Jeffries的故事。

--Ron Jeffries

我们第一次使用Null Object模式,是因为Rich Garzaniti发现,系统在向对象发送一个消息之前,总要检查对象是否存在,这样的检查出现很多次。我们可能会向一个对象索求它所相关的Person对象,然后再问那个对象是否为null。如果对象的确存在,我们才能调用它的rate()函数以查询这个人的薪资级别。我们在好些地方都是这样做的,造成的重复代码让我们很烦心。

所以,我们编写了一个MissingPerson类,让它返回'0'薪资等级[我们也把空对象(null object)称为虚拟对象(missing object)]。很快地,MissingPerson就有了很多函数,rate()自然是其中之一。如今我们的系统有超过80个空对象类。

我们常常在显示信息的时候使用空对象。例如我们想要显示一个Person对象信息,它大约有20个实例变量。如果这些变量可被设为null,那么打印一个Person对象的工作将非常复杂。所以我们不让实例变量被设为null,而是插入各式各样的空对象--它们都知道如何正确地显示自己。这样,我们就可以摆脱大量过程化的代码。

我们对空对象的最聪明运用,就是拿它来表示不存在的Gemstone会话:我们使用Gemstone数据库来保存成品(程序代码),但我们更愿意在没有数据库的情况下进行开发,每过一周左右再把新代码放进Gemstone数据库。然而在代码的某些地方,我们必须登录一个Gemstone会话。当没有Gemstone数据库时,我们就仅仅安插一个"虚构的Gemstone会话",其接口和真正的Gemstone会话一模一样,使我们无需判断数据库是否存在,就可以进行开发和测试。

空对象的另一个用途是表现出"虚构的箱仓"(missing bin)。所谓"箱仓",这里是指集合,用来保存某些薪资值,并常常需要对各个薪资值进行加和或遍历。如果某个箱仓不存在,我们就给出一个虚构的箱仓对象,其行为和一个空箱仓一样。这个虚构箱仓知道自己其实不带任何数据,总值为0。通过这种做法,我们就不必为上千位员工每人产生数十来个空箱对象了。

使用空对象时有个非常有趣的性质:系统几乎从来不会因为空对象而被破坏。由于空对象对所有外界请求的响应都和真实对象一样,所以系统行为总是正常的。但这并非总是好事,有时会造成问题的侦测和查找上的困难,因为从来没有任何东西被破坏。当然,只要认真检查一下,你就会发现空对象有时出现在不该出现的地方。

请记住:空对象一定是常量,它们的任何成分都不会发生变化。因此我们可以使用Singleton模式[Gang of Four]来实现它们。例如不管任何时候,只要你索求一个MissingPerson对象,得到的一定是MissingPerson 的唯一实例。

关于Null Object模式,你可以在Woolf [Woolf]中找到更详细的介绍。

做法

为源类建立一个子类,使其行为就像是源类的null版本。在源类和null子类中都加上isNull()函数,前者的isNull()应该返回false,后者的isNull()应该返回true。

下面这个办法也可能对你有所帮助:建立一个nullable接口,将isNull()函数放在其中,让源类实现这个接口。

另外,你也可以创建一个测试接口,专门用来检查对象是否为null。

编译。

找出所有"索求源对象却获得一个null"的地方。修改这些地方,使它们改而获得一个空对象。

找出所有"将源对象与null做比较"的地方。修改这些地方,使它们调用isNull()函数。

你可以每次只处理一个源对象及其客户程序,编译并测试后,再处理另一个源对象。

你可以在"不该再出现null"的地方放上一些断言,确保null的确不再出现。这可能对你有所帮助。

编译,测试。

找出这样的程序点:如果对象不是null,做A动作,否则做B动作。

对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同。

使用上述被覆写的动作,然后删除"对象是否等于null"的条件测试。编译并测试。

范例

一家公用事业公司的系统以Site表示地点(场所)。庭院宅第(house)和集体公寓(apartment)都使用该公司的服务。任何时候每个地点都拥有(或说都对应于)一个顾客,顾客信息以Customer表示:

  1. class Site...  
  2.   Customer getCustomer() {  
  3.       return _customer;   
  4.   }  
  5.   Customer _customer; 

Customer有很多特性,我们只看其中三项:

  1. class Customer...  
  2.   public String getName() {...}  
  3.   public BillingPlan getPlan() {...}  
  4.   public PaymentHistory getHistory() {...} 

本系统又以PaymentHistory表示顾客的付款记录,它也有其自己的特性:

  1. public class PaymentHistory...  
  2.   int getWeeksDelinquentInLastYear() 

上面的各种取值函数允许客户取得各种数据。但有时候一个地点的顾客搬走了,新顾客还没搬进来,此时这个地点就没有顾客。由于这种情况有可能发生,所以我们必须保证Customer的所有用户都能够处理"Customer对象等于null"的情况。下面是一些示例片段:

  1.     Customer customer = site.getCustomer();  
  2.     BillingPlan plan;  
  3.     if (customer == null) plan = BillingPlan.basic();  
  4.     else plan = customer.getPlan();  
  5. ...  
  6.     String customerName;  
  7.     if (customer == null) customerName = "occupant";  
  8.     else customerName = customer.getName();  
  9. ...  
  10.     int weeksDelinquent;  
  11.     if (customer == null) weeksDelinquent = 0;  
  12.     else weeksDelinquent = customer.getHistory().  
  13.     getWeeksDelinquentInLastYear(); 

这个系统中可能有许多地方使用Site和Customer对象,它们都必须检查Customer对象是否等于null,而这样的检查完全是重复的。看来是使用空对象的时候了。

首先新建一个NullCustomer,并修改Customer,使其支持"对象是否为null"的检查:

  1. class NullCustomer extends Customer {  
  2.   public boolean isNull() {  
  3.       return true;  
  4.   }  
  5. }  
  6.  
  7. class Customer...  
  8.   public boolean isNull() {  
  9.       return false;  
  10.   }  
  11.  
  12. protected Customer() {} //needed by the NullCustomer 

如果你无法修改Customer,可以使用第266页的做法:建立一个新的测试接口。

如果你喜欢,也可以新建一个接口,昭告大家"这里使用了空对象":

  1. interface Nullable {  
  2.   boolean isNull();  
  3. }  
  4.  
  5. class Customer implements Nullable 

我还喜欢加入一个工厂函数,专门用来创建NullCtomer对象。这样一来,用户就不必知道空对象的存在了:

  1. class Customer...  
  2.   static Customer newNull() {  
  3.       return new NullCustomer();  
  4.   } 

接下来的部分稍微有点麻烦。对于所有"返回null"的地方,我都要将它改为"返回空对象"。此外,我还要把foo==null这样的检查替换成foo.isNull()。我发现下列办法很有用:查找所有提供Customer对象的地方,将它们都加以修改,使它们不能返回null,改而返回一个NullCustomer对象。

  1. class Site...  
  2.   Customer getCustomer() {  
  3.       return (_customer == null) ?  
  4.           Customer.newNull():  
  5.           _customer;  
  6.   } 

另外,我还要修改所有使用Customer对象的地方,让它们以isNull()函数进行检查,不再使用==null检查方式。

  1.     Customer customer = site.getCustomer();  
  2.     BillingPlan plan;  
  3.     if (customer.isNull()) plan = BillingPlan.basic();  
  4.     else plan = customer.getPlan();  
  5. ...  
  6.     String customerName;  
  7.     if (customer.isNull()) customerName = "occupant";  
  8.     else customerName = customer.getName();  
  9. ...  
  10.     int weeksDelinquent;  
  11.     if (customer.isNull()) weeksDelinquent = 0;  
  12.     else weeksDelinquent = customer.getHistory().  
  13.     getWeeksDelinquentInLastYear(); 

毫无疑问,这是本项重构中最需要技巧的部分。对于每一个需要替换的可能等于null的对象,我都必须找到所有检查它是否等于null的地方,并逐一替换。如果这个对象被传播到很多地方,追踪起来就很困难。上述范例中,我必须找出每一个类型为Customer的变量,以及它们被使用的地点。很难将这个过程分成更小的步骤。有时候我发现可能等于null的对象只在某几处被用到,那么替换工作比较简单;但是大多数时候我必须做大量替换工作。还好,撤销这些替换并不困难,因为我可以不太困难地找出对isNull()的调用动作,但这毕竟也是很零乱很恼人的。

这个步骤完成之后,如果编译和测试都顺利通过,我就可以宽心地露出笑容了。接下来的动作比较有趣。到目前为止,使用isNull()函数尚未带来任何好处。只有把相关行为移到NullCustomer中并去除条件表达式之后,我才能得到切实的利益。我可以逐一将各种行为移过去。首先从"取得顾客名称"这个函数开始。此时的客户端代码大约如下:

  1. String customerName;  
  2. if (customer.isNull()) customerName = "occupant";  
  3. else customerName = customer.getName(); 

首先为NullCustomer加入一个合适的函数,通过这个函数来取得顾客名称:

  1. class NullCustomer...  
  2.   public String getName(){  
  3.       return "occupant";  
  4.   }  
  5.  
  6. 现在,我可以去掉条件代码了:  
  7.  
  8. String customerName = customer.getName(); 

接下来我以相同手法处理其他函数,使它们对相应查询做出合适的响应。此外我还可以对修改函数做适当的处理。于是下面这样的客户端程序:

  1. if (! customer.isNull())  
  2.     customer.setPlan(BillingPlan.special());  
  3.  
  4. 就变成了这样:  
  5.  
  6. customer.setPlan(BillingPlan.special());  
  7.  
  8. class NullCustomer...  
  9.   public void setPlan (BillingPlan arg) {} 

请记住:只有当大多数客户代码都要求空对象做出相同响应时,这样的行为搬移才有意义。注意,我说的是"大多数"而不是"所有"。任何用户如果需要空对象做出不同响应,他们仍然可以使用isNull()函数来测试。只要大多数客户端都要求空对象做出相同响应,他们就可以调用默认的null行为,而你也就受益匪浅了。

上述范例略带差异的某种情况是,某些客户端使用Customer函数的运算结果:

  1. if (customer.isNull()) weeksDelinquent = 0;  
  2. else weeksDelinquent = customer.getHistory().  
  3. getWeeksDelinquentInLastYear();  
  4.  
  5. 我可以新建一个NullPaymentHistory类,用以处理这种情况:  
  6.  
  7. class NullPaymentHistory extends PaymentHistory...  
  8.   int getWeeksDelinquentInLastYear() {  
  9.       return 0;  
  10.   }  
  11.  
  12. 并修改NullCustomer,让它返回一个NullPaymentHistory对象:  
  13.  
  14. class NullCustomer...  
  15.   public PaymentHistory getHistory() {  
  16.     return PaymentHistory.newNull();  
  17.   }  
  18. 然后,我同样可以删除这一行条件代码:  
  19.  
  20. int weeksDelinquent = customer.getHistory().  
  21. getWeeksDelinquentInLastYear(); 

你常常可以看到这样的情况:空对象会返回其他空对象。

范例:测试接口

除了定义isNull()之外,你也可以建立一个用以检查"对象是否为null"的接口。

使用这种办法,需要新建一个Null接口,其中不定义任何函数:

  1. interface Null {}  
  2.  
  3. 然后,让空对象实现Null接口:  
  4.  
  5. class NullCustomer extends Customer implements Null...  
  6.  
  7. 然后,我就可以用instanceof操作符检查对象是否为null:  
  8.  
  9. aCustomer instanceof Null 

通常我尽量避免使用instanceof操作符,但在这种情况下,使用它是没问题的。而且这种做法还有另一个好处:不需要修改Customer。这么一来即使无法修改Customer源码,我也可以使用空对象。

其他特殊情况

使用本项重构时,你可以有几种不同的空对象,例如你可以说"没有顾客"(新建的房子和暂时没人住的房子)和"不知名顾客"(有人住,但我们不知道是谁)这两种情况是不同的。果真如此,你可以针对不同的情况建立不同的空对象类。有时候空对象也可以携带数据,例如不知名顾客的使用记录等,于是我们可以在查出顾客姓名之后将账单寄给他。

本质上来说,这是一个比Null Object模式更大的模式:Special Case模式。所谓特例类(special case),也就是某个类的特殊情况,有着特殊的行为。因此表示"不知名顾客"的UnknownCustomer和表示"没有顾客"的NoCustomer都是Customer的特例。你经常可以在表示数量的类中看到这样的"特例类",例如Java浮点数有"正无穷大"、"负无穷大"和"非数量"(NaN)等特例。特例类的价值是:它们可以降低你的"错误处理"开销,例如浮点运算决不会抛出异常。如果你对NaN做浮点运算,结果也会是个NaN。这和"空对象的访问函数通常返回另一个空对象"是一样的道理。

转载于:https://www.cnblogs.com/chenying99/archive/2012/04/21/2462481.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/576496.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

zookeeper Error contacting service. It is probably not running

通过命令zkServer.sh start之后,显示STARTED 通过client测试报错 无法连接 连接关闭 Connecting to localhost:2181 2020-03-29 12:44:31,855 [myid:] - INFO [main:Environment100] - Client environment:zookeeper.version3.4.13-2d71af4dbe22557fda74f9a9b430…

引言

推荐系统从0到1_引言什么是推荐?什么是推荐系统?推荐系统的应用什么是推荐? 说起推荐,就不得不说搜索。搜索这里指信息检索,在大量的信息中,我们需要找到自己需要的信息,就用到了搜索引擎&…

JNI的方式调用DLL(SO)(上)

最近有个项目要调用原有的DLL文件,本来准备使用JNA,可是客户不同意,非要使用JNI的方式。万般无奈之下,只能重新使用JNI,现将JAVA调用动态链接库的一般方法总结如下。 JNI是Java Native Interface(JAVA本地调…

搭建推荐系统所需要的材料

搭建推荐系统所需要的材料人力物力推荐系统的原材料物品流量最后人力物力 在上一小节中《推荐系统的必要性》里已经讨论过一个推荐业务团队需要哪些人员储备,需要什么核心技术等。这里在重点说下物力,即机器资源设备。 往往从头开始搭建这样一个团队&a…

kafka:topic为什么要进行分区?副本机制是如何做的?

kafka为什么要在topic里加入分区的概念?如果没有分区,topic中的segment消息写满后,直接给订阅者不是也可以吗? Kafka可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个分区中,只要如…

将某字符串切割成阵列并排序列出

老师布置的另外一道题是将字符串"a;b;d;z;y;u"切割成阵列并排序列出。老师出这题也许是让Insus.NET掌握或复习Array.Sort()函数,不管怎样,先按自己的想法实现,然后是等老师的意见。protected void Page_Load(object sender, EventA…

关于管理,这5大误区你越早知道越好

关于管理,这5大误区你越早知道越好 原创: 陈春花 管理的常识 绝大部分人都感觉自己已经非常努力地工作,但结果却不尽如人意,到底问题出在哪里? 就让北京大学国家发展研究院管理学教授、北大国发院BiMBA商学院院长陈春…

大数据技术之 Kafka (第 3 章 Kafka 架构深入 ) Log存储解析

Kafka 工作流程 Kafka 中消息是以 topic 进行分类的,生产者生产消息,消费者消费消息,都是面向 topic的。 topic 是逻辑上的概念,而 partition 是物理上的概念,每个 partition 对应于一个 log 文件,该 log…

推荐系统整体框架概览

推荐系统整体框架概览推荐系统整体架构 推荐系统的核心组成部分离线核心节点服务UI总结推荐系统整体架构 先说点题外话,最近在看的书中讲到了怎么进行自学的方法,分了十个层级。第一个便是要了解所学内容的概况,也就是轮廓,大的东…

大数据技术之 Kafka (第 3 章 Kafka 架构深入 ) Kafka 生产者

3.2.1 分区策略 1)分区的原因 (1)方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的数据了; &a…

MySQL定时备份实现

一、备份数据库 –all-databases 备份所有数据库 /opt/mysqlcopy/all_$(date “%Y-%m-%d %H:%M:%S”).sql 备份地址 docker exec -it 容器名称 sh -c "mysqldump -u root -ppassword --all-databases > /opt/mysqlcopy/all_$(date "%Y-%m-%d %H:%M:%S").sq…

程序员健身的重要性

程序员健身的重要性人的身体就是人的灵魂最好的写照! --- 路德维系.维特根斯坦 健身不仅是保持健康体魄的关键要素之一,也是灵活的、具有创造性的脑力活动的基础。 --- 约翰.肯尼迪 …

Java 8 中的哈希表

JDK 的代码是开源的,我们打开idea开发工具,引入jdk1.8 找到hashmap HashMap 是基于 HashTable 的一种数据结构,在普通哈希表的基础上,它支持多线程操作以及空的 key 和 value。 在 HashMap 中定义了几个常量: static final in…

NSJSONSerialization介绍

ios5中apple增加了解析JSON的api——NSJSONSerialization。网上已经有人做过测试,NSJSONSerialization在效率上完胜SBJSON、TouchJSON、YAJL、JSONKit、NextiveJson。详情见这里。既然apple为我们提供了这么良好的工具,我们没理由不用吧。 NSJSONSeriali…

[转帖]ISE与Modelsim联合观察中间信号

如何仿真IP核(建立modelsim仿真库完整解析) 来源:http://www.ednchina.com/ART_49023_19_0_AN_7116cf44.HTM IP核生成文件:(Xilinx/Altera 同) IP核生成器生成 ip 后有两个文件对我们比较有用,假…

sql server 2008学习12 事务和锁

事务 事务的点: 1.begin tran 是事务开始的地方,也是 事务回滚的起点.也就说他会忽略这个起点之后的最终没有提交的所有语句, 2.commit tran 事务的提交 是一个事务的终点 当发出 commit tran命令时,可以认为 该事务是 持久的. 撤销已完成事务的唯一方法 是 发出一个新的事务.…

漫画:什么是时间复杂度?

时间复杂度的意义 究竟什么是时间复杂度呢?让我们来想象一个场景:某一天,小灰和大黄同时加入了一个公司...... 一天过后,小灰和大黄各自交付了代码,两端代码实现的功能都差不多。大黄的代码运行一次要花100毫秒&#x…

NLP 最新进展

参考文献: •http://www.dataguru.cn/article-14237-1.html •https://zhuanlan.zhihu.com/p/46652512 •https://github.com/google-research/bert •https://allennlp.org/elmo •https://arxiv.org/pdf/1802.05365 •https://arxiv.org/abs/1810.04805

Big GAN

参考文献: •https://github.com/kayamin/DR-GAN •https://www.jianshu.com/p/4ee8f9284b81 •http://cvlab.cse.msu.edu/pdfs/Tran_Yin_Liu_CVPR2017.pdf •https://arxiv.org/abs/1809.11096 •https://juejin.im/entry/5c05e76c51882539c60cf2d5 •https:…

什么样的知识,值得我们终生学习

#什么样的知识,值得我们终生学习? 原文:https://dwz.cn/vylyXXJi 一、引言 可能你从初中就开始抱怨:我学相似三角形能干什么?阿伏伽德罗常数有什么用?我一不跳楼,二不高空抛物,学自由…