寒假作業2/2


一、作業基本信息

這個作業屬於哪個課程 2021春軟件工程實踐W班
這個作業要求在哪里 作業要求
這個作業的目標 1.閱讀《構建之法》並提出五個問題
2.WordCount程序
其他參考文獻 《構建之法》、CSDN

目錄

閱讀《構建之法》並提問

1.書中的第六章講到了敏捷開發,

但幾乎整章的篇幅都在講“做法”,講敏捷流程的問題與解法,講怎樣打造敏捷的團隊,但我抱有疑問,為什么我要選擇敏捷?
在6.5的問答環節中也只是提到它是

前人經驗的總結,它被證明很有效果。

我不是很能接受這種說辭。而且查閱資料的時候我發現,作為開發人員,任何人都不會喜歡這種開發模式。

在表6-3中有提到:

敏捷開發適用於對產品可靠性不高、需求變化經常的場景。

這算是最貼近答案的一句話,但本質上只是在告訴你適用場景,依舊無法讓我信服所以我繼續思考:

我認為市場環境以需求為大,對於需求這一概念,有三個我們必須注意的問題:

  1. 如何滿足用戶不斷變化的需求?
  2. 如何真正滿足用戶的需求?
  3. 如何滿足不同層次用戶的需求?

敏捷開發確實很好的做到了第一點,但后面兩者呢?相對於傳統的瀑布+文檔開發流程,它到底有什么優勢?如果它在這兩方面沒有優勢,又該以哪個問題為依據來進行開發流程的選擇?
而且在看完16.4魔方的創新后,我更近一步地體會到對於目標用戶需求變化的把握很重要,而敏捷開發也確實很契合這種情景。但這個故事又激起了我新的疑問,我們對於需求的把握其實是存在一段空白期的,很多情況下我們並沒有充分的時間去了解目標用戶,在這種情況下連敏捷開發的優勢(上面提到的第一點)都無法發揮,也就是說有的情況雖說是適用敏捷開發,但由於各種各樣的情況導致在實踐過程中表現的不適用,這種情況該怎么選擇?

2.書中的16.3.2講到了動量和加速度的問題,提到了

關於PC桌面版和移動端的一個事例,前者收入不斷減少但還是遠大於后者,而后者雖然收入少但用戶量卻不斷增長,對於兩個平台的投入該如何權衡?

我乍看這個問題想到的觀點是:先繼續維護PC端,直到入不敷出了以后就停止運營,轉投移動端。這個方法很穩健,
但如果考慮兩個極端的話,可以發現兩個值得注意的問題:

如果在一開始就把PC端的所有資源都投入到移動端,可能能收獲多得多的回報,比如早一天上線,多吃一分國家政策的紅利,多搶占一份市場,現在很多擁有壟斷地位的公司都是這樣積少成多來的。

但如果突然宣布停運,不可避免的就是會傷害到用戶體驗。用戶是有黏性的,你家的PC端產品用得順手,在你移動端開發出來后自然會更傾向於使用你家的產品,但為了新產品而停運舊產品的話勢必會影響到口碑。
這里有一個很經典的例子,百度瀏覽器的電腦端在2019年4月發出公告宣布停運,5月正式停運,雖然有一個月的緩沖時間。但大部分用戶甚至都不知道有停運公告這一回事,而且在停運后出現了用戶發現收藏無法轉移至移動端,進一步加重了用戶的不滿情緒,以至於遷怒至移動端甚至百度產品,這個問題直到9月份才給出解決方案,但那時候已經人去樓空了。雖說百度家大業大,而且名聲本來就不好,但對於小公司如果遇到類似的情況,那將是毀滅性的打擊。所以很多企業也會選擇賠錢賺吆喝,但這吆喝對於公司的發展也是很重要的。

這其中各有各的利害關系,不怎么明白具體該如何取舍。

3.書中的16.4講了魔方的創新的故事,大概內容是

