土狗屋

土狗屋

《Clean Code》程式碼整潔之道

前七章內容閱讀與紙質書,後面的內容源於下面的電子書鏈接:

代碼整潔之道

HOW DO YOU WRITE FUNCTIONS LIKE THIS? 如何寫出這樣的函數#

寫軟體就像寫其他任何東西一樣。當你寫論文或文章時,你先把想法寫下來,然後再進行潤飾,直到它讀起來順暢。初稿可能笨拙且無序,因此你會不斷修改、重組和精煉,直到它讀起來符合你的期望。

寫代碼和寫別的東西很像。在寫論文或文章時,你先想什麼就寫什麼,然後再打磨它。初稿也許粗陋無序,你就斟酌推敲,直至達到你心目中的樣子。

當我寫函數時,它們往往冗長且複雜。它們有很多縮進和嵌套循環。它們的參數列表很長。名稱是隨意的,還有重複的代碼。但我也有一套單元測試,覆蓋了每一行笨拙的代碼。

我寫函數時,一開始都冗長而複雜。有太多縮進和嵌套循環。有過長的參數列表。名稱是隨意取的,也會有重複的代碼。不過我會配上一套單元測試,覆蓋每行醜陋的代碼。

然後我會潤飾和精煉這些代碼,拆分出函數、修改名稱、消除重複。我縮短方法並重新排列它們。有時我會拆分整個類,同時保持測試通過。

然後我打磨這些代碼,分解函數、修改名稱、消除重複。我縮短和重新安置方法。有時我還拆散類。同時保持測試通過。最後,遵循本章列出的規則,我組裝好這些函數。

最終,我得到的函數遵循了我在本章中列出的規則。我並不是一開始就這樣寫它們。我想沒有人能做到。

我並不從一開始就按照規則寫函數。我想沒人做得到。

  • 編程藝術是且一直就是語言設計的藝術。
  • 注釋的恰當用法是彌補我們在用代碼表達意圖時遭遇的失敗。注意,我用了 “失敗” 一詞。我是說真的。注釋總是一種失敗。我們總無法找到不用注釋就能表達自我的方法,所以總要有注釋,這並不值得慶賀。
    • Ps. 所以還是得寫,因為我現在還無法避免所有的 “失敗”
  • 我為什麼要極力貶低注釋?因為注釋會撒謊。也不是說總是如此或有意如此,但出現得實在太頻繁。注釋存在的時間越久,就離其所描述的代碼越遠,越來越變得全然錯誤。原因很簡單。程序員不能堅持維護注釋。

再次,我們看到這兩種定義的互補性;它們是虛擬的對立面!這揭示了對象和數據結構之間的根本二分法:

我們再次看到這兩種定義的本質;它們是截然對立的。這說明了對象與數據結構之間的二分原理:

過程式代碼(使用數據結構的代碼)便於在不改動既有數據結構的前提下添加新函數。OO 代碼,另一方面,便於在不改動既有函數的前提下添加新類。

過程式代碼(使用數據結構的代碼)便於在不改動既有數據結構的前提下添加新函數。面向對象代碼便於在不改動既有函數的前提下添加新類。

互補的情況也成立:

反過來講也說得通:

過程式代碼難以添加新數據結構,因為所有函數必須改變。OO 代碼難以添加新函數,因為所有類必須改變。

過程式代碼難以添加新數據結構,因為必須修改所有函數。面向對象代碼難以添加新函數,因為必須修改所有類。

所以,對於 OO 而言困難的事情,對於過程式代碼卻容易,而對於過程式代碼困難的事情,對於 OO 卻容易!

所以,對於面向對象較難的事,對於過程式代碼卻較容易,反之亦然!

在任何複雜系統中,總會有需要添加新數據類型而不是新函數的時候。在這些情況下,對象和 OO 是最合適的。另一方面,還會有想要添加新函數而不是數據類型的時候。在這種情況下,過程式代碼和數據結構會更合適。

在任何一個複雜系統中,總會有需要添加新數據類型而不是新函數的時候。這時,對象和面向對象就比較適合。另一方面,也會有想要添加新函數而不是數據類型的時候。在這種情況下,過程式代碼和數據結構更合適。

成熟的程序員知道,認為一切都是對象的想法是一種神話。有時你真的想要簡單的數據結構,並在其上運行程序。

