并非Null Object這么簡(jiǎn)單

Null Object

在大多數(shù)程序語(yǔ)言中,我們都需要與Null打交道缚忧,并且糾纏于對(duì)它的檢查中悟泵。一不小心讓它給溜出來(lái),就可能像打開(kāi)潘多拉的盒子一般闪水,給程序世界帶來(lái)災(zāi)難糕非。說(shuō)起來(lái),在我們?nèi)祟愂澜缰卸氐冢琋ull到底算什么“東西”呢峰弹?語(yǔ)義上講,它就是一場(chǎng)空芜果,即所謂“虛無(wú)”鞠呈。這個(gè)世界并沒(méi)有任何物質(zhì)可以代表“虛無(wú)”,因而它僅存于我們的精神層面右钾。說(shuō)虛無(wú)存在其實(shí)是一種悖論蚁吝,因?yàn)榇嬖谄鋵?shí)是虛無(wú)的反面。若從程序本質(zhì)上講舀射,Null代表一種狀態(tài)窘茁,指一個(gè)對(duì)象(或變量),雖獲聲明卻未真正誕生脆烟,甚至可能永遠(yuǎn)不會(huì)誕生山林。而一旦誕生,Null就被抹去了邢羔,回歸了正確的狀態(tài)驼抹。

站在OO的角度來(lái)講,既然Everything is object拜鹤,自然可以將Null同樣視為Object——這近似于前面提到的悖論框冀,既然是Null,為何又是Object呢敏簿?換言之明也,在對(duì)象世界里宣虾,其實(shí)沒(méi)有什么不存在,所謂“不存在”仍然是一種“存在”温数。這么說(shuō)容易讓人變糊涂绣硝,就好像我們搞不清楚“我是誰(shuí)”。所以帆吻,我寧肯采用Martin Fowler的說(shuō)法域那,將Null Object視為一種Special Case咙边,即Null其實(shí)是一種特例猜煮。

視Null為一種特例,即可用OO的特化來(lái)表達(dá)败许。當(dāng)某個(gè)對(duì)象可能存在Null這種狀態(tài)時(shí)王带,都可以將這種狀態(tài)表示為一種特化的類,它不再代表Null市殷,而是代表“什么都不做”愕撰。凡是返回Null的地方,都替換為這個(gè)Null Object醋寝,用以表達(dá)這種Null其實(shí)僅僅是一種特列搞挣。于是乎,我們像抹殺異教徒一般抹去了“虛無(wú)”的存在音羞。(當(dāng)虛無(wú)被抹去囱桨,是什么樣的存在?)

然而嗅绰,若在程序語(yǔ)言中實(shí)現(xiàn)自己的Null Object舍肠,固然可以在一定程度上消除對(duì)Null的檢查,卻存在一些約束:

  • 對(duì)于String之類的類型窘面,無(wú)法定義NullString子類翠语;
  • 每次都需要自己去定義子類來(lái)表示Null;
  • 必須約束團(tuán)隊(duì)不能返回Null财边;

Google的Guava框架為了解決這一問(wèn)題肌括,引入了Optional<T>:

public abstract class Optional<T> implements Serializable {
  public static <T> Optional<T> absent() {
    return (Optional<T>) Absent.INSTANCE;
  }
  public static <T> Optional<T> of(T reference) {
    return new Present<T>(checkNotNull(reference));
  }
  public static <T> Optional<T> fromNullable(@Nullable T nullableReference) {
    return (nullableReference == null)
        ? Optional.<T>absent()
        : new Present<T>(nullableReference);
  }
  public abstract boolean isPresent();
  public abstract T get();
  public abstract T or(T defaultValue);
  public abstract <V> Optional<V> transform(Function<? super T, V> function);
}

于是,我們可以這樣來(lái)使用Optional<T>:

  public final Optional<E> first() {
    Iterator<E> iterator = iterable.iterator();
    return iterator.hasNext()
        ? Optional.of(iterator.next())
        : Optional.<E>absent();
  }

first()方法返回的是一個(gè)Optional<E>類型酣难。這是Guava中操作集合的一個(gè)方法谍夭。當(dāng)我們要獲得第一個(gè)元素時(shí),可以調(diào)用該方法:

List<Person> persons = newArrayList();
String name = from(persons).first().transform(new Function<Person, String>() {
        @Override
        public String apply(Person input) {
            return input.getName();
        }
    }).or("not found");
assertThat(name, is("not found"));

不知是巧合鲸鹦,還是一種借鑒慧库,Java 8同樣定義了Optional用以處理這種情況。前面的代碼在Java 8下可以改寫為:

        List<Person> persons = newArrayList();
        String name = persons.stream().findFirst().map(p -> p.getName()).orElse("not found");
        assertThat(name, is("not found"));

其實(shí)在Scala的早期版本馋嗜,就已經(jīng)提供了Option[T]類型齐板。前面的代碼若用scala編寫,就變成:

case class Person(name: String, age: Int)
val persons = List[Person]()
persons.headOption.map(p => p.name).getOrElse("not found")

這樣的設(shè)計(jì)方式,還是Null Object模式嗎甘磨?讓我們回到Null的本原狀態(tài)橡羞,思考為什么會(huì)產(chǎn)生Null?首先济舆,Null代表一種異常狀態(tài)卿泽,即在某種未可知的情形下,可能返回Null滋觉;正常情況下签夭,返回的則是非Null的對(duì)象。Null與非Null椎侠,代表一種未知與不確定性第租。哈姆雷特糾結(jié)于“To be, or not to be, this is a question”,但在程序世界里我纪,可以抽象為一個(gè)集合來(lái)表達(dá)這種非此即彼的狀況慎宾。