村里本來不知道魔方。
1.A從其他地方學會了魔方的公式,靠口頭傳授玩法和魔方銷售獲得了魔方大師的稱號。
2.B找出了新的魔方玩法,還承諾買魔方送口訣印本,打破了A的壟斷地位(技術競品)。
3.他們兩人競爭激烈,人們很快都學會了怎么玩魔方,規則無非是比誰扭得快,市場飽和,人們厭倦(執行力比較)。
4.C改變了玩魔方的規則,利用屁股扭魔方進行表演,但很快人們也厭倦了。(改變規則)
5.D甚至都不會玩魔方,但他直接改裝了魔方,通過給魔方貼上女生喜歡的貼圖來吸引女性用戶。(了解用戶)

其中提到的一個問題引起了我的思考,
我們應該如何保護我們的創新

現在的市場已經很成熟了,不管是哪個方面都趨近飽和,所以不管是哪種新技術都會有人趨之若鶩,技術門檻不夠高,那就必然會有競品出現,哪怕是申請了專利,別人換個皮也經常很難告他侵權。所以為了保護創新,想要消滅競爭對手可以說是很不實際的。
A只是將其他地方的技術帶來,對於這個村來說,確實是種創新,但這種創新卻是無法長久獨立保持下去的,它可以輕易地被復制,對於A來說,所謂的創新對他而言不過是一個沒有拓展的公式。
而我認為,想要保護創新,最重要的是從公式入手,深挖出的藏在公式背后的原理,這些對公式的理解才是核心技術,可以利用它簡化玩魔方的步驟,提高自己的競爭力,打造屬於自己的品牌。說到保護一般我們都是想到藏着掖着,但想要保持技術上的與時俱進,終究不能閉門造車,要想讓創新保持生命力,就不能拒絕與人分享和探討新技術。而且可以看到創新只是一時的創新,人們終有一天是會厭倦的,不能讓功利上狹隘之見切斷行業生命力,比起壟斷一時掙快錢,更該注重的是推動行業進步的價值觀。
當然我清楚我的想法過於理想,現實情況往往是內卷過度,人人自危,你向別人分享探討技術可能只是在給競爭對手送溫暖罷了。所以我就困惑這種橫豎都是虧的局面該如何找到妥協點?

4.依然還是魔方的創新的事例,在后面提到了另一個問題——

市場飽和時后來者如何取得建立優勢?
我對這里與其說有所疑問,不如說有些想補充的觀點,想請教一下老師和助教是否有更多的見解。
文中提到市場飽和狀況下競爭者的三個選擇,可以幫助后來者取得優勢:

1.新找一個不知道魔方的村去賣魔方。
2.利用自己其他優勢,將魔方銷售捆綁在優勢項目上。
3.開發有差異化的新東西,體現獨特的價值。

我對文中C和D的行為分析如下:兩者都是利用了第三點。C打破了外界對某產品的定式思維,不再認為產品只存在一種使用方法,D則是將產品的使用范圍擴充到身為看客的女生身上,她們不玩魔方但卻看魔方表演 ,與魔方實際上是存在間接的關系的,D實際上很好的把握了潛在的目標用戶。
“如何取得優勢”是將后來者放在一個劣勢的立場上的,而在我看來,其實后來者本身自己就存在有優勢:

  • 市場飽和意味着某項技術已經趨近成熟了,這意味着有充足的學習資源,入門的門檻降低了,哪怕學習的不是最新的技術,也比從零開始研發要輕松,畢竟改良型創新往往是比顛覆性創新容易的。
  • 市場飽和表示用戶對這種產品已經有了一定的認識基礎,相比於從零開始宣發一個新概念,站在巨人的肩膀上所需的成本是肯定要小得多的。
  • 可以不踩別人踩過的彎路,可以根據別人目前的問題進行改進。根據我查閱的資料,很多成熟的公司中各個部門其實是相互掣肘的,很多產品存在的問題不是不知道,而是過不了內部的排期和部門間的KPI扯皮,產品經理就算知道了問題,設計了解決方案,但內部的研發資源卻是優先的,正常情況下各個部門就守着這一畝三分地,一個小小的改動可能就要更改整個業務流的流程,降低整個業務的效率,提高業務復雜度,這種事你同意其他部門也不同意,所以才產生了很多滴滴、美團那種“不出事就不整改”的案例。而后來的新企業就沒有這種桎梏,從產品設計之初就納入考慮范圍,這就是很大的優勢。
  • 正如文中體現的,市場飽和有時也意味着用戶對於當前的產品已經存在厭倦心理,所以對於“創新”這一概念的包容度其實會比正常情況下要大的,只要稍微做出一點創新成果就能收獲不錯的成效。

