使用Null Object设计模式时没注意到这一点就相当于白用
在各种项目中,我们可能会反复看到类似下面这样的代码
c
returnReference = call_a_function()
if returnReference == null {
// 如果函数返回的引用/指针为null,表示需要获取的对象/值不存在
// 执行处理异常情况的逻辑
} else {
// 需要的对象/值存在
// 执行处理正常情况的主逻辑
returnReference.doSomething()
}
也就是说,我们往往必须检查函数或方法的返回值,先确保其不为Null再调用上面的方法(向其发送消息)。这是因为对Null调用方法通常会报错(是的,存在不会报错的特殊情况)。当然,对于函数的参数,局部或全局变量等也需要相同的检查。
注:本文用首字母大写的Null泛指各种语言中的空指针或空引用;用代码体的
null
(PHP、Java)或nil
(Go)特指对应语言中的空指针或空引用。
对Null调用方法有可能不报错吗?试试文末那段Go代码吧^_^
为了防止这种判断是否为Null的if else
在项目中遍地开花,前辈程序员们发明了Null Object这一设计模式。该模式是用称为null object(空对象)的特殊对象来取代Null ,改用这种仅带有空值和空方法的对象来表示不存在、未知、无意义等异常数据。
如图所示,用null object取代Null就能消除大量if else
,使程序员聚焦于主逻辑,提升程序的可读性。
这样说可能太过抽象,我们还是先通过一个具体例子来体会一下Null Object设计模式的好处。
示例代码
假设我们要编写一个给小朋友使用的能播放各种动物叫声的程序。
我们先用PHP来编写这段程序。
php
interface Animal {
public function makeSound();
}
class Dog implements Animal {
public function makeSound() {
echo "WOOF\n";
}
}
class Cat implements Animal {
// ...
function makeAnimalFromAnimalType(string $animalType): Animal {
switch ($animalType) {
case 'dog':
return new Dog();
case 'cat':
return new Cat();
// ...
}
}
makeAnimalFromAnimalType('dog')->makeSound();
我们这里用输出语句(echo
语句)代替播放动物的叫声。如果小朋友想听小狗叫,这个程序就会通过makeAnimalFromAnimalType('dog')
创建出Dog
类的对象,然后调用上面的makeSound()
方法,这样就会"听"到"汪汪汪"(WOOF)的叫声。小猫喵喵叫也是同样的逻辑。
但如果小朋友想知道小兔子怎么叫呢?
我们没有定义Rabbit
类,而且也不知道兔子怎么叫。就算问了专家,知道了兔子的叫声,那狐狸怎么叫呢?小朋友总能想出叫声未知的动物。
这时Null Object模式就派上用场了。
首先我们定义出一个NullAnimal
类①,并实现了makeSound()
方法,只不过方法里什么也没有做。
php
class NullAnimal implements Animal { // ①
public function makeSound() {
// silence...
}
}
function makeAnimalFromAnimalType(string $animalType): Animal {
switch ($animalType) {
case 'dog':
return new Dog();
case 'cat':
return new Cat();
default:
return new NullAnimal(); // ②
}
}
$animalType = 'rabbit';
makeAnimalFromAnimalType($animalType)->makeSound(); // ③ ..the null animal makes no sound
然后我们在函数makeAnimalFromAnimalType()
中为所有叫声未知的动物统统返回new NullAnimal()
②。
只需这两步,我们就可以放心对makeAnimalFromAnimalType()
的返回值调用makeSound()
了。因为该函数不会返回null
③,也就防止后续代码对null
调用makeSound()
了。这就意味着无须再通过if makeAnimalFromAnimalType($animalType) == null
进行判断了。
Java、Go等语言的代码与此大同小异,这里就不一一列举了。
怎么样,这个设计模式很简单吧。
需要特别注意的点在哪里
Null Object模式看似简单,但正如本文标题说的,里面有一个特别需要注意的点,一旦没处理好就等于前功尽弃。
在揭晓答案之前,我们再来重点看一看这个根据动物的名称返回动物类实例的makeAnimalFromAnimalType()
的函数。
那么问题来了,如果把default
分支返回的null object NullAnimal
改为null
或nil
,会怎么样呢?
接下来会以PHP、Java和Go这三种主流语言为例。首先从PHP的代码看起。
php
<?php
function makeAnimalFromAnimalType(string $animalType): Animal {
switch ($animalType) {
case 'dog':
return new Dog();
case 'cat':
return new Cat();
default:
return null; // <--
}
}
makeAnimalFromAnimalType("rabbit");
// Fatal error: Uncaught TypeError: makeAnimalFromAnimalType(): Return value must be of type Animal, null returned in ...
报错了!(有点意外吧)因为makeAnimalFromAnimalType()
之后的类型提示: Animal
强制要求该函数返回一个类型为Animal
的值,而null
不是。
再来看看Java中的情况。
java
interface Animal {
void makeSound();
}
class Dog implements Animal {
// ..
class NullAnimal implements Animal {
// ..
public class AnimalSound {
public static Animal makeAnimalFromAnimalType(String animalType) {
switch (animalType) {
case "dog":
return new Dog();
default:
return null; // <--
}
}
public static void main(String[] args) {
Animal rabbit = makeAnimalFromAnimalType("rabbit"); // ①
rabbit.makeSound(); // ②
}
}
// Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Animal.makeSound()" because "<local1>" is null at AnimalSound.main(...
可以看到,不同于PHP,在Java中,makeAnimalFromAnimalType()
可以返回null
①,但是在null
上调用makeSound()
会抛出空指针异常②。
关于一个声明返回类型为T
的方法能否返回null
这个问题,在2015年于上海举办的PHP大会上,一位程序媛小姐姐还问过PHP大神鸟哥,鸟哥当时感叹道:人家这是认真听讲了。这一幕的视频在🎬www.bilibili.com/video/BV1v6...
最后再来看看Go语言的makeAnimalFromAnimalType()
能否返回nil
。
go
type Animal interface {
makeSound()
}
func makeAnimalFromAnimalTypes(animalType string) Animal {
switch animalType {
case "dog":
return &Dog{}
default:
return nil
}
}
func main() {
rabbit := makeAnimalFromAnimalTypes("rabbit")
rabbit.makeSound()
}
// panic: runtime error: invalid memory address or nil pointer dereference
Go和Java一样,makeAnimalFromAnimalTypes()
可以返回nil
,但是不能在它上面调用makeSound()
。
绕了这么半天,一会能返回null
/nil
,一会又不能,到底想说明什么问题呢?不要着急。
结论
回想一下,我们就是为了避免项目中if reference == null {} else {}
这样的代码遍地开花,才使用Null Object设计模式的。但使用该设计模式后,一旦本该返回null object (如new NullAnimal()
)的地方返回了null
/nil
,就可能绕过编译器的类型检查 (PHP看似没绕过去,但试试去掉函数后面的: Animal
呢,是不是就绕过去了),导致又有可能对Null调用方法,进而是不是还要再if-else
提前检查一下是不是Null呢,这不又回到了最初的情况!
所以使用Null Object设计模式时,要注意的点就是:应该返回null object的地方绝不能再返回null
/nil
了。
可明明定义出了NullAnimal
,又怎么可能在makeAnimalFromAnimalTypes()
的default
分支返回null
/nil
呢?那不白定义了,不可能犯这种低级错误的。
但不要忘了,这里给出的毕竟只是最简单的示例代码。在实际的项目中,可能有各种类型的null object,有的表示不存在的订单,有的表示查无此人的用户,有的表示......。
而且考虑到历史遗留代码、技术债等原因,产生null object的地方可能散布在代码中的各个位置,可不一定都集中在类似makeAnimalFromAnimalTypes()
这样的函数中。这就意味着本该返回null object却返回了Null的地方散布在整个项目中,我们必须确保每一处都没有返回Null。
也就是说,在确保每一处都没有返回Null之前,还是不能相信函数的返回值、参数、局部变量等绝不是Null。更糟糕的是,编译器不会帮助我们检查该不该返回Null,一切潜在错误都发生在程序运行时。
怎么样,这样看来是不是没注意到这一点,Null Object设计模式就相当于白使用了。
在Go语言中什么时候对
nil
调用方法不会报错呢?比如这段代码
go
// https://go.dev/play/p/QAugLYf2lE2
package main
import "fmt"
type dummyWriter struct{}
func (w *dummyWriter) Write(p []byte) (n int, err error) {
// do nothing
return 0, nil
}
func main() {
var nullDummyWriter *dummyWriter
fmt.Printf("%T %v\n", nullDummyWriter, nullDummyWriter)
fmt.Fprintln(nullDummyWriter, "Hello World!") // <-- 在Fprintln内部对nil调用了Write()
}
// *main.dummyWriter <nil>