用是軟件開發提高效率的最為重要的手段之一,引用框架、模板、類庫、設計模式等等本質上都是復用。低代碼平臺作為一個以提高效率為基本目標的研發平臺,一定也會在復用方面下足功夫?
開發一個應用如果有一個類似的產品拿來修改總是會比從頭研發來的容易。同時也能引導業務人員說出自己的真正需求,甚至能幫助用戶改進自己的工作流程。
樂扣低代碼平臺的應用模板中心
對于低代碼廠商來說構建這樣的業務模板庫一個長期積累的過程。但是對于一些大廠來說相對就容易一點,他們投入多、影響力大。比如:*搭,他們現在都在利用自己的影響力做一個應用中心生態系統,還能召集很多ISV。
頁面模板可以認為是從應用模板中抽取出來的典型功能模板,它的粒度比整個應用小,這樣復用的可能性更高。
樂扣低代碼平臺頁面模板
頁面模板可以做成帶參數的,比如:查詢列表頁面,可以帶數據模式(比如:表結構),根據這個模式生成對應表格的列,這樣可以增加復用的便利性。
區塊模板就是多個基礎組件的有機組合。它是在頁面上提出一些常見的組件組合,比如數據聯動租戶,特定應用用例等等。舉個例子:比如CMS中的新聞信息一般由一個標題(文字)+圖片+作者(文字)+發表日期(時間)+正文(富文本) 的組合。
樂扣低代碼平臺-區塊模板
區塊模板可以大大簡化研發工作,同時在不增加平臺的復雜度的情況下可以提高組件的豐富程度。
模板都是平臺內置的,在一個應用中如何復用呢?我們利用函數的思想,將頁面中可能反復出現的區域定義為通用模板,在頁面中引用這些模板(類似與調用函數)就可以了,如果引入后還想修改就可以用導入模板的功能,類似于復制代碼。
樂扣低代碼平臺中的通用模塊
復用是一個提高軟件開發效率的重要思想,低代碼平臺一定要利用好這個思想。同時也想請教各位看到這個文章的大佬,在低代碼平臺中還有哪些可以提高效率的建議。
寫代碼最重要一條,是怎樣復用其他程序員的代碼和思路來解決問題。
通過修改他人的代碼來解決復雜問題是種錯誤的做法,不僅成功的機率很低,就算成功也不會提供什么經驗。按照這種方式進行編程,無法成長為一名真正的程序員,在軟件開發領域,前景也是非常有限。
一旦問題達到了一定規模,期望程序員從頭開發一個解決方案不太現實,這會導致程序員大量時間浪費在低效率工作中,并且極大地依賴程序員精通各個方面的知識。另外,這種做法也容易導致程序充滿缺陷或難以維護。
良好的復用和不良的復用
良好的復用幫助我們編寫更好的程序,并且提高程序的編寫速度。不良的復用可能短時間內幫助我們借用其他程序員的思維,但最終會導致不良的開發。下面表格對它們之間的區別進行了總結。
左邊一列顯示了良好復用的屬性,右邊一列顯示了不良復用的屬性。在考慮是否對代碼進行復用時,要考慮它很可能會產生左邊一列的屬性還是右邊一列的屬性。
表:良好的復用和不良的復用
值得注意的是,良好的復用和不良的復用之間的區別,并不是我們復用了什么代碼或者我們怎樣復習它們,而是在于我們所借用的代碼和概念之間的關系。
按照編程的術語,良好的復用是指我們通過閱讀某個人對一個基本概念的描述后編寫代碼或者利用自己以前所編寫的代碼。
注意表格的最后一行,不良的復用常常會導致失敗,因為有可能是一位程序員在復用自己實際上并不理解的代碼。在有些情況下,被借用的代碼一開始能夠工作,但是當程序員試圖對借用的代碼進行修改或者擴展時,由于缺乏深入的理解,很難用有組織的方式完成這樣的任務。程序員必須通過不斷的嘗試和失敗來進行試驗,這樣就違背了我們的基本問題解決規則的第一條也是最重要一條:先規劃再開發。
可復用組件的 5 種類型
知道了我們打算采用的復用類型之后,現在可以對代碼的不同復用方法進行分類了。
在本文中,組件表示由一位程序員所創建的、可以被其他人復用以幫助解決編程問題的任何東西。
組件可以在任何地方出現,它可以是抽象的,也可以是具體的。它可以是一個思路,也可以是一段完整實現的代碼。如果我們把解決一個編程問題看成是處理一個手工項目,我們所學會的解決問題的技巧就像工具一樣,而組件就像是專用的零件。下面的每種組件都是復用程序員的以前工作的不同方式。
1. 代碼塊 Code Block
代碼塊就是將一塊代碼從一個程序清單復制到另一個程序清單。按照更通俗的說法,我們可以稱為復制粘貼工作。這是最低級形式的組件用法,常常代表了不良的復用,會出現不良復用可能導致的所有問題。當然,如果被復制的代碼是自己所編寫的,那就不會有實際的危害,但最好還是把一段現有的代碼包裝成為一個類庫或其他結構,允許它以一種更清晰和更容易維護的方式被復用。
2. 算法 Algorithm
算法是一種編程配方,它是完成一個目標的特定方法,是以日常語言或流程圖的形式表達的。算法是一種高級形式的復用,一般具有良好的復用屬性。算法在本質上只是思路。
3. 模式 Pattern
在編程中,模式(或設計模式)表示具有一種特定編程技巧的模板,比如 singleton。這個概念與算法有關,但又存在區別。算法就像解決特定問題的配方,而模式是在特定的編程情況下所使用的基本技巧。模式所解決的問題一般是在代碼本身的結構內部。
和算法一樣,模式也是高級形式的組件復用。
4. 抽象數據類型 Abstract Data Type
抽象數據類型是由它的操作而不是由這些操作的實現方法所定義的類型。堆棧類型就是一個很好的例子。抽象數據類型與模式的相似之處在于它們定義了操作的效果,但并沒有特別地定義這些操作的實現方式。但是,和算法一樣,這些操作存在一些眾所周知的實現技巧。
5. 庫 Library
在編程中,庫表示一些相關代碼片段的集合。庫一般包含了已編譯形式的代碼以及所需的源代碼聲明。庫可以包含獨立的函數、類、類型聲明以及任何可以出現在代碼中的東西。在C++中,最顯而易見的例子就是標準庫。
一般而言,庫的使用是良好的代碼復用。代碼被包含在庫中,因為它提供了各種程序一般需要使用的功能。庫的代碼可以幫助程序避免“重新發明輪子”。然而,作為程序開發人員,當我們使用庫代碼時,必須從中學到些什么,而不是單純走捷徑。
創建可復用組件的方法
組件非常有用,因此程序員應該盡可能地利用組件。
優秀的程序員必須養成習慣向他的工具箱中不斷添加組件。
對組件的收集可以通過兩種不同的方式進行:程序員可以明確分配時間學習新組件,把它作為一項基本任務,或者可以搜索一個組件來解決一個特定的問題。我們把第一種方法稱為探索式學習,把第二種方法稱為根據需要學習。
1. 探索式學習組件
我們首先從一個探索式學習的例子開始。假設我們想學習關于設計模式的更多知識,對于頻繁使用的設計模式,人們已經達成了廣泛的共識。因此我們能接觸大量的資源。
通過簡單地查找一些設計模式并對它們進行研究,我們就可以從中受益。如果實現了其中一些模式,就可以得到更多的收獲。
我們在典型的模式列表中可以找到的一種模式叫策略。這是一種思路,允許一種算法(或算法的一部分)在運行時才被選擇。在策略模式的最基本形式中,它允許更改函數或方法的操作方式,但不允許對結果進行更改。
例如,一個對類的數據進行排序(或者參與了排序過程)的類方法可能允許對排序的方法做出選擇(如選擇快速排序或插入排序)。不管選擇什么排序方式,其結果(排序后的數據)是相同的,但是允許客戶選擇排序方法可能使代碼的執行效率更高。
客戶對于具有很高重復率的數據可以避免選擇快速排序。根據策略的形式,客戶的選擇會對結果產生影響。例如,有一個表示一手牌的類,排序策略可能會決定 A 被認為是最大(比 K 大)還是最小(比 2 小)。
開始把學習融入到可復用實踐中
通過上文我們知道什么是策略模式,但還沒有運用于創建自己的實現。在工具商店瀏覽工具和實際購買并使用工具之間還是存在顯著區別的。因此,我們現在從貨架上取出這個設計模式,并把它投入到使用中。
嘗試新技巧的最快方法是把它融入到已經編寫完成的代碼中。讓我們設計一個可以用這種模式解決的問題,它建立在我們已經編寫完成的代碼基礎之上。
班長
在一所特定的學校中,每個班級具有一名指定的“班長”,如果教師離開了教室,就由這名學生負責維持課堂秩序。最初,這個稱號授予班里學習成績最好的學生。但是,現在有些教師覺得班長應該是具有最深資歷的學生,也就是學生 ID 最小的那個學生,因為學生 ID 是按照進入班級的先后順序依次分配的。還有一部分教師覺得指定班長這個傳統是件非常愚蠢的事情。為了表示抗議,他們簡單地選擇按照字母順序排列的班級花名冊中所出現的第 1 個學生。我們的任務是修改學生集合類,添加一個方法從集合中提取班長,同時又能適應不同教師的選擇標準。
正如我們所見,這個問題將要采用策略形式的模式。我們需要讓這個方法根據不同的選擇標準返回不同的班長。
為了在 C++ 中實現這一點,需要使用函數指針。我們已經簡單地從 qsort 函數中了解過這個概念。qsort 函數接受一個函數指針,它所指向的函數對需要進行排序的數組中的兩個數據項進行比較。
在這個例子中,我們完成類似的任務。我們將創建一組比較函數,接受 2 個 studentRecord 對象為參數并分別根據成績、學生 ID 值或姓名確定第 1 個學生是否“好于”第 2 個學生。
首先,我們需要為比較函數定義一個類型:
這個聲明創建了一個稱為 firstStudentPolicy 的類型,它是個函數指針,它所指向的函數返回一個 bool 值并接受兩個 studentRecord 類型的參數。
*firstStudentPolicy 號兩邊的括號 ? 是必要的,這是為了防止這個聲明被解釋為返回一個 BOOL 類型的指針的函數。有了這個聲明之后,我們就可以創建 3 個策略函數了:
前兩個函數非常簡單:
higherGrade 在第 1 條記錄的成績值大于第 2 條記錄時返回 true;
lowerStudentNumber 在第 1 條記錄的學生 ID 值小于第 2 條記錄時返回 true。
第 3 個函數 nameComesFirst 在本質上與前兩個函數相同,但它需要使用 strcmp 庫函數。這個函數接受 2 個“C 風格”的字符串,即以 結尾的字符數組而不是 string 對象。因此我們必須對兩條學生記錄的姓名字符串使用 c_str 方法。strcmp 函數在第 1 個字符串按照字母順序出現在第 2 個字符串之前時返回一個負數,因此我們檢查它的返回值以判斷它是否小于 0 ?。
現在,我們就可以修改 studentCollection 類本身了:
這個類聲明增加了一些新的成員:一個私有數據成員 _currentPolicy,它存儲了指向其中一個策略函數的指針、一個用于修改策略的 setFirstStudentPolicy 方法,以及根據當前策略返回班長的 firstStudent 方法本身。
setFirstStudentPolicy 的代碼非常簡單:
我們還需要修改默認構造函數對當前策略進行初始化:
現在,我們可以編寫 firstStudent 方法:
這個方法首先檢查特殊情況。如果沒有需要檢查的鏈表或者不存在策略? ,就返回一條啞記錄。否則,就使用本書中廣泛使用的基本搜索技巧,對這個鏈表進行遍歷并尋找最適當地匹配當前策略的學生。
我們把鏈表開始位置的那條記錄賦值給 first ?,使循環變量從鏈表的第 2 條記錄開始 ?,然后執行遍歷。
在遍歷循環中,對當前策略函數的調用 ? 告訴我們目前所查看的學生根據當前標準是否“好于”到現在為止所找到的最佳學生。當這個循環結束時,我們就返回“班長” ?。
班長解決方案的分析
使用策略模式解決了一個問題之后,我們很可能想確認這種技巧可以適用的其他場合,而不是一次了解了這個技巧之就將其束之高閣。我們還可以對示例問題進行分析,形成對這個技巧的價值的認識,明白什么時候使用它比較合適,什么時候使用它則是個錯誤,或至少它帶來的價值應多于麻煩。對于這個特定的模式,讀者可能會看到它弱化了封裝和信息隱藏。
例如,如果客戶代碼提供了策略函數,它就需要訪問通常屬于類內部的類型,在這個例子中也就是 studentRecord 類型。這意味著如果我們修改了這個類型,客戶代碼就有可能失敗。把這個模式應用于其他項目之前,必須在這個顧慮與它可能帶來的好處之間進行權衡。通過對自己的代碼進行檢查,可以對這個關鍵問題獲得深入的體會。
至于進一步的實踐,我們可以檢查已完成項目的庫,搜索可以使用這種技巧進行重構的代碼。記住,很多“現實世界”的編程涉及到對現有的代碼進行補充或修改,因此這是進行這類修改的一種非常好的實踐,還能發展自己運用某種特定組件的技能。而且,良好的代碼復用的一個優點是我們可以從中進行學習,而實踐能夠最大限度地提升學習的效果。
2. 根據需要尋找可復用組件
前一節描述了“漫游式學習”的過程。雖然這種類型的學習旅程對于程序員而言是極具價值的,但有時候我們必須直接針對一個特定的目標學習。
如果我們正在著手處理一個特定的問題,特別是當這項工作面臨極大的時間壓力時,我們會猜測某個組件可能會為我們提供極大的幫助。我們不想通過隨機漫游編程世界來碰到自己所需要的東西,而是想盡可能快地找到直接適用于自己所面臨問題的組件。
但是,這聽起來似乎有些挑戰,當我們并不準確地知道自己所尋找的是什么時,怎樣才能找到自己所需要的東西呢?思考下面這個示例問題:
高效的遍歷
一個編程項目將使用我們的 studentCollection 類。客戶代碼需要做到能夠遍歷集合中的所有學生。顯然,為了維護信息隱藏,客戶代碼不能直接訪問這個鏈表,但要求高效地對其進行遍歷。
由于這段描述中的關鍵詞是高效,讓我們精確地分析它在這個例子中的含義。我們假設 studentCollection 類的一個特定對象具有 100 條學生記錄。如果我們直接訪問這個鏈表,可以編寫一個迭代 100 次的循環。這是所有的鏈表遍歷中最高效的做法。任何要求我們迭代超過 100 次的循環都可以認為其結果是不夠高效的。
如果沒有高效這個需求,我們可以在這個類中添加一個簡單的 recordAt 方法來解決這個問題。這個方法返回集合中特定位置的學生記錄,第1條記錄的位置編號為 1:
在這個方法中,我們使用了一個循環 ? 對鏈表進行遍歷,直到找到了所需的位置或者到達了鏈表的尾部。當這個循環結束時,如果已經到達了鏈表的尾部,我們就創建并返回一條啞記錄 ?。如果是在指定的位置就返回這條記錄 ?。問題在于我們執行遍歷只是為了尋找一條學生記錄。這并不一定是完整的遍歷,因為當我們到達所需的位置時就會終止循環,但它終歸還是進行了遍歷。假設客戶代碼試圖求學生成績的平均值:
對于這段代碼,假設 sc 是個以前所聲明并生成的 studentCollection 對象,recNum 是個表示記錄數量的整數。假設 recNum 變量值為 100。當我們初步掃視這段代碼時,可能覺得計算平均成績只需要迭代這個循環 100 次,但由于每次調用 recordAt 函數本身就要執行一次不完整遍歷,因此這段代碼總共涉及 100 次遍歷,每次遍歷平均需要進行 50 次迭代。因此,它的結果并不是非常高效的 100 個步驟,而是大約需要 5000 個步驟,這是極為低效的。
什么時候搜索可復用組件
現在,我們觸及到真正的問題。讓客戶訪問集合成員對其進行遍歷是非常容易的,但高效地提供這種訪問卻是非常困難的。當然,我們可以嘗試只用自己的能力來解決這個問題。但是,如果我們可以使用一個組件,就能夠很快實現一個解決方案。
為了尋找一個適用于我們的解決方案的未知組件,第 1 個步驟是假設這個組件實際上存在。換句話說,如果我們不開始搜索,就肯定無法找到這樣一個組件。因此,為了最大限度地獲得組件的優點,需要使自己處于能夠讓組件發揮作用的場合。發現自己陷在問題的某個方面而無法自拔時,可以嘗試下面這些方法:
以通用的方式重新陳述這個問題。
向自己提問:這是否可能成為一個常見的問題?
第 1 個步驟非常重要,因為我們把問題陳述為“允許客戶代碼高效地計算一個類所封裝的記錄鏈表中的平均學生成績”,它聽上去特定于我們所面臨的情形。但是,如果我們把這個問題陳述為“允許客戶代碼高效地遍歷一個鏈表,并且不需要提供對鏈表指針的直接訪問”,我們就開始理解這可能成為一個常見的問題。
顯然,我們可以想象,由于程序常常需要在類中存儲鏈表和其他線性訪問的數據結構,因此其他程序員肯定已經想出了允許高效地訪問數據結構中的每個數據項的辦法。
尋找可復用組件
既然我們已經同意進行觀察,現在就可以尋找組件了。為了清晰起見,我們把原來的編程問題重新陳述為一個搜索問題:“尋找一個組件,可以用它修改我們的studentCollection類,允許客戶代碼高效地遍歷內部的鏈表。”
那么怎樣解決這個問題呢?我們首先可以觀察任意類型的組件:模型、算法、抽象數據類型或庫。
假設我們首先在標準 C++ 庫中進行尋找。我們沒有必要尋找一個可以“插入”到自己的解決方案中的類,而是挖掘一個與自己的 studentCollection 類相似的庫類,以借鑒思路。這就用到了我們用于解決編程問題的類比策略。以前對 C++ 庫的探索已經使我們與諸如 vector 這樣的容器類有了一定程度的接觸,因此我們應該尋找一種與學生集合類最為相似的容器類。
如果求助于自己所喜歡的 C++ 參考資料,例如一本相關的書籍或網絡上的一個站點并查看C++容器類,將會發現有一個稱為 list 的“線性容器”符合這個要求。
list 類是否允許客戶代碼對它進行高效的遍歷呢?它能夠做到這一點,只要使用一個稱為迭代器的對象。我們看到 list 類提供了產生迭代器的 begin 和 end 方法。迭代器是一種對象,它可以引用 list 類中的一個特定數據項,并且可以增加自己的值,使它引用list類中的下一個對象。如果 integerList 是一個包含了整數的 list<int> 并且 iter 是個 list<int>::iterator,我們就可以用下面的代碼顯示這個 list 中的所有整數:
通過使用迭代器,list 類向客戶代碼提供了一種機制高效地對 list 進行遍歷,從而解決了這個問題。此時,我們可能會想到把 list 類本身吸收到我們的 studentCollection 類中,替換原先所使用的鏈表。然后,我們可以為這個類創建 begin 和 end 方法,對它所包含的 list 對象的方法進行包裝,這樣問題就解決了。
但是,這種做法就涉及到良好的復用和不良的復用的問題。
一旦我們完全理解了迭代器的概念并且可以在自己的代碼中生成它,再把標準模板庫中的一個現有的類插入到自己的代碼中就是非常好的選擇,甚至是最好的選擇。但是,如果我們沒有能力做到這一點,對 list 類的這種偷懶用法就不會幫助自己成長為優秀的程序員。
當然有時候我們必須使用那些自己無法開發的組件,但是如果我們養成了讓其他程序員為自己解決問題的習慣,就很難成長為真正的問題解決專家。因此,讓我們自己實現迭代器。
在此之前,我們先簡單地觀察一下尋找迭代器方法的其他途徑。我們是在標準模板庫中進行搜索的,但也可以從其他地方開始搜索。
例如,我們也可以在一組常用的設計模式中進行搜索。在“行為模式”這個標題的下面,我們可以找到迭代器模式。在這個模式中,客戶允許對集合中的數據項進行線性訪問,而不需要直接接觸集合的底層結構。這正是我們所需要的。我們可以通過搜索一個模式列表找到它,也可以通過以前對模式的研究想到它。
我們還可以從抽象數據類型開始搜索,因為通用意義上的列表(以及特定意義上的鏈表)是常見的抽象數據類型。但是,對列表抽象數據類型的許多討論和實現并沒有考慮到把客戶對列表的遍歷作為一種基本操作,因此不會引發迭代器的概念。
最后,如果我們是在算法領域開始搜索的,很可能無法找到適用的東西。算法傾向于描述技巧性的代碼,而創建迭代器則相當簡單,正如我們稍后將要看到的那樣。在這個例子中,在類庫中搜索使我們以最快的速度找到了目標,其次是模式。但是,作為一個基本規則,在搜索一個有用的組件時,必須考慮所有的組件類型。
應用可復用組件
現在,我們準備為 studentCollection 類創建一個迭代器,但是標準模板庫的 list 類向我們所展示的只是怎樣在客戶代碼中使用迭代器的方法。
如果我們不知道該怎樣實現迭代器,可以考慮對 list 類以及它的祖先類的代碼進行研究,但是閱讀大量不熟悉的代碼無疑具有很大的難度,是萬般無奈的情況下不得已采用的辦法。
其實,我們可以用自己的方式來對它進行思考。把以前的代碼例子作為參考,我們可以認為迭代器是由 4 個核心操作所定義的:
1.集合類提供了一個方法 ,提供了引用集合第 1 個元素的迭代器。在 list 類中,這個方法是 begin。
2.測試迭代器是否越過了集合最后一個元素的機制。在 list 類中,這個方法是 end,它針對這個測試產生了一個特殊的迭代器對象。
3.迭代器類中使迭代器向前推進一步的方法,是使它引用集合中的下一個元素。在 list 類中,這個方法是重載的 ++ 操作符。
4.迭代器類中返回集合當前所引用的元素的方法。在 list 類中,這個方法是重載的 *(前綴形式)操作符。
站在編寫代碼的角度,上面這些并沒有困難之處,唯一的問題就是把所有的東西放在正確的位置。因此,我們現在就開始處理這個問題。
根據上面的描述,我們的迭代器(稱為 scIterator)需要存儲一個指向 studentCollection 中的一個元素的引用,并且能夠推進到下一個元素。因此,這個迭代器應該存儲一個指向 studentNode 的指針,這樣就允許它返回集合中的studentRecord對象并允許它推進到下一個 studentNode 對象。
因此,這個迭代器類的私有部分將具備以下這個數據成員:
我們馬上就遇到了一個問題。studentNode 類型是在 studentCollection 類的私有部分聲明的,因此上面這行代碼是行不通的。我們首先想到的是不應該把 studentNode 聲明為私有部分,但這并不是正確的答案。節點類型在本質上是私有的,因為我們并不希望任何客戶代碼依賴節點類型的某種特定性質實現,不想因為這個類進行了修改而導致客戶代碼的失敗。然而,我們還是需要讓 scIterator 類能夠訪問自己的私有類型。
我們通過一個友元聲明來解決這個問題。在 studentCollection 類的公共部分,我們添加了下面這一行:
現在,scIterator 可以訪問 studentCollection 類的私有聲明,包括 studentNode 的聲明。我們還可以聲明一些構造函數,如下所示:
我們稍微觀察一下 studentCollection 類再編寫 begin 方法,這個方法返回一個引用集合第 1 個元素的迭代器。根據本書所使用的命名方案,這個方法應該用名詞來表示,例如 firstItemIterator:
正如所看到的那樣,我們需要完成的任務就是把鏈表的頭指針塞到一個 scIterator 對象中并返回它。如果讀者的做事風格與我相似,看到指針飛臨此處可能會覺得有點緊張,但是注意 scIterator 正要保存一個指向 studentCollection 列表中的一個元素的引用。它不會為自己分配任何內存,因此我們并不需要擔心深拷貝和重載的賦值操作符。
現在我們返回到 scIterator 并編寫其他方法。我們需要一個方法推進迭代器,使它引用下一個元素,還需要編寫一個方法測試它是否越過了集合的尾部。我們應該同時考慮這兩個操作。
在推進迭代器之前,我們需要知道當迭代器越過了列表的最后一個節點之后應該具有什么值。如果不想搞什么特殊,迭代器在這個時候很自然應該是 值,這也是最容易使用的值。注意,我們已經在默認構造函數中把迭代器初始化為 ,因此當我們用 提示越過集合尾部時,就會在這兩種狀態之間產生混淆。但是,對于當前的問題而言,這并不會造成什么麻煩。這個方法的代碼如下:
記住,我們只是用迭代器概念來解決原先的問題。我們并不需要復制C++標準模板庫的迭代器類的準確規范,因此無需使用相同的接口。在這個例子中,我們并非對++操作符進行重載,而是選擇了一個稱為 advance ? 的方法,它判斷當前的指針是否為 ?,然后再把它推進到下一個節點 ?。類似地,我發現創建一種特殊的“尾”迭代器并與之進行比較是種很笨拙的做法,因此決定只選擇一個稱為 pastEnd ? 的 bool 方法,用于確定是否已經遍歷完了節點。
最后,我們需要一種方法獲取當前所引用的 studentRecord 對象:
正如我們之前所做的那樣,為了安全起見,如果指針的值為,我們就創建并返回一條啞記錄 ?。否則,我們就返回當前所引用的記錄 ?。這樣我們就完成了 studentCollection 類的迭代器概念的實現。
為了清晰起見,以下是 scIterator 類的完整聲明:
完成了所有的代碼之后,我們可以用一個示例遍歷對代碼進行測試。下面我們實現平均成績計算以進行比較。
這段代碼使用了所有與迭代器相關的方法,因此可以對我們的代碼進行很好的測試。我們調用 firstItemIterator 函數對 scIterator 對象進行初始化 ?,調用 pastEnd 函數作為循環終止測試 ?。我們還調用迭代器對象的 student 方法獲取當前的studentRecord以便提取成績 ?。最后,為了把迭代器移動到下一條記錄,我們調用了 advance 方法 ?。
當這段代碼順利運行時,我們可以合理地確信自己已經正確地實現了各個方法,而且對迭代器的概念有了堅實的理解。
高效遍歷解決方案的分析
和以前一樣,代碼能夠工作并不意味著這個事件的學習潛力就到此為止了。我們還應該仔細考慮完成了什么任務、它的正面效果和負面影響,并對我們所實現的基本思路的相應擴展進行思考。
在這個例子中,我們可以認為迭代器的概念確實解決了客戶代碼對集合的低效遍歷這個最初問題。一旦實現了迭代器之后,它的使用就變得非常優雅并容易理解。從負面的角度考慮,基于 recordAt 方法的低效方法顯然要容易編寫得多。在決定是否為一種特定的情況實現迭代器時,必須考慮遍歷的發生頻率、列表中一般會出現多少個元素等問題。
如果很少進行對列表的遍歷并且列表本身很短,那么低效問題很可能并不嚴重。但是,如果我們預期列表將會增長或者無法保證它不會增長,那么就應該使用迭代器方法。
當然,如果我們已經決定使用標準模板庫的一個 list 類對象,就不需要再擔心迭代器的實現難度這個問題,因為我們用不著自己實現它。下次再遇到類似的情況時,我們就可以使用 list 類,而不必感覺自己是在偷懶,也不必認為以后會在這方面遇到困難。因為我們已經對列表和迭代器進行了研究,理解了它們幕后的工作原理,即使自己從來沒有研究過它們的實際源代碼。
把話題再深入一步,我們可以考慮迭代器的更廣泛應用以及它們可能存在的限制。
例如,假設我們需要一個迭代器,不僅希望它能夠高效地推進到 studentCollection 中的下一個元素,而且能夠同樣高效地退回到前一個元素。既然我們已經理解了迭代器的工作原理,就很容易明白在當前的 studentCollection 實現上是沒有辦法完成這個任務的。如果迭代器維護一個指向列表中某個特定節點的鏈(即next字段),把它推進到下一個節點只需要訪問節點中的這個鏈。但是,撤回到前一個節點則要求反向遍歷列表。我們可以采用雙鏈表,每個節點維護兩個分別指向前一個節點和下一個節點的指針。
我們可以對這個思路進行歸納,開始考慮不同的數據結構以及它們可以向客戶提供的高效的遍歷類型或數據訪問。
考慮這樣的類似問題可以幫助我們成為更優秀的程序員。我們不僅能夠學習新的技巧,還能夠了解不同組件的優點和缺點。了解組件的優缺點可以幫助我們合理地使用組件。沒有考慮到一種特定方法所存在的限制可能會導致悲慘的結果。對自己所使用的組件了解越多,發生這種事件的概率也就越低。
選擇可復用組件類型
正如我們在這些例子中所看到的那樣,示例問題可以通過不同類型的組件來解決。一個模式可能表達了一種解決方案的思路,一種算法可能規劃了思路的一種實現或者解決同一個問題的另一種思路,一種抽象數據類型可能封裝了某個概念,類庫中的一個類可能包含了一種抽象數據類型的完整的、經過測試的實現。如果它們都是對于解決我們的問題所需要的同一個概念的一種表達,那么我們怎樣才能知道哪種組件類型放進我們的工具箱是最適合的呢?
一個主要的考慮是把組件集成到自己的解決方案需要多大的工作量。把一個類庫鏈接到自己的代碼常常是解決問題最迅速的方法,從一段偽碼描述實現一種算法可能需要大量的時間。
另一個重要的考慮是組件所提供的靈活性有多大。組件常常是以一種漂亮的、預包裝的形式出現,但是當它集成到解決方案時,程序員發現雖然這個組件具有他所需要的大多數功能,但它并不能完成所有的任務。
例如,也許一個方法的返回值格式不正確,需要額外的處理。如果堅持使用這個組件,在使用過程中可能會出現更多的問題,最后還是不得不放棄,只能從頭尋找新的方案。如果程序員選擇了一種位于更高概念層次的組件(如模式),最終的代碼實現將會完美地適合需要解決的問題,因為它就是根據這個問題而創建的。
下圖對這兩個因素的相互影響進行了總結。一般而言,來自類庫的代碼馬上就能被使用,但它無法被直接修改。它只能通過間接的方式修改,或者使用C++模板,或者讓解決問題的代碼實現本文前面所提到的策略模式之類的東西。另一方面,模式所表示的東西可能僅僅是個思路(如“這個類只能具有1個實例”),它提供了最大的實現靈活性,但是對于程序員而言則需要大量的工作。
當然,這并不是一個基本的指導方針,每個人所面臨的情況可能各不相同。也許我們從類庫中所使用的類在自己的程序中位于相當低的層次,它的靈活性并不重要。例如,我們可能想自己設計一個集合類,包裝了類似list這樣的基本容器類。由于list類所提供的功能相當廣泛,因此即使我們必須對這個集合類的功能進行擴展,預計作為底層容器的list類也完全能夠勝任。在使用模式之前,也許過去已經實現了一個特定的模式,我們可以對以前所編寫的代碼進行適配,這樣就不需要創建太多的新代碼。
(圖:組件類型的靈活性與所需要的工作量)
在使用組件方面的經驗越豐富,對于選擇正確的組件就會有更大的自信。在積累足夠的經驗之前,可以把靈活性和所需工作量之間的權衡作為粗略的指導方針。對于每種特定的情況,可以提出下面這幾個問題:
能不能直接使用這個組件?還是需要額外的代碼才能讓它應用于自己的項目?
我是否確信已經從各個方面理解了問題,或者理解了與這個組件相關聯的問題,并且確定它在未來也不會發生變化?
通過選擇這個組件,是不是能夠擴展我的編程知識?
這些問題的答案可以幫助我們評估選擇某個組件所需要的工作量以及自己能夠從每種可能的方法中獲得多大的益處。
可復用組件選擇的實戰
現在我們已經理解了基本的思路,下面可以通過一個簡單的例子來說明具體的細節了。
對某些數據進行排序,但其他數據保持不變
一個項目要求我們對一個 studentRecord 對象數組按成績進行排序,但是存在一個難點:這個程序的另一部分使用 ?1 這個特殊的成績值表示那些無法移動記錄的學生。因此,盡管所有其他記錄必須移動,但那些成績值為 ?1 的記錄必須保留在原先的位置。最終所產生的結果是一個排好序的數組,但其間散布著一些成績值為 ?1 的記錄。
這是一個需要技巧的問題,我們可以嘗試用多種方法來解決這個問題。為了簡單起見,我們把選項減為 2 個:
選擇一種算法,例如像插入排序這樣的算法并對它進行修改,忽略那些成績值為 ?1 的 studentRecord 對象。
想出一種方法,用 qsort 庫函數來解決這個問題。
這兩個選擇都是可行的。由于我們已經熟悉了插入排序的代碼,在它的里面插入幾條 if 語句,顯式地檢查并跳過那些成績值為 ?1 的記錄應該不會太困難。讓 qsort 為我們完成工作就需要一些變通。我們可以把具有真正成績值的學生記錄復制到一個單獨的數組中,用 qsort 對它們進行排序,然后再復制回原先的數組,并保證在復制時不會覆蓋原先成績值為 ?1 的記錄。
讓我們對這兩個選項進行分析,觀察組件類型的選擇是怎樣影響最終代碼的。我們首先從算法組件開始,編寫經過修改的插入排序算法來解決這個問題。和往常一樣,我們將分幾個階段來解決這個問題。
首先,我們通過去掉 ?1 成績這個階段性問題來削減這個問題,對 studentRecord 對象數組進行排序時不考慮任何特殊規則。如果 sra 是包含了arraysize個studentRecord 類型的對象數組,它的代碼應該如下所示:
這段代碼與整數的插入排序非常相似。唯一的區別是它在執行比較時調用了 grade 方法 ?,另外還更改了用于交換空間的臨時對象的類型 ?。這段代碼能夠順利完成任務,但是對它以及本節后面的代碼段進行測試的時候,有一個需要警惕的地方:正如以前所編寫的那樣,studentRecord 類會對數據執行驗證,它不會接受 ?1 作為成績值,因此需要進行必要的修改。現在我們就可以完成這個版本的解決方案了。
我們需要讓插入排序忽略成績值為 ?1 的記錄。這個任務并不像聽上去那么簡單。在基本的插入排序算法中,我們總是在數組中交換相鄰的位置,如上面代碼中的 j 和 j – 1。但是,如果我們讓有些記錄的成績值保留為 ?1,那么需要與當前記錄進行交換的下一條記錄的位置可能相隔甚遠。
下圖用一個例子描述了這個問題。它顯示了最初配置下的數組,并用箭頭提示第 1 條記錄需要被交換到的位置,它們并不是相鄰的。而且,最后一條記錄(表示 Art)最終將從位置 [5] 交換到位置 [3] ,然后再從 [3] 交換到 [0],因此對這個數組排序所進行的所有交換都涉及到非相鄰的記錄(至少對于我們所排序的那些記錄是這樣的)。
圖:修改后的排序算法中需要被交換的記錄之間的任意距離
在考慮怎樣解決這個問題時,我設法尋找一個類比。我在處理鏈表的問題的選擇中找到了一個類比。在許多鏈表算法中,我們在鏈表遍歷時不僅需要維護一個指向當前節點的指針,還需要維護一個指向前一個節點的指針。因此在循環體結束的時候,我們經常把當前節點指針賦值給前一節點指針,然后再把當前節點指針指向下一個節點。
這個例子也需要類似的做法。當我們按照線性順序遍歷這個數組尋找下一條“真正的”學生記錄時,還需要追蹤前一條“真正的”的學生記錄。把這個思路投放到實踐中就產生了如下的代碼:
在基本的插入排序算法中,我們反復地把未排序的元素插入到數組中一塊不斷增長的已排序區域中。外層的循環選擇下一條需要被放到排序區的未排序元素。
在這個版本的代碼中,我們首先在外層循環體中判斷位置i的成績值是不是 ?1 ?。如果是,我們就簡單地跳到下一條記錄,保留這個位置不變。
當我們確定位置 i 的學生記錄可以被移動時,就把 rightswap 初始化為這個位置 ?。然后我們就進入內層循環。在基本的插入排序算法中,內層循環的每次迭代都把一個元素與它相鄰的元素進行交換。
但是,在這個版本的插入排序中,由于我們讓成績值為 ?1 的學生記錄保持不動,所以只有當位置 j 的學生記錄的成績值不是 ?1 時才執行交換 ?。
然后,我們在 leftswap 和 rightswap 這兩個位置之間執行交換并把 leftswap 賦值給 rightswap ?,如果還有要執行的交換就設置下一次交換。
最后,我們必須修改內層循環的終止條件。正常情況下,插入排序的內層循環是在到達了數組的前端或者找到了小于需要被插入值的元素的時候終止。在這個例子中,我們必須用邏輯或操作符創建一個復合條件,使循環能夠跳過成績值為 ?1 的記錄 ?。(由于?1小于所有合法的成績值,因此會永久地停止循環。)
這段代碼解決了我們的問題,但它很可能會散發出某種“不良的氣味”。標準的插入排序算法很容易理解,尤其是當我們理解了它的主旨時。但是,這個經過修改的版本就很難讀懂,如果我們想在以后還能看懂這段代碼,很可能需要添加幾條注釋。
也許可以對它進行重構,但我們先試試用其他方法來解決這個問題并觀察其結果。
我們所需要的第一樣東西就是一個在 qsort 中使用的比較函數。在這個例子中,我們將比較兩個 studentRecord 對象,并且這個函數將把一個成績值減去另一個成績值:
現在,我們就可以對記錄進行排序了。我們將分為 3 個階段完成這項任務。首先,我們把所有成績值不是 ?1 的記錄復制到第 2 個數組,元素之間沒有空隙。接著,我們調用 qsort 對第 2 個數組進行排序。
最后,我們把第 2 個數組的記錄復制回原來的數組,跳過那些成績值為 ?1 的記錄。最終的代碼如下所示:
盡管這段代碼的長度和前面那個解決方案差不多,但它更加簡明易懂。
我們首先聲明第 2 個數組 sortArray ?,它的長度與原先的數組相同。sortArrayCount 變量被初始化為 0 ?。在第1個循環中,我們將用這個變量追蹤檢查有多少條記錄已經被復制到第2個數組中。在這個循環的內部,每次遇到一條成績值不是 ?1 的記錄時 ?,我們就把它賦值給 sortArray 中的下一個空位置并將 sortArrayCount 的值增加 1。
當這個循環結束時,我們就對第 2 個數組進行排序 ?。sortArrayCount 變量被重置為 0 ?。我們將在第 2 個循環中用它追蹤檢查有多少條記錄已經從第 2 個數組復制回原先的數組。注意,第 2 個循環對原先的數組進行遍歷 ?,尋找需要被填充的位置 ?。
如果我們用其他方法來完成這個任務,可以嘗試對第 2 個數組進行遍歷,并把記錄推回到原來的數組。那樣,我們將需要一個雙重循環,其中內層循環搜索原先的數組中下一個具有真正成績值的位置。這是問題的難易程度取決于它的概念化層次的又一個例子。
比較結果
這兩個解決方案都可以完成任務并且都采用了合理的方法。對于大多數程序員而言,對插入排序進行修改并在排序時使部分記錄保持不動的第 1 個解決方案很難編寫和讀懂。但是,第 2 個解決方案似乎有些低效,因為它需要把數據復制到第 2 個數組并復制回來。
下面對這兩種算法進行簡單的分析。假設我們對 10,000 條記錄進行排序,如果需要排序的次數極少,那就無需太關注效率問題。我們無法確切地知道 qsort 所采用的底層算法是什么,但是對于通用目的的排序,最壞的情況是要執行 1 億次的記錄交換,最佳的情況只需要執行 13 萬次。
不管實際的交換次數是這個范圍內的哪個數字,來回復制 10,000 條記錄相對于排序而言并不會對性能產生非常大的影響。另外,還必須考慮 qsort 所采用的排序算法可能比我們所采用的簡單排序更為高效,這樣使第 1 個解決方案不需要把數據來回復制到第 2 個數組的優點也化為烏有。
因此在這個場景中,使用 qsort 的第 2 種方法要更好一點。它更容易實現、更容易讀懂,因此也更容易維護。并且,我們可以預期它的執行效率不遜于第1個解決方案,甚至更勝一籌。
第 1 個解決方案的最大優點是我們可以把學會的技巧應用于其他問題,而第 2 個解決方案由于過于簡單而缺乏這一點。
作為基本規則,當我們處在需要最大限度地提高自己的編程能力的階段時,應該優先選擇高層次的組件,例如算法或模式。當我們處在需要最大限度地提高編程效率(或者時間期限非常緊張)的階段時,應該優先考慮低層次的組件,盡可能選擇預創建的代碼。
當然,如果時間允許,可以嘗試不同的方法,就像我們剛剛所完成的那樣,這樣可以得到最大的收獲。
思考題
盡可能多地對可復用組件進行試驗,一旦掌握了怎樣學習新組件,將會極大地提高自己的編程能力。
對策略模式的一個反對意見是它需要暴露類的一些內部實現,例如類型。修改本文前半部分的“班長”程序,使策略函數都存儲在這個類中,并通過傳遞一個代碼值(例如,一種新的枚舉類型)來進行選擇,而不是傳遞策略函數本身。
考慮一個 studentRecord 對象的集合。我們想要根據學生編號尋找一條特定的記錄。把學生記錄存儲在一個數組中,根據學生編號對數組進行排序,并研究和實現插值搜索算法。
對于上面的問題,通過一個抽象數據類型來實現一個解決方案。這個抽象數據類型允許存儲任意數量的數據項,并可以根據鍵值提取單獨的記錄。對于能夠根據一個鍵值高效地存儲和提取數據項的結構,它用基本術語表示就是符號表,符號表思路的常見實現是散列表和二叉搜索樹。
本文節選自人民郵電出版社《像程序員一樣思考》第 7 章,部分內容有簡化,感興趣的讀者可以在各大書店購買。
人郵的公眾號「人郵 IT 書坊」長期提供最新 IT 圖書資訊,歡迎關注。
文描述了日常使用表單:購買復用分析單、設計自查單、代碼走查單的標準寫法。需要的可評論需要。
一、購買復用分析表
購買復用分析表
項目名稱 | 項目編號 | ||
編寫人 | 編寫日期 | 2013-3-6 | |
審核人 | 審核日期 | 2013-3-6 | |
批準人 | 批準日期 | 2013-3-6 |
構件標識 | 產品構件名稱 | 制作/外購/復用 | 理由 |
構件1 | 報表框架 | 復用 | 1、2、5 |
構件2 | 服務框架 | 復用 | 1、2、5 |
構件3 | 制作 | 4、6、10 | |
構件4 | 復用 | 6、15 | |
序號 | 影響決策的因素提示 | 說明 |
1 | 產品或服務提供的功能適合項目 | |
2 | 此資源和技能項目可以利用 | |
3 | 產品的質量 | |
4 | 對核心能力無負面影響 | |
5 |
*請認真填寫需求信息,我們會在24小時內與您取得聯系。