5.書中的17.6提到了績效管理,書中對於個人在團隊中績效提到

可以用二維的評價體系:完成任務維度團隊貢獻維度

但我認為這種評價體系過於理想化了,對於是否擁有更完備的方法抱有疑問。
在查閱資料后發現,大項目往往是由PM分成若干小部分,然后由部門主管分配給個人,在工作的過程中經常會遇到諸如對接沖突等不可控因素導致項目進度受阻,這時對於任務完成度和團隊貢獻度的評判會變得非常主觀。
按我的理解,績效評估的目的本身就在於激勵員工持續做好的、更大的貢獻,影響員工工作效率的內部驅動因素就是對工作的使命感,讓員工了解自己工作的結果是否有意義,如果不能客觀地評價員工的貢獻,那就喪失了績效評估的本意。

冷知識與故事

Ada Lovelace為Charles Babbage的分析機寫了一個計算伯努利數的算法實現,因此被后世公認為是世界上第一個程序員。實際上,分析機由於其設計思想過於先進,在當時根本沒有被制造出來,也就是說實際上並沒有任何計算機能夠用來運行她的程序。
后來的企業架構師們重新吸收了她的這個技能,用來學習如何更好地使用UML進行編程。(這是在諷刺現在的某些“軟件架構師”頂多只會紙上談兵地畫畫UML。) 編程史趣事

二、WordCount程序

項目地址

Github

PSP表格:

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划 20 20
• Estimate • 估計這個任務需要多少時間 20 20
Development 開發 870 865
• Analysis • 需求分析 (包括學習新技術) 60 120
• Design Spec • 生成設計文檔 20 30
• Design Review • 設計復審 30 45
• Coding Standard • 代碼規范 (為目前的開發制定合適的規范) 20 20
• Design • 具體設計 100 240
• Coding • 具體編碼 500 240
• Code Review • 代碼復審 20 20
• Test • 測試(自我測試,修改代碼,提交修改) 120 150
Reporting 報告 110 120
• Test Report • 測試報告 30 20
• Size Measurement • 計算工作量 20 10
• Postmortem & Process Improvement Plan • 事后總結, 並提出過程改進計划 60 90
合計 1000 1005

解題思路描述

  1. 程序IO模塊:
    要求從文件讀入數據,向文件輸出數據。
    考慮封裝成兩個方法,給輸入方法提供【輸入文件名】,輸出文件內容的字符串,這樣計算模塊只需要關心字符串是啥就行了;給輸出方法提供【要輸出的字符串】以及【輸出文件名】。
  2. 程序的計算模塊共要求四項功能,我將其分為了以下幾個部分:
    • 統計字符數:
      將傳入的字符串先轉成char[],然后判斷是否為ASCII字符,如果是,則字符數計數+1
    • 統計行數:
      最重要的就是區分是否為無效行,由於題中要求含有空白符的也算無效行,所以選擇使用的正則表達式,只要正則表達式沒錯就沒問題。
    • 統計單詞數和詞頻:
      想到詞頻就想到了使用Map,鍵為單詞,值為單詞數。
      統計單詞數和計算詞頻都需要先將文本分割成一個個有效單詞,所以考慮在分割文本時既記錄單詞數,同時維護Map。
      分割單詞和判斷單詞合法也使用正則表達式。
    • 獲得頻率前十的單詞:
      考慮對Map進行排序,獲得前十的entrySet。