像我們為 ACMEPort 定義的包裝器非常有用。事實上,包裝第三方 API 是一種最佳實踐。當你包裝一個第三方 API 時,你最小化了對它的依賴:未來你可以選擇轉移到不同的庫,而不會有太大的懲罰。包裝還使得在測試自己的代碼時更容易模擬第三方調用。

類似我們為 ACMEPort 定義的這種打包類非常有用。實際上,將第三方 API 打包是個良好的實踐手段。當你打包一個第三方 API,你就降低了對它的依賴:未來你可以不太痛苦地改用其他代碼庫。在你測試自己的代碼時,打包也有助於模擬第三方調用。

包裝的最後一個優勢是你不必綁死在特定廠商的 API 設計選擇上。你可以定義一個你覺得舒適的 API。在前面的例子中,我們為端口設備故障定義了一種類型的異常,並發現這樣能寫出更整潔的代碼。

打包的好處還在於你不必綁死在某個特定廠商的 API 設計上。你可以定義自己感覺舒服的 API。在上例中,我們為端口設備錯誤定義了一個異常類型,然後發現這樣能寫出更整潔的代碼。

對於代碼的某個特定區域,單一異常類通常可行。伴隨異常發送出來的信息能夠區分不同錯誤。如果你想要捕獲某個異常,並且放過其他異常,就使用不同的異常類。

對於代碼的某個特定區域,單一異常類通常可行。伴隨異常發送出來的信息能夠區分不同錯誤。如果你想要捕獲某個異常,並且放過其他異常,就使用不同的異常類。


在此之前的內容主要是通過紙質書閱讀的,筆記難以整理(其實是懶)就零散放在上面了,接下來幾章主要通過大佬的在線翻譯(見頂部鏈接)閱讀的,比較好進行記錄。

第八章 邊界#

  • 我們沒有測試第三方代碼的責任,但為要使用的第三方代碼編寫測試,可能最符合我們的利益。

學習第三方代碼很難。整合第三方代碼也很難。同時做這兩件事難上加難。如果我們採取不同的做法呢?不要在生產代碼中試驗新東西,而是編寫一些測試來探索我們對第三方代碼的理解。Jim Newkirk 將這類測試稱為學習測試。

學習第三方代碼很難。整合第三方代碼也很難。同時做這兩件事難上加難。如果我們採取不同的做法呢?不要在生產代碼中試驗新東西,而是編寫測試來遍覽和理解第三方代碼。Jim Newkirk 把這叫做學習性測試(learning tests)。

在學習性測試中,我們如在應用中那樣調用第三方 API。我們基本上是在通過核對試驗來檢查自己對那個 API 的理解程度。測試聚焦於我們想從 API 得到的東西。

在學習性測試中,我們如在應用中那樣調用第三方代碼。我們基本上是在通過核對試驗來檢查自己對那個 API 的理解程度。測試聚焦於我們想從 API 得到的東西。

第九章 單元測試#

有些讀者可能會同意這種做法。或許,在很久以前,你也用過我為那個 Timer 類寫測試的方法。從編寫那種用後即扔的測試,到編寫全套自動化單元測試是一大進步。所以,就像那個我指導過的團隊一樣,你或許也會認為脏測試好過沒測試。

有些讀者可能會同意這種做法。或許,在很久以前,你也用過我為那個 Timer 類寫測試的方法。從編寫那種用後即扔的測試到編寫全套自動化單元測試是一大進步。所以,就像那個我指導過的團隊一樣,你或許也會認為脏測試好過沒測試。

這個團隊沒有意識到的是,脏測試等同於 —— 如果不是壞於的話 —— 沒測試。問題在於,測試必須隨著生產代碼的演進而修改。測試越脏,就越難修改。測試代碼越纏結,你就越有可能花更多時間塞進新測試,而不是編寫新生產代碼。隨著你修改生產代碼,舊測試開始失敗,而測試代碼中的亂七八糟的東西將阻礙代碼再次通過。於是,測試變得就像是不斷翻番的債務。

