哈工大18年春軟件構造課程討論題



這是哈工大18年春軟件構造課程(徐漢川老師)的討論題目,少部分答案摘錄自課件PPT和網上的資源(鏈接在文中給出)。如有錯誤還望指出,謝謝。


一、在軟件測試過程中,“測試用例的數目”、“測試的覆蓋度”、“測試的效率”三者之間存在一定的關系。簡要分析它們之間的折中性。


測試:
--> 在規定的條件下對程序進行操作,以發現程序錯誤,衡量軟件品質,並對其是否能滿足設計要求進行評估的過程。

測試用例:
--> 為某個特殊目標而編制的一組測試輸入、執行條件以及預期結果,以便測試某個程序路徑或核實是否滿足某個特定需求。

覆蓋度:
1.代碼覆蓋度
--> 基於代碼的測試覆蓋評測測試過程中已經執行的代碼的多少,與之相對的是要執行的剩余代碼的多少。
2.輸入空間覆蓋度
--> 參照模塊的規格說明,測試用例占總輸入空間的比例。

效率:
--> 成果(測試結果)/資源(測試時間空間)


綜上,我們的目的在於實現較高的測試效率,而測試用例的數目與資源成正比,與覆蓋度正相關,但與測試成果不成正比例關系,所以應該在保證測試成果的情況下減少資源的占用。即在保證測試效果(對於白盒測試來說,代碼覆蓋度也可以進行考量,但不是測試的本質目的)的前提下減少測試用例,例如使用輸入空間分區的策略。




二、閱讀《敏捷軟件開發宣言》,並闡述敏捷思想在傳統過程模型中為何無法做到?這些思想對軟件開發帶來的優勢是什么?


>>> 什么是敏捷思想?

  1. 個體和互動高於流程和工具:團隊應該自我進行組織和溝通,例如結對編程,並有明確的動機驅動開發。一個協作溝通的開發團優於一個孤立運行的專家團隊。

  2. 工作的軟件高於詳盡的文檔:一款工作的軟件能比一份厚重的文檔更能向用戶呈現開發結果。開發時最好對代碼做明智的注釋而不是靠詳細的文檔來解釋程序(文檔也會很快過時)。

  3. 客戶合作高於合同談判:由於軟件開發初期開發人員(甚至客戶)都很難知曉真正的需求,所以開發團隊應該直接接觸用戶或代理,從中獲取反饋,並在此基礎上不斷明確、改變需求和開發方向。

  4. 響應變化高於遵循計划:軟件開發的重點應該是相應市場變化而不是死板遵循合同。

>>> 傳統開發的特點

  1. 強調文檔對於團隊成員的指導作用,開發人員是在不知道項目細節的情況下進行開發的。
  2. 將開軟件開發過程分為可行性分析和項目開發計划、需求分析、概要設計、詳細設計、編碼、測試、維護七個階段,每個階段的輸出是下一個階段的輸入。

  3. 開發計划一開始就已經訂好,沒有大量同客戶的溝通交流。


>>> 敏捷思想在傳統過程模型中為何無法做到?

  1. 傳統軟件開發是一個由文檔進行驅動的過程。而敏捷思想強調以需求作為驅動。
  2. 傳統軟件開發是按照計划在線性模式下開發,開發人員必須要完成這個階段的任務,並編寫文檔,然后才能進入下一個階段。而敏捷思想強調迭代開發,每次迭代交付一些成果,關注業務優先級,檢查與調整。

  3. 傳統軟件開發的測試階段往往都是在整個代碼編寫完后才進行測試,假如在測試中發現問題,有可能要對整個模塊進行修改。而敏捷思想強調以測試作為開發前提。

  4. 傳統軟件開發中,開發者只在初期與客戶和市場交流。而敏捷思想強調隨時交流,持續反饋。

