天天看點

《設計模式沉思錄》—第2章2.7節多使用者檔案系統的保護

本節書摘來自異步社群《設計模式沉思錄》一書中的第2章,第2.7節多使用者檔案系統的保護,作者【美】john vlissides,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

2.7 多使用者檔案系統的保護

我們已經讨論了如何給我們正在設計的檔案系統添加簡單的單使用者保護。前面提到我們會将這個概念擴充到多使用者環境,在這個環境中許多使用者共享同一個檔案系統。無論是配以中樞檔案系統的傳統分時系統,還是當代的網絡檔案系統,對多使用者的支援都是必不可少的。即使那些為單使用者環境所設計的個人計算機作業系統(如os/2和windows nt),現在也已經支援多使用者。無論是什麼情況,多使用者支援都給檔案系統保護這一問題增加了難度。

我們将再一次采用最簡易的設計思路,效仿unix系統的多使用者保護機制。unix檔案系統中的一個節點是與一個“使用者”相關聯的。在預設的情況下,一個節點的使用者就是建立該節點的人。從節點的角度來看,這種關聯把所有使用者分為兩類:該節點的使用者,以及其餘所有的人。用标準的unix術語來說,“其餘所有的人”就是other⑪。

通過對一個節點的使用者和其他人進行區分,我們可以給每種類型分别指定保護級别。例如,如果一個檔案對它的使用者是可讀的,但對其他人是不可讀的,那麼我們說該檔案是“使用者可讀的”但“其他人不可讀的”。對寫保護和可能會提供的其他保護模式(如擴充性、自動存檔等)來說,它們的工作方式與此相似。

使用者必須有一個登入名,無論是對系統還是對其他使用者來說,這個登入名都唯一辨別了該使用者。雖然在現實中一個人可以有多個登入名,但對系統來說,“使用者”和“登入名”是不可分割的。重要的是要保證一個人不能将自己與任何不屬于他的登入名(假設他有一個登入名)相關聯。這就是為什麼我們在登入到unix系統時,不僅需要提供登入名,而且還要提供密碼來驗證身份的原因。這個過程被稱為身份驗證。unix不遺餘力地對僞裝加以防範,這是因為冒名頂替者能夠通路合法使用者能夠通路的任何東西。

現在可以讨論具體的細節了。我們該如何對使用者進行模組化?作為面向對象的開發人員,答案很顯然:使用對象。每個對象都有一個類,是以我們要定義一個user類。

我們現在需要考慮user類的接口。客戶代碼能夠用user對象做什麼?事實上,在目前的階段更重要的是客戶代碼不能用user對象做什麼。特别是,我們不應該允許客戶代碼随意建立user對象。

為了了解其中的原因,讓我們假設user對象和登入名之間存在一對一的映射。(雖然我們可以允許一個登入名有多個user對象,但目前這樣的需求尚不明确。)進一步假設一個user對象必須有一個合法的登入名與之相關聯。這個假設是合理的,因為從系統的角度而言,沒有登入名的使用者是沒有意義的。最後,如果客戶沒有同時提供登入名和密碼,那麼我們不能讓他們建立user對象。否則,一個流氓程式隻需要用相應的登入名建立user對象,就可以通路機密的檔案和目錄了。

一個user對象的存在代表了一次身份驗證。于是很明顯,我們必須對建立user對象執行個體的過程嚴加控制。如果應用程式提供了錯誤的登入名或密碼,那麼建立user對象執行個體的嘗試應該失敗,而且同時不會産生不完整的user對象,也就是那些由于在建立的時候缺乏必需的資訊而導緻無法正常使用的user對象。這幾乎排除了我們用傳統的c++構造函數來建立user執行個體的可能性。

我們需要一種安全的方法來建立user對象,在客戶代碼使用的接口中,這種方法不應該涉及構造函數。這裡安全的意思是,客戶代碼應該無法通過任何不正當的方式來建立user對象的執行個體。那麼我們如何用面向對象的術語來表述這樣的安全性呢?