這個團隊沒有意識到的是,脏測試等同於 —— 如果不是壞於的話 —— 沒測試。問題在於,測試必須隨著生產代碼的演進而修改。測試越脏,就越難修改。測試代碼越纏結,你就越有可能花更多時間塞進新測試,而不是編寫新生產代碼。修改生產代碼後,舊測試就會開始失敗,而測試代碼中亂七八糟的東西將阻礙代碼再次通過。於是,測試變得就像是不斷翻番的債務。

9.3 - 整潔的測試#

這些測試顯然呈現了構造 - 操作 - 檢驗(BUILD-OPERATE-CHECK)模式。每個測試都清晰地拆分為三個環節。第一個環節構造測試數據,第二個環節操作測試數據,第三個部分檢驗操作是否得到期望的結果。

這些測試顯然呈現了構造 - 操作 - 檢驗(BUILD-OPERATE-CHECK)模式。每個測試都清晰地拆分為三個環節。第一個環節構造測試數據,第二個環節操作測試數據,第三個部分檢驗操作是否得到期望的結果。

9.5-F.I.R.S.T#

及時(Timely)測試應及時編寫。單元測試應該恰好在使其通過的生產代碼之前編寫。如果在編寫生產代碼之後編寫測試,你會發現生產代碼難以測試。你可能會認為某些生產代碼本身難以測試。你可能不會去設計可測試的代碼。

及時(Timely)測試應及時編寫。單元測試應該恰好在使其通過的生產代碼之前編寫。如果在編寫生產代碼之後編寫測試,你會發現生產代碼難以測試。你可能會認為某些生產代碼本身難以測試。你可能不會去設計可測試的代碼。

第十章 - 類#

在類這一章中,我試著將其中大部分的概念遷移到 Go 中的 interface {} 進行理解。

類的名稱應當描述其權責。實際上,命名正是幫助判斷類的長度的第一個手段。如果無法為某個類命以精確的名稱,這個類大概就太長了。類名越含混,該類越有可能擁有過多權責。例如,如果類名中包括含義模糊的詞,如 Processor 或 Manager 或 Super,這種現象往往說明有不恰當的權責聚集情況存在。

類的名稱應當描述其權責。實際上,命名正是幫助判斷類的長度的第一個手段。如果無法為某個類命以精確的名稱,這個類大概就太長了。類名越含混,該類越有可能擁有過多權責。例如,如果類名中包括含義模糊的詞,如 Processor 或 Manager 或 Super,這種現象往往說明有不恰當的權責聚集情況存在。

內聚#

  • 當類喪失了內聚性,就拆分它!

所以將大函數拆為許多小函數,往往也是將類拆分為多個小類的時機。程序會更加有組織,也會擁有更為透明的結構。

所以,將大函數拆為許多小函數,往往也是將類拆分為多個小類的時機。程序會更加有組織,也會擁有更為透明的結構。

10.3 - 為了修改而組織#

如果系統解耦到足以這樣測試的程度,也就更加靈活,更加可復用。部件之間的解耦代表著系統中的元素互相隔離得很好。隔離也讓對系統每個元素的理解變得更加容易。

如果系統解耦到足以這樣測試的程度,也就更加靈活,更加可復用。部件之間的解耦代表著系統中的元素互相隔離得很好。隔離也讓對系統每個元素的理解變得更加容易。

通過降低連接度,我們的類就遵循了另一條類設計原則,依賴倒置原則(Dependency Inversion Principle,DIP)。本質而言,DIP 認為類應當依賴於抽象而不是依賴於具體細節。

通過降低連接度,我們的類就遵循了另一條類設計原則,依賴倒置原則(Dependency Inversion Principle,DIP)。本質而言,DIP 認為類應當依賴於抽象而不是依賴於具體細節。

第 11 章 - 系統#

“一開始就做對系統” 純屬神話。反之,我們應該只去實現今天的用戶故事,然後重構,明天再擴展系統、實現新的用戶故事。這就是迭代和增量敏捷的精髓所在。測試驅動開發、重構以及它們打造出的整潔代碼,在代碼層面保證了這個過程的實現。

“一開始就做對系統” 純屬神話。反之,我們應該只去實現今天的用戶故事,然後重構,明天再擴展系統、實現新的用戶故事。這就是迭代和增量敏捷的精髓所在。測試驅動開發、重構以及它們打造出的整潔代碼,在代碼層面保證了這個過程的實現。

不要過早動手

我們都知道最好是將責任授予最有資格的人。我們常常忘記,延遲決策至最後一刻也是好手段。這不是懶惰或不負責;它讓我們能夠基於最有可能的信息做出選擇。提前決策是一種預備知識不足的決策。如果決策太早,就會缺少太多客戶反饋、關於項目的思考和實施經驗。

眾所周知,最好是授權給最有資格的人。但我們常常忘記了,延遲決策至最後一刻也是好手段。這不是懶惰或不負責;它讓我們能夠基於最有可能的信息做出選擇。提前決策是一種預備知識不足的決策。如果決策太早,就會缺少太多客戶反饋、關於項目的思考和實施經驗。

無論你是在設計系統還是單獨的模塊,別忘了使用大概可工作的最簡單方案。

無論是設計系統或單獨的模塊,別忘了使用大概可工作的最簡單方案。

第 12 章 Emergence 迭進#

據 Kent 所述,只要遵循以下規則,設計就能變得 “簡單”:

據 Kent 所述,只要遵循以下規則,設計就能變得 “簡單”:

  • 運行所有測試
  • 不可重複
  • 表達了程序員的意圖
  • 儘可能減少類和方法的數量

運行所有測試;不可重複;表達了程序員的意圖;儘可能減少類和方法的數量;

這些規則按其重要程度排列。

以上規則按其重要程度排列。

值得注意的是,遵循一個簡單明瞭的規則,即我們需要有測試並持續運行它們,會影響我們系統對 OO 低耦合度和高內聚度的主要目標的遵循。編寫測試會導致更好的設計。

遵循有關編寫測試並持續運行測試的簡單、明確的規則,系統就會更貼近 OO 低耦合度、高內聚度的目標。編寫測試引致更好的設計。

在這個重構步驟中,我們可以應用有關良好軟體設計的所有知識。我們可以提高內聚性,降低耦合度,分離關注點,模塊化系統關注點,縮小我們的函數和類,選擇更好的名稱,等等。這也是應用簡單設計后三條規則的地方:消除重複,保證表達力,儘可能減少類和方法的數量。

在重構過程中,可以應用有關優秀軟體設計的一切知識。提升內聚性,降低耦合度,切分關注面,模塊化系統性關注面,縮小函數和類的尺寸,選用更好的名稱,如此等等。這也是應用簡單設計后三條規則的地方:消除重複,保證表達力,儘可能減少類和方法的數量。

我們中的大多數人都經歷過費解代碼的糾纏。我們中的許多人自己就編寫過費解的代碼。寫出自己能理解的代碼很容易,因為在寫這些代碼時,我們正深入於要解決的問題中。代碼的其他維護者不會那麼深入,也就不易理解代碼。

我們中的大多數人都經歷過費解代碼的糾纏。我們中的許多人自己就編寫過費解的代碼。寫出自己能理解的代碼很容易,因為在寫這些代碼時,我們正深入於要解決的問題中。代碼的其他維護者不會那麼深入,也就不易理解代碼。

所以有一個角度的閱讀源碼的方式就是從測試開始(當然前提是準備閱讀的目標擁有足夠優秀的測試代碼,並且覆蓋率夠高。)

編寫良好的單元測試也具有表達性。測試的主要目的之一就是通過實例起到文檔的作用。讀到我們的測試的人應該能很快理解某個類是做什麼的。

編寫良好的單元測試也具有表達性。測試的主要目的之一就是通過實例起到文檔的作用。讀到測試的人應該能很快理解某個類是做什麼的。

第 13 章 Concurrency 並發編程#

“對象是過程的抽象。線程是調度的抽象。”

—James O. Coplien1

“對象是過程的抽象。線程是調度的抽象。”——James O

以下是一些關於編寫並發軟體的中肯說法:

下面是一些有關編寫並發軟體的中肯說法:

  • 並發會在性能和編寫額外代碼上增加一些開銷。
  • 正確的並發是複雜的,即便對於簡單的問題。
  • 並發錯誤通常無法重現,因此經常被忽略為偶發事件,而不是它們實際上是的真正缺陷。
  • 並發通常需要對設計策略的根本性修改。

並發會在性能和編寫額外代碼上增加一些開銷;正確的並發是複雜的,即便對於簡單的問題也是如此;並發缺陷並非總能重現,所以常被看做偶發事件而忽略,未被當做真的缺陷看待;並發常常需要對設計策略的根本性修改。

  • 與並發相關的代碼有其自身的開發、修改和調優生命周期。
  • 與並發相關的代碼有其自身的挑戰,這些挑戰與非並發相關的代碼不同,且往往更為困難。
  • 錯誤的並發代碼可能失敗的方式數量使得它足夠具挑戰性,而不需要周邊應用代碼的額外負擔。

與並發相關代碼有自己的開發、修改和調優生命周期;開發相關代碼有自己要對付的挑戰,和非並發相關代碼不同,而且往往更為困難;即便沒有周邊應用程序增加的負擔,寫得不好的並發代碼可能的出錯方式數量也已經足具挑戰性。

建議:將你的並發相關代碼與其他代碼分開。

建議:分離並發相關代碼與其他代碼。

第 14 章 Successive Refinement 逐步改進#

讓我讓你放心。我並不是從頭到尾簡單地寫這個程序。更重要的是,我不指望你能夠一次性寫出整潔優雅的程序。如果我們在過去幾十年中學到什麼,那就是編程是一種技藝,而非科學。要編寫整潔代碼,你必須先寫髒代碼,然後再清理它。

先放鬆一下神經。這段程序並非從一開始就寫成現在的樣子。更重要的是,我也沒指望你能夠一次過寫出整潔、漂亮的程序。如果說我們從過去幾十年裡面學到什麼東西,那就是編程是一種技藝甚於科學的東西。要編寫整潔代碼,必須先寫肮髒代碼,然後再清理它。

漸進主義要求我在做其他修改之前迅速修正這個問題。事實上,修正並不困難。我只需移動對 null 的檢查。再也不用檢測 boolean 是否為 null,而是檢查 ArgumentMarshaller。

漸進主義要求我在做其他修改之前迅速修正這個問題。修正並不費勁。我只是把對 null 值的檢查移了個位置。再也不用檢測 boolean 是否為 null,而是檢查 ArgumentMarshaler 是否為 null。

14、15、16 三章基本都是實踐內容,但是由於本書的例子是基於 Java 構建的,而本人對 Java 確實不太熟悉(案例必然涉及到 Java 世界的一些特有特性或者輪子),便決定不在這三章中琢磨太多時間。直接將重心放回了最後總結的章節。

第 17 章 Smells and Heuristics 味道與啟發#

本章內容相當於之前內容的一個總結,所以英文原文我就暫且不一並摘錄了。另外,全文都不保證完整,畢竟這只是我的一篇筆記。一般情況下我只會記下讓我有感觸的部分。

  • 無關或不正確的注釋就是廢棄的注釋。注釋會很快過時。最好別編寫將被廢棄的注釋。如果發現廢棄的注釋,最好儘快更新或刪除掉。廢棄的注釋會遠離它們曾經描述的代碼,變成代碼中無關和誤導的浮島。
  • 如果注釋描述的是某種充分自我描述了的東西,那麼注釋就是多餘的。

下方的 G 系列來自 17.4 一般性問題

G2:明顯的行為未被實現#

  • 遵循 “最小驚異原則”(The Principle of Least Surprise),函數或類應該實現其他程序員有理由期待的行為。例如,考慮一個將日期名稱翻譯為表示該日期的枚舉的函數。

    Day day = DayDate.StringToDay(String dayName);
    
  • 如果明顯的行為未被實現,讀者和用戶就不能再依靠他們對函數名稱的直覺。他們不再信任原作者,不得不閱讀代碼細節。

G3:不正確的邊界行為#

  • 沒什麼可以替代謹小慎微。每種邊界條件、每種極端情形、每個異常都代表了某種可能搞亂優雅而直白的算法的東西。別依賴直覺。追索每種邊界條件,並編寫測試。

G5:重複#

  • 每次看到重複代碼,都代表遺漏了抽象。重複的代碼可能成為子程序或乾脆是另一個類。將重複代碼疊放進類似的抽象,增加了你的設計語言的詞彙量。其他程序員可以用到你創建的抽象設施。編碼變得越來越快,錯誤越來越少,因為你提升了抽象層級。
  • 更隱蔽的形態是採用類似算法但具體代碼行不同的模塊。這也是一種重複,可以使用模板方法模式或策略模式來修正。

G6:在錯誤的抽象層級上的代碼#

  • 良好的軟體設計要求分離位於不同層級的概念,將它們放到不同容器中。有時,這些容器是基類或派生類,有時是源文件、模塊或組件。無論哪種情況,分離都要完整。較低層級概念和較高層級概念不應混雜在一起。

G10: 垂直分離#

  • 變量和函數應該在靠近被使用的地方定義。本地變量應該正好在其首次被使用的位置上面聲明,垂直距離要短。本地變量不該在其被使用之處幾百行以外聲明。
  • 私有函數應該剛好在其首次被使用的位置下面定義。私有函數屬於整個類,但我們還是要限制調用和定義之間的垂直距離。找個私有函數,應該只是從其首次被使用處往下看一點那麼簡單。

G11:前後不一致#

  • 如果在特定函數中用名為 response 的變量來持有 HttpServletResponse 對象,則在其他用到 HttpServletResponse 對象的函數中也用同樣的變量名。如果將某個方法命名為 processVerificationRequest,則給處理其他請求類型的方法取類似的名字,例如 processDeletion Request。

G17:位置錯誤的權責#

  • 最小驚異原則在這裡起作用了。代碼應該放在讀者自然而然期待它所在的地方。PI 常量應該在出現在聲明三角函數的地方。OVERTIME_RATE 常量應該在 HourlyPayCalculator 類中聲明。

G20:函數名稱應該表達其行為#

  • 如果你必須查看函數的實現(或文檔)才知道它是做什麼的,就該換個更好的函數名,或者重新安排功能代碼,放到有較好名稱的函數中。

G26:準確#

  • 期望某個查詢的第一次匹配就是唯一匹配可能過於天真。用浮點數表示貨幣幾近於犯罪。因為你不想做並發更新就避免使用鎖和 / 或事務管理往好處說也是一種懶惰行為。在可以用 List 的時候非要把變量聲明為 ArrayList 就過分拘束了。把所有變量設置為 protected 卻不夠自律。
  • 在代碼中做決定時,確認自己足夠準確。明確自己為何要這麼做,如果遇到異常情況如何處理。別懶得理會決定的準確性。如果你打算調用可能返回 null 的函數,確認自己檢查了 null 值。如果查詢你認為是數據庫中唯一的記錄,確保代碼檢查不存在其他記錄。如果要處理貨幣數據,使用整數,並恰當地處理四捨五入。如果可能有並發更新,確認你實現了某種鎖定機制。
  • 代碼中的含糊和不準確要麼是意見不同的結果,要麼源於懶惰。無論原因是什麼,都要消除。

G28:封裝條件#

  • 如果沒有 if 或 while 語句的上下文,布爾邏輯就難以理解。應該把解釋了條件意圖的函數抽離出來。

    // 例如:
    
    if (shouldBeDeleted(timer))
    is preferable to
    
    // 好於
    
    if (timer.hasExpired() && !timer.isRecurrent())
    

G33:封裝邊界條件#

  • 邊界條件難以追蹤。把處理邊界條件的代碼集中到一處,不要散落於代碼中。我們不想見到四處散見的 +1 和 -1 字樣。

    if(level + 1 < tags.length)
    {
      parts = new Parse(body, tags, level + 1, offset + endTag);
      body = null;
    }
    
    // 注意,level + 1 出現了兩次。這是個應該封裝到名為 nextLevel 之類的變量中的邊界條件。
    
    int nextLevel = level + 1;
    if(nextLevel < tags.length)
    {
      parts = new Parse(body, tags, nextLevel, offset + endTag);
      body = null;
    }
    

17.6 名稱#

N2:名稱應與抽象層級相符#

  • 不要取溝通實現的名稱;取反映類或函數抽象層級的名稱。這樣做不容易。人們擅長於混雜抽象層級。每次瀏覽代碼,你總會發現有些變量的名稱層級太低。你應當趁機為之改名。要讓代碼可讀,需要持續不斷的改進。看看下面的 Modem 接口:

N5:為較大作用範圍選用較長名稱#

  • 名稱的長度應與作用範圍的廣泛度相關。對於較小的作用範圍,可以用很短的名稱,而對於較大作用範圍就該用較長的名稱。
    • 類似 i 和 j 之類的變量名對於作用範圍在 5 行之內的情形沒問題。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。