這是構建之法 《現代軟件工程》課的作業題之一。
下面的題目, 從簡單的命令行處理和數據處理開始開始,讓同學們逐步練習,鞏固算法,學會松耦合的設計,學會PSP,源代碼控制,單元測試,回歸測試,增量改進程序,等等。
第一步: 像《構建之法》的人物阿超那樣,花二十分鍾寫一個能自動生成小學四則運算題目的命令行 “軟件”。
具體要求:任何編程語言都可以,命令行程序接受一個數字輸入,然后輸出相應數目的四則運算題目和答案。例如輸入數字是 30, 那就輸出 30 道題目和答案。 運算式子必須至少有兩個運算符,運算數字是在 100 之內的正整數,答案不能是負數。 如:
23 - 3 * 4 = 11
擴展要求:看看小學三、四、五年級的四則運算要求, 逐步實現各個年級的難度。 寫博客紀錄自己實現每一個擴展的思路。 在擴展的過程種,你是獨立開發每個年級的運算題的功能,處理四年級的代碼和三年級的代碼沒有任何聯系,還是逐步把一些公共的代碼放在一起? 或者你用面向對象的方法,運用基類 (base class) 和子類來管理各種不同的需求和實現的細節?
第二步, 分別滿足下面的各種需求。
從這一步開始,我們要求學生填寫個人軟件流程 PSP 的耗時估計和實際時間的表格,要求把代碼簽入到源代碼管理服務器上。
下面這些需求都可以用命令行參數的形式來指定:
a) 一次可以出一千道道題目,並且沒有重復的,把題目寫入一個文件中。我們大家都知道,(1+2) 和 (2+1) 是重復的題目。 高級要求: 怎么嚴格定義題目重復呢,請看詳細題目要求
和同學們比較一下各自程序的功能、性能、實現方法的異同等等。
b) 當你有多於一個運算符的時候,如何對一個表達式求值?逐步擴展功能和可以支持的表達式類型,最后希望能支持下面類型的題目 (最多 10 個運算符,括號的數量不限制):
25 - 3 * 4 - 2 / 2 + 89 = ?
1/2 + 1/3 - 1/4 = ?
(6 - 4 ) * (3 + 28) =?
提示:很多學生在開始的時候用簡單的條件判斷來處理運算,如果只有一個運算符,那還是比較簡明的,運算符多了之后,怎么辦呢? 一些學生就用很多條件判斷來處理運算的優先級,這是某學生代碼片段:
public void jisuan(double a, string operation1,double b, string operation2,double c, string rightanswer)
{
bool aa = false;
if (operation1 == "+" || operation1 == "-")
{
if (operation2 == "*" || operation2 == "/")
{
aa = true;
}
else
{
aa = false;
}
}
else
{
aa = false;
}
if (aa == true)
...
那如果有 3 個運算符呢?怎么辦,繼續增加不同的 if/else 來解決?4個運算符呢?... 顯然這不是一個好的算法, 一個好的算法,即使問題變得更復雜,算法本身應該依然是簡明的, 並且程序本身的代碼量並不會變得異常復雜。 我們看看有什么好的數據結構能高效地表示四則運算。
請參考調度場算法:
http://hczhcz.github.io/2014/02/27/shunting-yard-algorithm.html
https://en.wikipedia.org/wiki/Shunting-yard_algorithm (中文版)
c) 除了整數以外,還要支持真分數的四則運算。 (例如: 1/6 + 1/8 = 7/24 )
d) 讓程序能接受用戶輸入答案,並判定對錯。 最后給出總共 對/錯 的數量。
e) 到目前為止,這個程序的界面都是中文的, 隨着這個應用大受歡迎,別的國家的用戶也要用,那么怎么能高效地讓這個 App 支持不同文字界面互換呢?你是在程序里面不斷插入 if ... else ... 來處理中英文,還是有高效率,可以擴展的辦法?這個程序最終會擴展為支持10種語言,而且每個語言的用戶需要符合他們文化的圖標。請問你還是用 if/else 來解決么?
例如:你的程序已經支持了兩種語言,並且中文界面有一個符合中國用戶期望的圖標,英文界面有一個符合美國用戶期望的圖標, 現在你要支持第三個語言 - 日文,並且需要一個符合日本用戶的圖標(假設是富士山的圖像)。 然后還有另外 7 個語言 (德國、法國、意大利、...)和不同的圖像要支持。那么,這可以用if/else 來解決, 還是有更高效的辦法?
當顯示文字直接放在代碼中,例如像下面這樣:
if (isEnglish) UIControl.setText("Welcome to my calculator app"); else if (isChinese) UIControl.setText("歡迎使用我的四則運算程序"); else if ...
這種用戶顯示的文字和代碼邏輯放在一起的設計,我們稱之為緊耦合。 程序員精通代碼邏輯,但未必精通各國語言,也不太會設計各種圖標,那我們會讓懂法語和德語的團隊成員(或者臨時請來的專家)直接在代碼上面改么?似乎他們會覺得代碼編輯器的界面好難掌握哦... 改錯了,程序編譯不過去怎么辦?我們可以想象,如果這些和邏輯無關的資源能夠放在一個單獨的地方, 這樣負責文字和圖像的人員(他們可能不太懂編程)能去修改,然后在主程序里,我們把各種語言的處理抽象為下面的操作:
UIControl.setText(GetStringByLanguage(language_Id, string_id); //根據當前顯示的語言,拿到適當的文字,然后顯示。
這樣會不會更好?邏輯代碼和現實的資源各自分開, 通過良好定義的渠道(language_id, string_id) 結合起來了, 這是一種松耦合。 請看同學的嘗試1, 嘗試2.
程序可以用很多辦法來提高效率保持簡明:抽象 (把復雜的操作抽象為一個名字/類/函數,把復雜的處理隱藏在內),循環(高效地把類似的事情做 N 遍),遞歸(在處理問題的時候,發現這個子問題也是類似的,那我就調用自己來處理),組合(把各個操作組合起來完成復雜任務)... 等
第三步,增加一個運算符,程序應該有怎樣的改變?不得不扔掉全部重寫么,還是可以只改部分模塊?
一些高年級的老師看到這個程序,希望讓他們的同學也來用,但是有一個新需求, 要支持乘方 (power) 運算,我們都知道,乘方運算的優先級高於乘除法。如何表示乘方, 有兩種表示方法:
1. 4 ^ 2 = 16, 4 的二次方等於 16。 這里, ^ 表示乘方
2. 4 ** 2 = 16, 4 的二次方等於16。這里, ** 表示乘方 (** 之間不能有空格,否則是錯誤的算式)
由於歷史的原因, 不同的學校用了不同的表示方法, 老師希望這兩種表示方法都要支持,可以通過設置來選擇。
第4步,每個同學選一個方向,把程序擴展一下:
a) 把程序變成一個 Windows/Mac/Linux 電腦圖形界面的程序 (取決於你目前使用的電腦),同時增加 “倒計時” 功能, 每個題目必須在 20 秒鍾完成,如果完不成,則得0 分並進入下一題。增加“歷史紀錄” 功能, 把用戶做題的成績記錄下來並可以展現歷史記錄。
b) 把程序變成一個智能手機程序 (你正在用什么手機, 就寫那個手機的程序), 增加倒計時,和歷史紀錄功能(見上)。
c) 把程序變成一個網頁程序, 用戶通過設定參數,就可以得到各種題目。
d) 選一個你從來沒有學過的編程語言,試一試實現基本功能。
估計做好這個軟件需要的時間,並且寫出大概的設計步驟和實現算法。
e)把這個程序的思路變成一個可以一步一步演示的步驟, 可以是命令行的輸出, 也可以是圖形界面:
輸入:一個正常的四則運算句子
輸出:程序用動畫表示分詞的過程,后序轉換的過程,處理不同運算符優先級的過程, 逐步算出得出結果的過程。
例如:輸入是 (6^2 - 4 ) * (3 + 28)
輸出是
(36 - 4 ) * (3 + 28)
32 * (3 + 28)
32 * 31
992
如果能做到這一步, 這個程序所牽涉到的 “知識點” 就能說 “精通” 了。
可以開始做相關的第二個作業。
第三步,程序理解和擴展
我們說了程序要能輸出不重復的題目,大家有不同的實現方法,那么能否比較一下各自的異同? 例如這個實現方法:
https://gist.github.com/vczh/2c058aed996effc0a519ed3d265a3eb5
1) 能否讀一下這個程序,注釋一下它的核心設計是什么,寫一篇博客分析一下?
2) 如果我們要讓這個程序增加一個運算 -- 乘方運算。 你要在現有程序中做什么樣的修改,才能讓這個程序比較優雅地實現這個新的需求。
=========================
其他題目:
- 四則運算練習
- 計算程序文件的行數 WC
- 電梯調度