>>> 這些思想對軟件開發帶來的優勢是什么?

  1. 以人為本,強調客戶和開發團隊間的有效溝通和緊密協作,使得客戶需求得到滿足(可以等有價值的信息出現或對技術優化后才去決定)。
  2. 適應性較強,接受開發過程中需求的頻繁修改,能有效地應對市場需求的變更。
  3. 通過增量迭代,每次都優先交付那能產生價值效益“不完全”功能,及時搶占競爭激烈的市場。並最大化單位成本收益。




三、學校發布了手機網上預約系統的招標公告,你所帶領的小組獲得此項目的開發權。目前項目開發中存在以下實際情況:你所帶領的小組不是很熟悉手機系統的編程,並且對B/S和C/S結構的區別僅僅停留在書本上,缺少實際的開發經驗。項目時間非常緊迫,考慮到教師和學生都需要此項目提供的功能,因此校方希望能夠盡早的看到此項目的早期版本。校方提出了很多擴展要求,但是沒有被包含到需求陳述中,因此后期可能會針對系統做出大量的調整和更改。請從“瀑布模型”、“原型模型”、“增量模型”三種模型中選擇最適合此項目開發的過程模型,並簡述選擇原因。


項目要求:

  1. 時間緊,盡早看到早期版本。
  2. 后期用戶提出大量的調整和更改。
  3. 不熟悉開發,對deadline不清楚。

瀑布模型:
--> 線性,通過一系列確定過程完成項目,容易使用,但是開發后期的改動開銷會很大,同時無法拿出早期版本。不符合。

增量模型:
--> 線性,將項目分成各個小項目,逐次按照優先程度完成,但並不能在完成小項目以后看到一個早期版本,同時小項目開始開發后其要求就被“凍結”,無法從客戶獲得新的反饋。不符合。

原型模型:
--> 迭代,先構造一個項目原型,在該原型的基礎上,逐漸完成整個開發工作。即首先建造一個快速原型,實現客戶與系統的交互,用戶或客戶對原型進行評價,進一步細化待開發軟件的需求。通過逐步調整原型使其滿足客戶的要求,開發人員可以確定客戶的真正需求是什么;第二步則在第一步的基礎上開發客戶滿意的軟件產品。符合要求1和要求2,同時可以幫助開發人員確定deadline能否實現。


Ps.

缺少開發經驗,對開發平台不了解,項目時間緊迫,后續擴展要求多,依然帶領小組獲得了項目開發權

--> 典型的為了出題而出題,看着想打人。




四、在面向對象的設計原則SOLID中,“依賴轉置原則(DIP)”與“開放封閉原則(OCP)”之間有什么內在的聯系?


DIP譯為“依賴倒置”(inversion)似乎更合適。

以下摘錄改編自:設計模式六大原則


1.依賴倒置原則(Dependency inversion principle)

定義:高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。

解決方案:將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯系,則會大大降低修改類A的幾率。

依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是接口或者抽象類,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規范和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。

2.開閉原則( Open/closed principle)

定義:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。

問題由來:在軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有代碼經過重新測試。

解決方案:當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。

開閉原則就是想表達這樣一層意思:用抽象構建框架,用實現擴展細節。因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟件架構的穩定。而軟件中易變的細節,我們用從抽象派生的實現類來進行擴展,當軟件需要發生變化時,我們只需要根據需求重新派生一個實現類來擴展就可以了。前提是我們的抽象要合理,要對需求的變更有前瞻性和預見性。


綜上,實現依賴倒置原則的實現其實就體現了開閉原則,例如當我們想添加C類功能時,由於類A依賴的是一個抽象接口,我們只需要依照結構添加一個新的類C即可(對擴展開放),而不是改動類A的代碼(對修改關閉)。

Ps:
設計模式前五個原則,恰恰是告訴我們用抽象構建框架,用實現擴展細節的注意事項而已:單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱(實現效果),它告訴我們要對擴展開放,對修改關閉。