代碼規范

代碼規范

接口設計與實現

將程序分為兩個文件,

  • WordCount.java :處理文件IO、主函數所在文件
  • Lib.java : 專職於處理字符串的計算核心

項目結構:

具體實現:

I/O部分:

使用擁有緩沖區, 具有較高性能的BufferedReader和BufferedWriter來進行文件的存取。
選用BufferedReader的read()而非readLine()能夠保證\r\n等字符不丟失。
考慮過要讀取的字符串大小大於緩沖區大小(8M)的情況,去查閱了資料並翻了下源碼發現BufferedReader並不需要擔心這個問題(實際上測試的時候使用了80M的文件也沒有出問題,想要知道答案的話有時不必看源碼,自己測測就知道了),以下是我對源碼的解讀:
read()讀取部分的源碼如下:

for (;;) {
    if (nextChar >= nChars) {
        fill();
        if (nextChar >= nChars)
            return -1;
    }
    
    return cb[nextChar++];
}

畫成流程圖可以如下所示:

由源碼得知nChars和nextChar是類成員變量,初始值為0,我們可以進一步將問題簡化:

什么時候fill()完還有會有nextChar >= nChars?

其中調用的fill()方法的作用是從底層輸入流填充字符到緩沖區。在fill()方法的內部,有個這樣的代碼:

//每次調用fill時dst重置為0
do {    
    n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
if (n > 0) {    
    nChars = dst + n;    
    nextChar = dst;
}

其中cb是BufferedReader的內部緩沖區,n表示成功從底層流讀入緩沖區的字符數,n最大值為緩沖區大小。
可以看到,假設我們讀取一個超大文件,除了最后一次外的n值都會是緩沖區大小,最后一次會讀到輸入流尾,n等於剩余那部分的大小。再接着讀,因為輸入流此時已經為空,n就會等於0,此時就不會進入if(n>0),也就意味着nChars和nextChar的大小會保持上一次讀取的最終狀態,也就是它們相等,由此造成fill()完還有會有nextChar >= nChars的情況。
綜上可以看出,只有當輸入流的所有內容都讀完read()才會返回-1,否則無論緩沖區多大都會一直讀取。

同時考慮到read()調用的次數較多,所以使用StringBuilder來拼接字符串,不必像String一樣不斷創建銷毀。
readFromFile()返回文件內容,供給Lib模塊進行字符串處理。
writeToFile()只是個純粹的輸出字符串的工具,參數給什么就輸出什么。

統計字符數:

與解決思路中說的一樣,使用字符數組,能夠針對單個字符進行合法性的驗證。
當然,題目中后來補充說明了沒有漢字,所以其實也可以直接用str.length()替代。

//str是文件內容字符串
char[] ch = str.toCharArray();
for(int i=0;i<ch.length;i++){
    if(ch[i] <= 127){        
        count++;    
    }
}

統計有效行數:

使用正則表達式對字符串進行匹配,每找到一個有效行就增加技術,思路很簡單,主要是正則表達式的正確性要保證。
java的字符串匹配我較為陌生,去查了一下,
Pattern.compile()會對作為參數的正則表達式進行編譯,返回Pattern類,可以把它當做正則表達式的對象。
matcher()相當於把正則表達式與某字符串關聯(匹配),返回的就是一個匹配器matcher。
匹配器常用兩個方法:matcher.matches()和matcher.find()
前者將字符串與整個正則表達式匹配,成功則直接返回true。分割完單詞數組后,對單詞一個個地進行單詞合法性判斷的時候其實就可以使用這個方法。但實際上,String本身就自帶matches()方法,所以后面統計單詞時我就沒有用matcher.matches()。
后者第一次調用的時候會找到第一個匹配項,而后在每次調用find()時都會在上次find()的結束位置繼續開始搜索。可以看到這個性質很符合我們的要求——給定一個字符串,搜索所有滿足正則表達式的子串。
正則表達式的構造取了些巧,並不是只匹配有效行,而是利用一前一后的兩個\\s* 將無效行吞並入有效行,當做有效行的一部分。

//private static String LINE_REGEX = "\\s*\\S+\\s*\n";
Matcher matcher = Pattern.compile(LINE_REGEX).matcher(str);
while(matcher.find()){
    count++;
}

統計單詞數:

在最開始先將文本分割成一個個單詞。
原本打算使用效率更高的StringTokenizer,但它不支持正則表達式。由於題中要求的分隔符並不唯一,所以想要使用的話還得先把字符串中的分隔符全都替換成一種,生成新字符串又有額外的開銷,而且java新規范也不提倡使用StringTokenizer,所以就直接用split了。

由於分割單詞是個比較耗時的行為,所以為了提高性能,考慮在分割文本時既記錄單詞數,同時維護記錄着詞頻的Map。
具體實現時考慮到

  1. 題目中的排序要求較復雜,先按value排序,同頻詞還要用字典序排序,
  2. 對於沒有排序需求,單單要求統計單詞數的情況,使用TreeMap只會導致排序這一耗時操作很多余。

綜上,沒有使用有序的TreeMap,而是選擇使用了性能較好但無序的HashMap,到統計詞頻的時候再進行排序操作。


//private static String SPLIT_REGEX = "[^a-zA-Z0-9]";
//private static String WORD_REGEX = "[a-zA-Z]{4,}[a-zA-Z0-9]*";
String[] temps = str.split(SPLIT_REGEX);
for(int i=0;i<temps.length;i++){
    if(temps[i].matches(WORD_REGEX)){
        //空間換時間, 創建一個臨時對象存儲小寫單詞, 不然toLowerCase()兩遍, 對於很長的單詞會降低效率.
        String word = temps[i].toLowerCase();
        if(!hashMap.containsKey(word)){
            hashMap.put(word, 1);
        }else{
            hashMap.put(word, hashMap.get(word) + 1);
        }
        count++;
    }
}

獲得詞頻前十的單詞:

考慮到程序的擴展性,將要顯示的單詞個數作為方法的參數,想要獲得詞頻前十只需要傳入參數10就行。
采用java8新引進的流(Stram API)來進行排序。
傳統java對集合的操作,如果業務邏輯處理復雜的話很可能需要大量的迭代器進行幫助,操作相當繁瑣。
而流是專門用於集合復雜操作的工具,使用流可以使得代碼簡化,性能提高,對於題中的排序要求,更是只需使用現成的比較器就行。
使用comparingByValue可以按value排序,使用Comparator.reverseOrder()比較器構造,即可實現按值升序。
使用thenComparing指定排序的第二優先級,而Map.Entry.comparingByKey()就是按key排序,而且就是字典序。

Map<String, Integer> map = hashMap.entrySet().stream()
                .sorted(Map.Entry.<String, Integer>comparingByValue(Comparator.reverseOrder())
                        .thenComparing(Map.Entry.comparingByKey()))
                .limit(k)
                .collect(Collectors.toMap(Map.Entry::getKey,
                        Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));

性能改進

考慮性能時分析了程序的消耗較大的一些因素:
1.分隔方式的效率。
    到底是先全部將分隔符都轉為一種(比如說空格),然后用更高效率的StringTokenizer?還是直接利用split(正則表達式)進行處理?
最終選用了后者,理由見上文具體實現部分。
2.IO效率。選用最簡單的FileWriter?還是選用緩沖流BufferedWriter?還是具有更高讀寫性能的MappedByteBuffer(NIO)?
    其實就是在后面兩者中選擇,但由於不了解NIO,也不太明白MappedByteBuffer的使用注意點,即使查了資料還是看的雲里霧里,為了穩妥起見還是選用了后者。
3.排序算法效率,選用java8的流式?還是利用集合類中自帶的sort方法?
    最終選用了前者,理由見上文具體實現部分。
4.文件存取方式:Lib模塊的各個方法到底參數為文件名還是文件內容?
    最終是將Lib設計成一個只用來處理字符串的模塊,文件的存取交給調用該模塊的程序(即WordCount.java),這樣能保證程序一次運行只會讀取一次文件,在進行大文件的讀取時能較大提高性能。
5.字符串拼接:
    由於大量字符串使用運算符拼接的時候會一直創建新的對象,導致浪費大量內存和性能,所以使用Stringbuffer來代替簡單的String拼接。

最終性能測試結果:
10000個亂序單詞:

100000個亂序單詞:

1000000個亂序單詞:

10000000個亂序單詞:

單元測試:

使用JUnit4測試。
分成兩類:
一是字符、有效行、單詞數測試,使用Assert.assertEquals()將輸出結果和預期的正確結果相比較看測試是否通過,方法大同小異,這里就貼一種出來。

/**
 * 測試charsCount方法
 */
@Test
public void testCharsCount() {
    String str = WordCount.readFromFile("input.txt");
    Lib lib = new Lib(str);
    Assert.assertEquals(250, lib.charsCount(str));
}

二是Map排序測試,通過輸出排序后的Map,觀察是否和預期的一致來判斷。

/**
 * 測試getTopK方法
 */
@Test
public void testGetTopK() {
    String str = WordCount.readFromFile("input.txt");
    Lib lib = new Lib(str);
    Map<String, Integer> map = lib.getTopK(10);
    for(Map.Entry<String, Integer> entry : map.entrySet()){
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

正確性測試用例包含了如下情況(已全部通過):
空文本,
空行,只有空白符的無效行,只有分隔符的有效行,普通有效行,
空格作為分隔符, 非空白字符作為分隔符,
少於四位的非法單詞,
只有四位的合法單詞, 只有四位的非法單詞
多余四位的合法單詞(前四位為字母),多余四位的非法單詞(前四位包含數字)
全大寫單詞,全小寫單詞, 首字母大寫單詞,首字母小寫單詞,大小寫混雜單詞
先存入字典序靠前的同頻詞,先存入字典序靠后的同頻詞,
文件單詞數少於k個(測試時指定k為10), 文件單詞數大於k個。

項目覆蓋率如下圖所示:

覆蓋率優化方法:
1.減少不必要的判斷,盡量少用多if判斷;
2.消除重復代碼,重復代碼會導致優化覆蓋率時重復工作增加。
3.構造異常用例,覆蓋到異常處理的路徑。

異常處理:

Lib中由於將文件存取分離了,所以不存在異常處理。
WordCount負責文件的存取,所有異常均為I/O異常,比如FileInputStream的構造時如果指定的文件不存在,則拋出FileNotFoundException,而BufferedReader的read()和close()則會拋出IOException。處理方式就是捕獲+異常信息輸出。

心路歷程與收獲:

整個項目花費的最多的時間在安排項目架構和選用具體實現上。
在耦合度、性能、可移植性三個方面的取舍花了大量的時間,查了各種各樣的資料,尤其是緩沖、HashMap、Stream API、正則表達式,找不到資料的地方還要去翻源碼。
而實際上,安排好了項目架構、各種功能及其具體實現方式后,具體的編碼階段就十分輕松。

收獲:

  • 學會了git、github的相關知識,對於版本控制等有關項目管理的知識有了新的認識。
  • 學會了利用JUnit進行單元測試。
  • 學會了通過查看覆蓋率進行代碼的針對優化。
  • 對Java各知識點有了更深入的了解。
  • 提高了查詢資料的能力,在查詢資料時發現了許多不錯的資料查詢方法、平台。
  • 學會了正則表達式的使用,以前只知道有這種很好用的東西,現在可以自己編寫了。
  • 根據《碼出高效_阿里巴巴Java開發手冊》結合自己的編程習慣,規范了自己的代碼風格。
  • 對文字編碼、不同系統的回車、命令行輸入等項目遇到的其他知識點有了了解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM