Trick and Magic(OO博客第二彈)

  代碼是設計,不是簡單的陳述。而設計不只要求功能的正確性,更注重設計風格和模式。java

  真正能夠投入應用的程序設計,不是那種無腦的「黑箱」,超巨大的數組,多重循環暴力搜索,成噸全局變量……事實上,在實際應用中更重要的是權衡兼顧功能,性能,可讀性,魯棒性等等方面,而最終完成一個綜合的工程。咱們真正作的事是程序設計,而不是無腦地寫代碼。git

  在本次OO三次做業的週期裏,我逐漸開始接觸了多線程併發程序和真正工程化的模板設計,歷經瘋狂Google--爆肝寫碼--艱難讀碼的過程後,我從中發現了許多許多很是tricky的東西,但願能把它們總結出來並與你們分享。github

(1)從增長邏輯到增長數據正則表達式

  當你遇到一系列功能性的名詞時,你會怎麼辦,例如IFTTT做業中的四種觸發器:modified,renamed,size-changed,path-changed。我想不少人的第一反應是下面這樣:數據庫

/*
* str表明從輸入中獲取到的字符串,數字對應各類觸發器類型
*/
if(str.equals("RENAMED")
    return 0;
else if(str.equals("MODIFIED")
    return 1;
else if(str.equals("SIZE-CHANGED")
    return 2;
else if(str.equals("PATH-CHANGED")
    return 3;
else
    return -1;

if-else加數字變量,簡單粗暴,迅速解決,可是當整個程序變的很大時,或者多我的合做完成工程任務時,這樣的方法會致使可讀性和協做性很是差。編程

因而乎,稍微動動腦,又出現了另一種改進版:數組

/*
* str表明從輸入中獲取到的字符串,四個變量均初始化爲false;
*/
if(str.equals("RENAMED")
    renamed=true;
else if(str.equals("MODIFIED")
    modified=true;  
else if(str.equals("SIZE-CHANGED")
    size-changed=true;
else if(str.equals("PATH-CHANGED")
    path-changed=true;
else
   throw new XXXException(str);

  這種作法初始化四個boolean類型的變量,而後在if-else中對應改變他們的值,四個變量使用實際意義命名,達到了不錯的效果。安全

  可是更往深層次想想,假如我每一個if分支裏不僅僅是一條語句,或者有新的觸發器增長,即增長需求,那這種方法須要增長一個變量定義,增長一路分支,再去完善新分支中的內容。這樣每次都增長邏輯分支,有沒有更好的方式呢?多線程

  固然是有的。不過首先,咱們要明確一下程序中添加邏輯和添加數據的區別。添加邏輯和數據的方式是不同的,成本更是不同的。用一句話總結,就是添加數據是很是簡單,低成本和低風險的,而添加數據是複雜,高成本和高風險的。下面是一個添加數據方法,即表格驅動法的例子,我使用出租車做業中出租車的四種狀態爲例:併發

package enums;
import java.util.HashMap;
import java.util.Map;
//該做業中關於出租車狀態的輸入輸出都是以數字形式,因此這裏的鍵值使用數字字符串。 public enum TaxiStatus { SERVICE, ORDER, WAIT, STOP; private static Map<String,TaxiStatus> taxi_map=new HashMap<>(); public static void initialize(){ taxi_map.put("0",TaxiStatus.STOP); taxi_map.put("1",TaxiStatus.SERVICE); taxi_map.put("2",TaxiStatus.WAIT); taxi_map.put("3",TaxiStatus.ORDER); } public static TaxiStatus getValueOf(String str){ return taxi_map.get(str); } public static boolean inMap(String str){ return taxi_map.containsKey(str); } }

  經過使用枚舉enum和Map將輸入形式和枚舉類型映射起來,創建了一一對應關係。每次在判斷輸入信息時調用inMap和getValueOf方法便可,如爲false則說明輸入不合法,拋出相應異常,若是輸入正確則返回對應的枚舉名。每次新增需求,僅僅須要增長枚舉類型中的數據和映射關係便可。

if(inMap(str))
    return TaxiStatus.getValueOf(str);
else
    throw new XXXException;

注:因爲本人java萌新,因此對於enum和map的使用還比較初級,因此表達的可能比較複雜。對於表格驅動法,最簡單的例子就是字典。

  從添加邏輯到添加數據的優勢以下:

  1.將代碼中的數據部分和邏輯部分分割開來,使整個程序設計一目瞭然。

  2.對於需求更新甚至是全新的需求有着很是強的適應能力,每次僅需修改數據部分。

  3.測試時,只要數據正確就不用測試程序自己的正確性,而添加邏輯必須得再進行測試。

  4.添加數據法,或者說相似這樣的代碼能夠重用於各類各樣的場景下,而邏輯只能用於其所處的具體語境下,換句話說就是寫死在程序中。

  5.若是是在大型系統中,添加數據僅僅須要任意人員填寫一個表單請求將新的數據加入數據庫便可,而邏輯修改必須須要專門的開發人員來處理。

  6.添加數據的方式強制限定了代碼的風格,任何人添加數據必須遵照已經定義在數據存儲容器中的模式,而添加邏輯有不少能夠自定義的空間,在多人合做時容易產生問題。

 

(2)輸入處理時的小魔法(正則技巧和自定義異常類)

  談起輸入處理,正則表達式就是必不可少的一環了。主流的處理方法有兩類:

  1. group法

String regex = "(IF) (.+) (renamed|modified|path-changed|size-changed)"
            + " (THEN) (recover|record-summary|record-detail)";
Matcher matcher = Pattern.compile(regex).matcher(s);
//中間省略錯誤處理
path = matcher.group(2);
trigger = Trigger.parse(matcher.group(3));
task = Task.parse(matcher.group(5));

  2.spilt法

if(input_line.matches(regex)){
    String[] part=input_line.split("[|]");
    String filename=part[1];
    String trigger=part[2];
    String mission=part[4];
}

  這兩種方法有一個尷尬的地方就是隻有開發者在寫代碼的當天知道數組下標1,2,4或者group參數2,3,5的表明含義,一旦時間過去好久或是輸入格式變化,再次進行修改更新就很麻煩。而事實上正則表達式中有一種給每一個匹配部分打上「標籤」的方法,經過這種方法將實際含義做爲標籤,瞬間解決了相關問題。

String INPUT_FORMAT= "\\[(?<id>.*),(?<src>\\(\\d+,\\d+\\)),(?<dst>\\(\\d+,\\d+\\))\\]";
Pattern INPUT_PATTERN=Pattern.compile(INPUT_FORMAT);
Matcher mc=INPUT_PATTERN.matcher(input);
    if(mc.find()){
       String identifier=mc.group("id");
       String src_str=mc.group("src");
       String dst_str=mc.group("dst");
}

  這段代碼的關鍵點即在正則表達式中使用"(?<標籤名>匹配內容)"這樣的格式來進行匹配,而對應group的下標就是各個標籤名,這樣的對應一目瞭然,也易於添加和修改。

 

  輸入處理部分,是一個難度不大可是狀況複雜的部分。因爲須要判斷的狀況不少,對於每一種不合法的輸入狀況又得有專門的處理,因此稍不注意就會造成好幾層循環嵌套分支的局面,看起來十分複雜,在做業初期一個inputHandler方法寫到七八十行是常有的事,不少人都會陷入以下模式:

if(condition1)
    do something
if(condition2)
    do something
//省略各類狀況
if(condition2333)
    do something

  如今,就要隆重推出咱們的異常類大法了!!!首先是我出租車做業的異常類繼承層次圖:

   首先,在每一個異常類中,定義每種狀況對應的處理方式和信息反饋,例如:

package exceptions;

public class SameSrcDstException extends InputFailedException{
    public SameSrcDstException(String src,String dst,double time){
        super(String.format("#Same Src and Dst:%s  %s  %f",src,dst,time));
    }
}

  其實,在輸入內容的具體解析方法中,根據不一樣的狀況,拋出對應的異常:

    public static TaxiRequest inputParse(String input,double time)throws InputFailedException {
        input=InputHelper.removeSpace(input);
        Matcher mc=INPUT_PATTERN.matcher(input);
        if(mc.find()){
            String identifier=mc.group("id");
            String src_str=mc.group("src");
            String dst_str=mc.group("dst");
            String[] src_spilt=src_str.split(SPILT_FORMAT);
            String[] dst_spilt=dst_str.split(SPILT_FORMAT);
            int src_x=Integer.parseInt(src_spilt[1]);
            int src_y=Integer.parseInt(src_spilt[2]);
            int dst_x=Integer.parseInt(dst_spilt[1]);
            int dst_y=Integer.parseInt(dst_spilt[2]);
            if(rangeJudge(src_x,src_y)){
                throw new OutLocationException(src_str,time);
            }
            if(rangeJudge(dst_x,dst_y)){
                throw new OutLocationException(dst_str,time);
            }
            if(sameSrcDst(src_x,src_y,dst_x,dst_y)){
                throw new SameSrcDstException(src_str,dst_str,time);
            }
            return new TaxiRequest(identifier,new Point(src_x,src_y),
                    new Point(dst_x,dst_y),time);
        }
        else{
            throw new InvalidInputContent(input,time);
        }
    }

  最後,在輸入處理的全局範圍中,使用try-catch進行捕捉,調用相關異常類的方法進行處理,一套清晰完整高效的輸入處理流程就構建起來了。

    private void inputRequest(){
        Scanner input=new Scanner(System.in);
        while(input.hasNext()){
            String input_line=input.nextLine();
            if(InputHelper.isEND(input_line))
                break;
            try{
                TaxiRequest request=InputHelper.inputParse(input_line,time);
                quene.add(request,start_time);
            }catch (InputFailedException e){
                System.out.println(String.format("#Failed:%s",e.getMessage()));
            }
        }
        input.close();
    }

(3)線程安全的迷惑點

  關於線程安全,每一個人都有這樣一個問題,哪些類是線程安全的?而我能夠不負責任的告訴你,全部類都不是線程安全的!!!

  相信不少人都發現了做爲隊列用容器ArrayList和Vector的問題,其中Vector通常被稱做線程安全類,這是爲何呢,經過對比二者的源碼,緣由很明顯,就是一個synchronized的問題,Vector中可能會產生線程安全問題的方法都加了synchronized進行修飾,而ArrayList則否則:

public synchronized int size() {
        return elementCount;
    }
public int size() { return size; }
//前者是Vector中實現然後者是ArrayList中實現

  可是,切不可簡單的認爲只要使用Vector就萬事無憂了。這裏的線程安全只是指Vector類實例化的對象的單個方法自己是線程安全的,但若是一個類中的方法A調用了所謂線程安全中的類的多個方法,而且A沒有synchronized修飾,那麼該方法A若是被多個線程重入,是仍然會產生線程安全問題的。實例以下:

Object value = map.get(key);
if(value == null)
{
    value = new Object();
    map.put(key,value);
}
return value;

  map是一個線程安全類的對象,可是在該方法片斷中get和put方法之間的部分,是有可能發生當前線程時間片結束,而另外一個線程得到時間片進入該段代碼執行的,從而形成一個key可能對應兩個value這樣的問題,因此是線程不安全的。而解決的方法就是對這段代碼總體加synchronized。因而可知,在這種意義上,沒有真正線程安全的類,咱們必須依靠手中的synchronized,在合適的位置對具體的代碼片斷進行保護,線程安全類只是給咱們提供一個小單元的線程安全。

 

(4)三次做業的心路歷程

  這三次多線程大冒險是很是曲折的,具體的失敗經歷就一筆帶過了。其中多線程的控制問題一直刁難着我,在初始接觸的時候,我一直糾結的一個問題就是:爲何提供的方法中不能肯定地讓某個線程休息,讓某個線程工做,同時notify方法每次通知的又是哪一個線程,究竟線程的執行順序是怎樣的?爲何不採用將全部線程固定好順序分享時間片來執行。通過這三次做業以後,我目前的答案是:

  1.線程執行的順序不必定能夠遵循一種固定的模式,例如網頁請求的處理,請求的發送時間是隨機的,爲了作到及時響應,咱們必須在輸入到達的時候就作到及時響應,完成線程切換。

  2.有一些狀況根本不用考慮線程執行的前後順序,不管什麼樣的順序程序也能正常執行,因此不須要多費力去調整順序。

  3.能夠經過外部定義相關條件判斷來構造「有序」。如經典生產者消費者問題中的變量empty和full,記錄產品的隊列的事實狀況從而有序的控制生產者和消費者之間的順序。

  多線程程序,最重要的就是共享對象,正是因爲共享對象的存在才致使了多線程相關的問題。若沒有共享對象,多線程程序只是幾個同時執行的單線程罷了,沒有什麼兩樣。在設計的階段,定義怎樣的共享對象類,可以有效的在線程之間傳遞信息,是首要問題。接着,對於具體的方法,判斷是否有對共享對象產生影響的行爲,若是是,就要作相應的保護和同步,這是根據類的設計來對應處理的。解決好了這兩個問題多線程編程中多線程的部分也就基本OK了。

  在公測和互測中的各種BUG,要麼是對於某些特殊狀況缺少考慮,要麼是功能變多後代碼之間的互相影響。而在構建完整個工程後再debug是很是痛苦的,不光找bug痛苦,改bug更痛苦,牽一髮而動全身,你永遠不知道改了當前的錯誤會不會產生新的錯誤。由此看來,代碼的總體框架設計,可讀性,可擴展性真的是基石通常的存在。好的代碼風格和習慣,會一點一點給那些堅持好習慣的人帶來驚喜。

 

  最後,很是感謝這幾回做業結束後分享給我代碼的同窗和互測遇到的同窗!!!讀代碼真的是一件收益良多的事情。

  固然特別特別感謝HansBug,他的工程模板堪稱業界良心,最後附上模板GitHub連接 https://github.com/HansBug/java-project-template

相關文章
相關標籤/搜索