五、有三個開發者參與一個項目,A負責開發初始代碼,B負責修復bug和優化代碼,C負責測試並報告bug。項目的Git服務器為S,三人的本地Git倉庫已經配置好遠程服務器(名字均為origin)。項目的Git版本狀態如圖所示,三人的本地Git倉庫的狀態也是如此,其中包含主分支master,當前工作分支是master。

此時他們三人開展了以下工作:

a) A開發了某項新功能,他創建了分支b1並在該分支上提交新代碼,推送至服務器S;

b) C獲取了A的提交,並在其上開辟了新分支b2,在b2上撰寫測試程序並提交和推送至服務器S;

c) C在執行測試程序過程中發現了A的代碼存在問題,C將bug信息報告給B;

d) B獲取了C推送的包含測試程序的版本,在其基礎上開辟了一個新分支b3用於bug修復,當B確認修改后的代碼可通過所有測試用例之后,向Git做了一次提交,將b3合並到b2上並推送至服務器S;

e) C獲取B的修復代碼並重新執行其中包含的測試程序,確認bug已被修復,故將其合並到主分支master上,推送至服務器S,對外發布。

題目:

(1) 在圖上補全上述活動結束后服務器S上的版本狀態圖(需注明各分支的名字與位置);

(2) 寫出B為完成步驟d所需的全部Git指令,指令需包含完整的參數。


一般來說bug修復后分支會被刪掉,但這里並沒有明確是否刪除b3,所以用兩問中均以括號表示可能。另外這里使用Fast forward模式合並分支。

  1. 步驟如下:

git pull
git checkout b2
git checkout -b b3
.....修復bug
git add ./
git commit -m "fixed the xxx bug"
git checkout b2
git merge b3
(git branch -d b3)
git push

這里要注意的是,“將b3合並到b2”意味着 git checkout b2; git merge b3 而非 git checkout b3; git merge b2 ,這是由於b3的分支commit比b2更新,git不會允許你“回退”的,而是會提示“commit版本已是最新”。


如果想保留bug分支信息,可以采用非快速合並(git merge --no-ff):




六、比較static factory method、 factory method DP 和 abstract factory DP三者間的異同 abstract factory 和 builder bridge 和 strategy flyweight 和 pool


factory method DP 和 abstract factory DP:

factory method 和 abstract factory 形式的不同在於factory method是一個“method”,而abstract factory是一個對象接口。對於factory method來說,由於他是一個方法,所以可被子類型覆蓋(例如實現接口或繼承抽象類),從而將對象的實例化“延遲”交給子類完成,或者說使用者不需要關心子類的實現,而是關注對象的功能,通過調用工廠方法得到相應的對象。對於abstract factory對象來說,它不是“生產一個實例”,而是“生產一類組件的工廠”,亦即該對象中可以提供多個工廠接口,可以在不指定具體類的情況下提供關聯或者獨立組件。

它們之間的相同點在於不在將實例化的任務交給具體類本身的構造方法(new)完成,而是交給另外的工廠接口實現。

“static factory method”技術由Joshua Bloch在《Effective Java》中提出。其不是一個設計模式 —— “Note that a static factory method is not the same as the Factory Method pattern from Design Patterns. The static factory method described in thisitem has no direct equivalent in Design Patterns”。它的定義為“A class can providea public static factory method, which is simply a static method that returns an instance of the class.”從這個角度看,靜態工廠方法也是將類的實現放到了別的地方實現,只不過是靜態的,既無法在子類型里面覆蓋。另外,這里也強調了這種靜態方法返回的是靜態方法所在的實例化對象。例如Boolean類中的靜態工廠方法:

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

所以,課程PPT上給出的靜態工廠方法示例其實是使用了工廠方法模式的“變體靜態工廠方法”:

public class TraceFactory1 {
    public static Trace getTrace() {
        return new SystemTrace();
    }
}