讓我們考慮一下面向對象概念的三個基本要素:繼承、封裝和多态。其中與安全性最相關的非封裝莫屬。事實上,封裝是安全性的一種形式。根據定義,客戶代碼是肯定無法通路封裝後的代碼和資料的⑫。那麼在我們的例子中我們想要封裝什麼?至少應該包括整個身份驗證的過程,這個過程以使用者的輸入為開始,以user對象的建立為終止。

我們已經找到了問題。現在我們需要尋找一個解決方案,并用對象來把這個解決方案表達出來。也許現在是看一些模式的時候了。

此時此刻,我承認在模式的選擇上,我們還沒有什麼指導方法。但我們知道對象的建立和封裝都是非常重要的部分。為了縮小搜尋的範圍,《設計模式》根據每個模式的目的将它們分為三組:建立型、結構型以及行為型。其中建立型模式看起來和我們的問題聯系最緊密:abstract factory、builder、factory method、prototype以及singleton。因為一共隻有5個模式,是以我們可以先快速浏覽每個模式,看是否能從中找到一個合适的模式。

abstract factory關注的是建立一系列的對象而無需指定具體的類。這很好,但我們的設計問題并沒有涉及一系列對象,而且我們也不反對建立具體的類(即user類)的執行個體。是以我們就排除了abstract factory。下一個是builder,它關心的是建立複雜的對象。它讓我們使用相同的一些步驟來構造對象,而這些對象具有不同的表現形式,這和我們的問題沒有太大的關系。除了沒有強調對系列的支援,factory method的意圖與abstract factory相似,是以它和我們的問題也沒有很緊密的聯系。

prototype怎麼樣?它把待建立對象執行個體的類型放到參數中。這樣我們就可以用一個執行個體(這個執行個體在運作的時候是可以替換的)作為原型,并調用它的copy操作來建立新的執行個體,而無需編寫代碼用new操作符和類名(在運作的時候是無法改變的)來建立新的執行個體。如果要改變被執行個體化的對象的類,隻需用一個不同的執行個體作為原型即可。

但這也不對。我們的興趣并不在于改變要建立什麼對象,而在于對客戶代碼如何建立user對象加以控制。由于任何人都可以對原型進行複制,是以和原始的構造函數相比,我們并沒有獲得更多的控制權。此外,在系統中保持一個user對象作為原型會對我們的身份驗證模型産生危害。

剩下的隻有singleton了。它的意圖是確定每個類隻有一個執行個體,并提供一個全局通路點來通路該執行個體。該模式規定了一個名為instance的靜态成員函數,該函數不帶任何參數,它會傳回這個類的唯一執行個體。為了防止客戶代碼直接使用構造函數,所有的構造函數都是受保護的。

乍一看這似乎也不怎麼适用——一個程式可能需要一個以上的user對象,不是嗎?但即便我們不想把執行個體的數量限制為隻有一個,我們确實想禁止每個使用者有一個以上的執行個體。無論是哪種情況,我們都要對執行個體的數量加以限制。

是以,singleton可能還是适用的。再仔細看一下singleton的效果部分,我們發現了下面的描述:

[singleton] 允許執行個體的數量是可變的。該模式讓我們能夠非常容易地改變想法來允許singleton類有一個以上的執行個體。此外,我們還可以用相同的方法來對應用程式使用的執行個體數量加以控制。隻有那個有權通路singleton執行個體的[instance]操作才需要修改。

就是它了!我們的情況正是singleton模式的一個變體,我們可以将instance操作重新命名為login并給它指定一些參數。

login確定隻為每一個登入名建立一個執行個體。為了達到這個目的,user類可能會維持一個私有的靜态散清單,該散清單以登入名為索引,用來儲存user對象。login在這個散清單中查找loginname參數。如果找到了對應的user項,那麼就傳回該項,否則login就執行下面的操作。

(1)建立一個新的user對象,通過密碼來進行身份驗證。

(2)在散清單中登記該user對象,以便今後通路。

(3)傳回該user對象。

下面對user::login操作的屬性進行了總結。

可以在應用程式的任何地方通路它。

它防止使用者為每個登入名建立一個以上的執行個體。

與構造函數不同的是,如果登入名或密碼不正确,那麼它會傳回0。

應用程式不能通過從user派生子類的方式來修改login。

必須承認的是,這是對singleton模式的一種非正規應用。客戶代碼能夠建立一個以上的user執行個體,這意味着我們并未嚴格遵循該模式的意圖。但是,我們确實要對執行個體的數量進行控制,而該模式對此也提供了支援。我們都明白,模式并不代表唯一的解決方案。一個好的模式不僅僅隻是對一個問題的解決方案的描述,它還給我們以洞察力和了解力,進而能夠對解決方案進行修改,使之符合我們自己的情況。

即便如此,singleton并沒有告訴我們一切。例如,由于我們已經提供了login操作,是以如果客戶代碼期望我們提供一個對應的logout操作來讓使用者登出系統,那将是個合理的要求。logout将會引出一些重要的問題,這些問題與singleton對象的記憶體管理有關。然而奇怪的是,singleton模式對這些問題卻隻字未提。我們将在第3章就這些問題展開讨論。

※   ※   ※

下一個問題:客戶代碼如何使用user對象?為了找到答案,讓我們先來看一些用例。

首先,考慮登入的過程。假設當使用者想要通路系統(或者至少是系統中受保護的部分)時,系統會執行一個登入程式。登入程式會調用user::login來得到一個user對象。然後登入程式通過某種方式讓其他應用程式能夠通路該user對象,這樣該使用者就不必再次登入了。

其次,讓我們考慮一個應用程式如何通路一個幾天前建立的檔案,該檔案的建立者的登入名為“johnny”。假設這個應用程式的使用者登入名為“mom”,并且該檔案是使用者可讀的但其他人不可讀的。是以,系統不應該允許“mom”通路該檔案。在單使用者系統中,應用程式通過調用streamout操作并在參數中傳入一個流,來要求得到檔案的内容。

void streamout(ostream&);

我們希望這個調用在多使用者的情況下最好保持不變,但它沒有在參數中引用正在通路該檔案的使用者。缺少了這個引用,系統将無法確定使用者具有通路檔案的權限。我們要麼将這個引用在參數中顯式地傳入。

void streamout(ostream&, const user*);

要麼通過登入過程隐式地得到這個引用。大部分的情況下,應用程式會在整個生命周期中代表唯一一個使用者。在這種情況下,需要不停地在參數中提供user對象是很煩人的。但是,一個協作式的應用程式可供多個使用者使用,這很容易想象而且很合理。在這種情況下,給每個操作指定user對象是必需的。

是以,我們需要給node接口中的每個操作增加一個const user*參數——但同時不應該強迫客戶代碼必須提供該參數。預設參數所提供的靈活性讓我們能夠得體地處理這兩種情況。

通常情況下,使用者是隐式的,我們需要一個全局可通路的操作來得到這個唯一的user執行個體。這就是singleton,但為了靈活性,我們還應該允許應用程式設定singleton執行個體。是以,我們不使用唯一的user::instance靜态操作,而是使用下面的get和set靜态操作。

setuser讓應用程式将隐式的使用者設定為任何const user,當然這個const user應該是通過正當手段得到的。現在登入程式就可以調用setuser來設定全局user執行個體了,因為其他應用程式也應該使用該執行個體⑬。

到目前為止,一切都很容易了解。但我一直在回避一個簡單的問題:所有這些是如何對streamout和node接口中的其他模闆方法的實作産生影響的?或者更加直接一點,它們如何使用user對象?