從函數(shù)式編程的角度來(lái)講,我們可以將這樣的集合設(shè)計(jì)為一個(gè)Monad浅悉。根據(jù)DSL in Action一書(shū)中對(duì)Monad的介紹趟据,一個(gè)Monad由以下三部分定義:

  • 一個(gè)抽象M[A],其中M是類型構(gòu)造函數(shù)。在Scala語(yǔ)言中可以寫成class M[A]术健,或者case class M[A]汹碱,有或者trait M[A]
  • 一個(gè)unit方法(unit v)。對(duì)應(yīng)Scala中的函數(shù)new M(v)或者M(jìn)(v)的調(diào)用苛坚。
  • 一個(gè)bind方法比被,起到將運(yùn)算排成序列的作用。在Scala中通過(guò)flatMap組合子來(lái)實(shí)現(xiàn)泼舱。bind f m對(duì)應(yīng)的Scala語(yǔ)句是m flatMap f等缀。

同時(shí),Monad還必須滿足以下三條規(guī)則:

  • 右單位元(identity)娇昙。即對(duì)于任意Monad m尺迂,有m flatMap unit => m。對(duì)于Option冒掌,unit就是Option伴生對(duì)象定義的apply()方法噪裕。若m為Some("Scala"),則m flatMap {x => Option(x)}股毫,其結(jié)果還是m膳音。
  • 左單位元(unit)。即對(duì)于任意Monad m铃诬,有unit(v) flatMap f => f(v)祭陷。
  • 結(jié)合律苍凛。即對(duì)于任意Monad m,有m flatMap g flatMap h => m flatMap {x => g(x) flatMap h}兵志。

假設(shè)我們定義一個(gè)函數(shù)f:

def f(v: String) = Option(v)

Option("Scala") flatMap {x => f(x)}的結(jié)果就等于f("scala")醇蝴。

無(wú)論是Scala中的Option[A],還是Java 8中的Optional[T]想罕,都是一個(gè)Monad悠栓。此時(shí)的Null不再是特例,而是抽象Option[A]對(duì)稱的兩個(gè)元素中的其中一個(gè)按价,在Scala中惭适,即Option[T]中的Some[T]或None。它們倆面貌相同俘枫,卻是一對(duì)性格迥異的雙生子腥沽。

在設(shè)計(jì)為Monad后,就可以利用Monad提供的bind功能鸠蚪,完成多個(gè)函數(shù)的組合。組合時(shí)师溅,并不需要考慮返回為None的情況茅信。Monad能保證在前一個(gè)函數(shù)返回空值時(shí),后續(xù)函數(shù)不會(huì)被調(diào)用墓臭。讓我們來(lái)看一個(gè)案例蘸鲸。例如,我們需要根據(jù)某個(gè)key從會(huì)話中獲得對(duì)應(yīng)的值窿锉,然后再將該值作為參數(shù)去查詢符合條件的特定Customer酌摇。在Scala中,可以將這兩個(gè)步驟定義為函數(shù)嗡载,返回結(jié)果分別為Option[String]與Option[Customer]:

def params(key: String): Option[String]
def queryCustomer(refId: String): Option[Customer]
val customer = 
    (
        for {
            r <- params("customerId")
            c <- queryCustomer(r)
        } yield c
    ) getOrElse error("Not Found") 

這段代碼用到了Scala的for comprehension窑多,它實(shí)則是對(duì)flatMap和map的一種包裝。尤其當(dāng)嵌套多個(gè)flatMap和map時(shí)洼滚,使用for comprehension會(huì)更加直觀可讀埂息。翻譯為flatMap,則為:

params("customerId").flatMap{
    r => queryCustomer(r).map {
        c => c
    }
} getOrElse error("Not Found") 

當(dāng)我最初看到Guava設(shè)計(jì)的Optional[T]時(shí)遥巴,我以為是Null Object模式的體現(xiàn)千康。顯然,它的功能要超出Null Object的范疇铲掐。但它也并非Monad拾弃,在前面給出的定義中,我們可以看到Guava的Optional[T]僅提供了map(即定義中的transform)功能摆霉,而沒(méi)有提供更基本的flatMap操作豪椿。具有函數(shù)式編程功能的Scala與Java 8加強(qiáng)了這一功能颠毙,利用Monad強(qiáng)化了程序?qū)ull的處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子徐许,更是在濱河造成了極大的恐慌征堪,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狈孔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)霎奢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)饼灿,“玉大人幕侠,你說(shuō)我怎么就攤上這事“恚” “怎么了晤硕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)庇忌。 經(jīng)常有香客問(wèn)我舞箍,道長(zhǎng),這世上最難降的妖魔是什么皆疹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任疏橄,我火速辦了婚禮,結(jié)果婚禮上略就,老公的妹妹穿的比我還像新娘捎迫。我一直安慰自己,他們只是感情好表牢,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布窄绒。 她就那樣靜靜地躺著,像睡著了一般初茶。 火紅的嫁衣襯著肌膚如雪颗祝。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天恼布,我揣著相機(jī)與錄音螺戳,去河邊找鬼。 笑死折汞,一個(gè)胖子當(dāng)著我的面吹牛倔幼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播爽待,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼损同,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼翩腐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起膏燃,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤茂卦,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后组哩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體等龙,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年伶贰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蛛砰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡黍衙,死狀恐怖泥畅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情琅翻,我是刑警寧澤位仁,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站望迎,受9級(jí)特大地震影響障癌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辩尊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望康辑。 院中可真熱鬧摄欲,春花似錦、人聲如沸疮薇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)按咒。三九已至迟隅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間励七,已是汗流浹背智袭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掠抬,地道東北人吼野。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像两波,于是被迫代替她去往敵國(guó)和親瞳步。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闷哆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容