說這里使用了工廠方法模式是因為它將Trace獨立到TraceFactory及其子類實現,說是“變體靜態工廠方法”是因為這里的靜態工廠方法不是返回的當前類(TraceFactory1)的對象,而是返回了一個Trace對象。


abstract factory DP 和 builder DP:

abstract factory注重於“在不指定具體類的情況下提供一個可以產生一系列相關或獨立組件的接口”,而builder注重於“將復雜對象的構造和其表示分離開來,並以此通過相同的構造過程獲得不同表示的對象”。既前者通過工廠產生的是組件,后者是通過一個builder逐步構建,最終獲得一個對應的可用對象。

兩者的相同點在於都沒有直接生成具體類型(new)。


bridge DP 和 strategy DP:

兩者的不同在於bridge DP是一個結構設計模式,而strategy DP是一個行為設計模式。其中bridge關注於“將抽象與實現解耦,以便兩者可以獨立變化”,而strategy關注於“對於一個操作,可以在運行時決定使用的算法或行為”。

由於兩者不是同一類設計模式,無法比較相同點,不過在使用strategy DP的時候,由於要運行時決定算法,其實現必須是與表示解耦的(而非直接使用具體的算法),這就使用到了bridge DP。


flyweight DP 和 pool DP(來自課件PPT):

原理不同:object pool的原理是復用,pool中object被取出后,只能由一個client使用;flyweight的原理是共享,一個object可被多個client同時使用。對象性質不同:object pool中的對象是同質的,對於client來說,可以用pool中的任何一個object。如:需要connection時可以從connection pool中任意的拿一個出來使用;Flyweight的每一個實例的內部屬性相同,但是外部屬性不同,是異質的。flyweight使用時,是去Flyweight-Factory中找一個特定的對象出來(如果沒有的話,就創建一個)。應用場合不同:object pool對應的場景是對象被頻繁創建、使用,然后銷毀。每個對象的生命期都很短。同一時刻內,系統中存在的對象數也比較小。Flyweight對應的情況是在同一時刻內,系統中存在大量對象。

相同點在於兩者都是讓多個索引(重復)利用同一個對象或同一個對象集合,提高內存的使用效率,減少GC。




七、結合自己之前所參與的某個軟件項目,闡述用戶提出哪些NFR,它們之間有何折中,以及你是如何應對的。


軟件項目(比賽的作品):“基於Apache Ignite的分布式醫療數據學習框架”
主要用戶:醫院
NFR:portability, performance, ease of use, security, maintainability, economy


折中及應對策略:

1.(performance && economy) vs. portability

>> 一開始用戶打算在數據收集端/感應器就建立學習框架(ARM架構),然后構建一個協調分配的中心,該中心負責數據的存儲和分配(x86架構)。但是在開發的過程中發現Apache Ignite在計算節點增加時其性能會顯著下降(很多資源用來處理任務協調),而數據收集端的數量在未來應該會變得比較多,同時分別在兩個框架部署也會增大工作量。經過考慮,我們認為性能和經濟(開發時間)更加重要,數據收集端只應完成數據收集的任務(不部署計算節點),而數據的整理和計算交給計算能力更強的中心負責。

-> performance && economy 100%

2.maintainability vs. economy

>> 由於用戶大多為醫護人員,而對於數據的維護工作也常常不能由開發人員進行,所以一個方便抽象的管理平台是必要的,但是這也會增加我們的工作量。經過考慮,我們認為如果維護工作不做好,該軟件的計算結果都有可能受到影響,即軟件的正確性難以保證,所以維護性必須認真對待。最后我們的方案是使用JavaFX開發出一套圖形界面管理平台,用戶只需要觀察平台上的情況和報告,通過經驗使用已經建立好的處理方法就能維護數據了。而JavaFX和Apache Ignite的解耦和也做的很好,可以分別同步開發。

-> 70% maintainability vs. 30% economy

3.security vs. ease of use