與單使用者的設計相比,關鍵的差別并不在于模闆方法本身,而在于傳回類型為布爾值的基本操作。例如,streamout變成了如下所示。

在第二行我們可以看到一個明顯的差別。如果參數中指定了使用者,那麼局部變量user會被初始化為指定的使用者,如果參數中沒有指定使用者,那麼user會被初始化為預設的user對象。但更為顯著的差別是在第三行,其中isreadableby取代了isreadable。isreadableby根據存儲在節點中的資訊,檢查該節點是使用者可讀還是其他人可讀。

isreadableby揭示了對user::owns的需求——這個操作檢查user對象中的登入名以及與節點相關聯的登入名。如果兩者的值相同,那麼該使用者擁有節點。owns操作需要一個接口來從節點擷取登入名。

const string& node::getloginname();

節點也需要isuserreadable和isotherreadable之類的基本操作,這些操作就使用者和其他人是否能夠讀寫節點提供了更詳細的資訊。node基類可以在執行個體變量中儲存一些标志,并将這些操作簡單地實作為對這些标志的通路,或者也可以将此類與存儲有關的細節交給子類去處理。

我們已經讨論了足夠多的細節,現在讓我們重新回到設計層面。

讀者應該還記得我們把世界一分為二——使用者和其他人。但那樣可能有些過于極端了。舉個例子,如果我們正在和一些同僚開發一個項目,那麼我們很可能想要通路彼此的檔案。我們可能還要對檔案進行保護,使開發組之外的人員無法窺探它們。這正是unix提供了第三種類型(即group)的原因之一。一個group是經過命名的一組登入名。使節點成為組可讀(group-readable)或組可寫(group-writable)使我們能夠對通路權限進行精細的控制,對需要互相協作的工作環境來說,這樣的控制能夠很好地滿足要求。

要在設計中加入組的概念,我們需要了解哪些資訊?我們知道兩條資訊。

(1)一個組有零個或多個使用者。

(2)一個使用者可以是零個或多個組的成員。

第二條意味着應該使用引用,而不應該使用聚合,因為删除一個組并不會删除其中包含的使用者。

根據我們對組的了解,用對象來表示它們再合适不過了。問題是,我們是要定義一個新的類層次,還是隻對已有的類層次進行擴充呢?

我肯定user類是唯一适合擴充的候選類。另一種選擇是将group類定義為一種類型的node,這種選擇既沒有意義也沒有用處。是以讓我們來考慮一下group和user之間的繼承關系會給我們帶來什麼。

我們已經熟悉了composite模式。它描述了葉節點(如file)和複合節點(如directory)之間的遞歸關系。它給所有的節點以完全相同的接口,這樣我們不僅能以統一的方式來處理它們,而且能以層級的形式來組織它們。也許我們想要的是使用者群組之間的複合關系:user是leaf類,而group是composite類。

讓我們再回顧一下composite模式的适用性部分,其中提到該模式适用于下面的情況。

我們想要表示一個部分—整體的對象層次結構。

我們想讓客戶代碼忽略複合對象和個體對象之間的差別。客戶代碼将以統一的方式來處理複合結構中的所有對象。

根據這些标準,我們可以确信composite模式并不适用。下面三條就是原因。

類之間的關系并不是遞歸的。由于unix檔案系統不允許一個組裡包含其他的組,是以我們不需要這樣的支援。僅僅因為該模式指定了遞歸關系并不代表我們的應用程式就一定需要用到這種關系。

一個使用者可能隸屬多個組。是以類之間的關系并不是嚴格意義上的層次結構。

以統一的方式來處理使用者群組是有問題的。用一個組來登入是什麼意思?用一個組來進行身份驗證又是什麼意思?

這三條原因反駁了user和group之間的複合關系。但由于系統必須記錄哪些使用者隸屬哪些組,是以我們仍然需要在使用者群組之間建立某種關聯。

事實上,為了得到最佳的性能,我們需要雙向映射。系統中使用者的數量很可能要比組的數量多得多,是以系統必須能夠在不檢查每個使用者的前提下,确定一個組中的所有使用者。查找一個使用者隸屬的所有的組也同樣重要,因為這可以讓系統更快地檢查使用者是否是某個組的成員。

實作雙向映射的一個顯而易見的方法是給group和user類增加集合:在group類中加入一個節點集合,在user類中加入一個組集合。但這種方法有兩個嚴重的缺點。

(1)映射關系難以修改。我們至少必須修改一個基類,甚至可能要修改兩個基類。

(2)所有對象都必須承擔集合的開銷。不包含任何使用者的組需要承擔集合的開銷,不屬于任何組的使用者也需要承擔集合的開銷。即使在開銷最小的情況下,每個對象仍然需要額外存儲一個指針。

組和使用者之間的映射不僅複雜,而且可能會發生變化。上面這個顯而易見的方法将管理和維護映射的職責分散了,進而導緻了剛才提到的缺點。有一個不太顯而易見的方法可以避免這些缺點,那就是将職責集中起來。

mediator模式将對象間的互動升格為完整的對象狀态。通過不讓對象顯式地互相引用,它促進了松耦合,進而讓我們能夠單獨地改變對象之間的互動,同時無需改變對象本身。

圖2-5是在應用該模式之前的典型情況。有許多互相互動的對象(該模式将它們統稱為colleague),每一個colleague都直接引用了(幾乎所有的)其他colleague。

《設計模式沉思錄》—第2章2.7節多使用者檔案系統的保護

圖2-6所示是應用該模式後的結果。

《設計模式沉思錄》—第2章2.7節多使用者檔案系統的保護

該模式的核心是一個mediator對象,它對應于第二張圖中的grouping對象。各colleague之間沒有顯式地互相引用,而是引用了mediator。

在我們的檔案系統中,grouping對象定義了使用者群組之間的雙向映射。為了使映射易于修改,該模式為所有的mediator對象定義了一個抽象基類,我們可以從這個基類派生與映射有關的子類。下面是grouping(mediator)提供的一個簡單接口,該接口讓客戶代碼實作使用者與組之間的注冊和登出操作。

這個接口中第一個要注意的地方是靜态的get和set操作,這兩個操作類似于我們将singleton模式應用于user類時定義的靜态操作。我們在此應用singleton模式也是出于相同的原因:映射需要是全局可通路和可設定的。

通過在運作的時候替換grouping對象,我們能夠一下子改變映射。例如,也許出于監管的目的,一個超級使用者能夠對映射進行重定義。我們必須對修改映射的操作進行嚴密保護,這也是為什麼客戶代碼在調用setgrouping時必須傳入一個經過身份驗證的const user*的原因。與此類似,在建立或解除映射關系時,傳給register和unregister操作的使用者參數也必須經過身份驗證。

最後兩個操作getgroup和getuser用來得到相應的組和使用者。可選的索引參數為客戶代碼提供了一種便捷的方式來周遊多個值。具體子類可以給這些操作定義不同的實作。注意,這些操作并沒有直接用到user對象,而是用一個字元串來表示相應的登入名。這使得任何客戶代碼即使無權通路某個user對象,仍然可以知道該使用者與哪些組相關聯。

mediator模式的隐患之一是它有産生巨型mediator類的趨向。由于mediator類封裝的互動可能非常複雜,是以它可能會成為一個難以維護和擴充的巨型類。運用其他一些模式有助于預防這樣的可能性。例如,我們可以使用template method來允許子類對mediator的部分行為進行修改。strategy不僅能讓我們完成同樣的任務,而且還提供了更好的靈活性。composite讓我們能夠以遞歸的形式把一些較小的部分組合成一個mediator類。

繼續閱讀