>> 患者的病歷中既有寶貴的醫療資料,也有關乎患者隱私的數據,所以對於軟件訪問的控制也是很重要的。為了達到合理的安全性,我們采取了權限分配、日志記錄、數據隔離、傳輸加密等措施。但是這些措施也對易用性產生了影響。通過考慮,我們認為隱私數據的保護和追責性是本軟件的一個重要底線,所以易用性應該很大程度上讓位於位於安全性。而對於易用性的補充,我們采取了秘鑰分發的措施,一定程度上增加了易用性。

-> 90% security vs. 10% ease of use




八、結合子類型和多態的知識,了解java中的協變和逆變:

1.數組的協變性

2.泛型中的協變和逆變

3.如何理解:函數f可以安全替換函數g,要求函數f接受更一般的參數類型,返回更特化的結果類型(輸入類型是逆變,輸出類型協變)


逆變與協變:如果A、B表示類型,f(⋅)表示類型轉換,≤表示繼承關系(比如,A≤B表示A是由B派生出來的子類):

f(⋅)是逆變(contravariant)的,當A≤B時有f(B)≤f(A)成立;
f(⋅)是協變(covariant)的,當A≤B時有f(A)≤f(B)成立;
f(⋅)是不變(invariant)的,當A≤B時上述兩個式子均不成立,即f(A)與f(B)相互之間沒有繼承關系。


1.數組的協變性

在Java中數組是具有協變性的,如果B是A的子類型,則B[]是A[]的子類型(f(⋅)映射為數組),即子類型的數組可以賦予父類型的數組進行使用,但數組的類型實際為子類型。例如:

Fruit[] fruits = new Apple[10]; // subclass of fruits
fruits[0] = new Apple();
fruits[1] = new RedFujiApple(); // subclass of Apple

這里fruits所引用的數組其實是Apple[]類型。

從協變數組讀取元素是完全安全的,無論是編譯期還是運行時,都不會發生任何問題:

Fruit fruit = fruits[0]; // return an Apple, which is the subclass of Fruit

但是將Fruit類型以及其子類型的元素寫入到協變數組fruits中是有可能在運行時出現問題的,因為Apple類型無接受Fruit類型和其它非Apple的子類型(編譯器無法檢查):

fruits[0] = new Fruit(); // java.lang.ArrayStoreException
fruits[0] = new Orange(); //subclass of Fruit, java.lang.ArrayStoreException

這是Java數組的“缺陷”,在利用數組的協變性時,應該盡量把協變數組當作只讀數組使用。


2.泛型中的協變和逆變

Java中泛型是不變的,但可以通過通配符"?"實現協變和逆變:

<? extends>實現了泛型的協變:
List<? extends Number> list = new ArrayList<Integer>();

<? super>實現了泛型的逆變:
List<? super Number> list = new ArrayList<Object>();

由於泛型的協變只能規定類的上界,逆變只能規定下界,使用時需要遵循PECS(producer-extends, consumer-super):

要從泛型類取數據時,用extends;
要往泛型類寫數據時,用super;
既要取又要寫,就不用通配符(即extends與super都不用)。


3.如何理解:函數f可以安全替換函數g,要求函數f接受更一般的參數類型,返回更特化的結果類型(輸入類型是逆變,輸出類型協變)

由LSP原則(Liskov Substitution Principle),即所有引用父類型的地方必須能透明地使用其子類型的對象。為了安全的替換替換函數g,我們需要用原有參數類型或其父類接受客戶的傳入,返回原有類型的子類。以此遵循對應的規格說明。

個人覺得這里並沒有“協變”與“逆變”這一說,因為沒有產生“類型轉換”。

Ps:如果說的是子類型覆蓋父類型方法時參數和返回類型的關系,在Java 1.5以及之后支持協變返回類型(covariant return type),即子類型方法的返回類型可以是父類型方法返回類型的子類型。但是參數類型還是必須一致。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com