拒絕紙上談兵--萌新開發手冊(踩坑分享之封裝)

xujl發表於2017-09-29

  首先宣告一下,我也是菜雞,本文內容純屬自己從專案獲得的經驗用來分享,如果文章中有什麼觀點或說法不對的地方,歡迎大佬們看到了及時糾正,不過對於大部分萌新來說,文章應該總體上是沒有什麼錯的。

廢話不多說,下面開始:

  說起封裝,有的人覺得很簡單,有的人會覺得無從下手,無論是哪種情況,想要寫出比好的封裝,最簡單的方法就是被坑了。為什麼這麼說呢,因為只有當你自己被自己寫的程式碼坑到時,你才知道這裡該封裝,該如何封裝,坑的越痛,記憶越深刻,別問我怎麼知道的,我不想說。當然,既然我在寫這篇文章,目的就是希望萌新們少踩點坑,有些東西如果能夠一開始就做好,豈不美哉。
  上面說的都是比較虛的東西,下面我們來點乾貨,到底在真正的專案中,我們在那些地方需要封裝?應該怎麼去封裝?

1.對第三方庫的封裝

  使用了蠻多第三方庫,也加了不少群,發現很多萌新(甚至是工作了近一年的準初級程式猿)在使用別人的庫時,都喜歡直接照著文件就在自己的專案中到處使用,完全沒有意識到這麼寫的隱患。如果一個庫需有很多配置引數,相信大部分人還是知道,應該自己寫個工具類來進行統一配置,但是當一個庫的用法比較簡單時,許多人就喜歡直接到處寫了,這樣其實會有很多問題的。
  首先,耦合過強不易替換。不對第三方進行封裝,最直接的影響就是不能夠快速替換,以我們最常見的圖片載入庫來說,早幾年比較流行的圖片載入框架是一個叫做ImageLoader的庫。試想一下,一個app中,使用圖片載入的地方,少則十幾處,多則幾十處,如果你每個地方都引用了ImageLoader這個類,那麼當你某一天需要換掉這個庫的時候會發生什麼?沒錯,你需要到處修改,雖然studio有重構快捷鍵和全域性查詢功能,可以方便的找到所有引用的地方,但是明明可以靠程式碼解決的問題,又何必給自己挖坑呢。
  其次,不易修改。有些庫,可能本身配置就比較少,剛好它的預設配置就能滿足你當前的需求,這時,很多人也就直接用了。那麼,你們有沒有想過,後面你發現需要修改某一個屬性配置時要怎麼辦呢?沒錯和上面類似的,你又要去到處該,簡直就是地獄了。
  那麼我們該如何對第三方進行封裝呢?這裡只說幾點我認為的基本要求,1.第三方庫的所有類只能出現在自己的封裝類中,專案裡其他任何地方不應該對第三方庫產生引用,只有這樣才能做到,你想替換這個庫時,只進行最少的程式碼修改。2.如果專案本身對一個庫的配置沒有太多變化的時候,儘量不要把配置過多的暴露到封裝類之外,這樣做可以保證封裝類呼叫的簡潔性。
  下面以一個下拉重新整理庫為例進行簡單封裝
  重新整理庫SmartRefreshLayout--https://github.com/scwang90/SmartRefreshLayout
  許多萌新,基本上拿到一個庫就開始照著別人的文件往專案加了,比如這樣用

public class RefreshActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //下面示例中的值等於預設值
        SmartRefreshLayout refreshLayout = (SmartRefreshLayout)findViewById(R.id.refreshLayout);
        refreshLayout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);
        refreshLayout.setDragRate(0.5f);//顯示下拉高度/手指真實下拉高度
        refreshLayout.setRefreshHeader(new ClassicsHeader(this));//設定Header
        refreshLayout.setRefreshFooter(new ClassicsFooter(this));//設定Footer
        refreshLayout.autoRefresh();//自動重新整理
    }
}複製程式碼

  除錯了一下,沒什麼問題,然後就開始在專案各處使用。突然有一天,產品經理髮話了,這個重新整理樣式不好看,重新設計,然後你就一臉懵逼了,你這個設定寫的到處都是,只能去一個一個改了。當然這個問題其實是個很基礎的問題,基本上有個把月經驗的同學都知道需要統一配置,所以也許他們會這麼寫:

public class RefreshActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //下面示例中的值等於預設值
        SmartRefreshLayout refreshLayout = (SmartRefreshLayout)findViewById(R.id.refreshLayout);
        RefreshLayoutUtil.refreshLayoutConfig(refreshLayout ,this);
        refreshLayout.autoRefresh();//自動重新整理
    }
}
=========================分割線==================================
 public class RefreshLayoutUtil {
        public static void refreshLayoutConfig (SmartRefreshLayout refreshLayout, Context context) {
            refreshLayout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);
            refreshLayout.setDragRate(0.5f);//顯示下拉高度/手指真實下拉高度
            refreshLayout.setRefreshHeader(new ClassicsHeader(context));//設定Header
            refreshLayout.setRefreshFooter(new ClassicsFooter(context));//設定Footer
        }
    }複製程式碼

  這也就是我上面說的對第三庫進行統一封裝的一部分,我們這裡採用的是對通用配置進行統一封裝,好了現在產品經理告訴我,你想改幾次樣式?來來,隨便改,皺下眉頭算我輸。
  擁有了上面的小技巧,大家終於能輕鬆一點。然而,你剛躺下,被子都沒捂熱,突然產品經理一個電話,XX你的app重新整理就崩潰了。然後你就開始debug看了,最後發現不是你的問題,是這個庫本身就有bug,然後你打算上github上提給作者,一看,我去,作者竟然已經宣佈停止維護了。那怎麼辦?有些同學會選擇把庫下載下來,自己試著修改,不過礙於水平有限,大部分人在嘗試了一下修改然後無果後都會選擇換成另一個庫。
  好嘛,我們現在又來換庫試試,去掉庫依賴,我去,竟然有幾十個類和xml報錯,這次是真的絕望了,看來只能刪程式碼跑路了。。。
  開個玩笑,我們肯定不可能跑路的,只能硬著頭皮改。關鍵是改之前你要考慮,下次再出這個問題怎麼辦呢?當然還是通過封裝來解決了:

public class RefreshLayout extends SmartRefreshLayout {
    public RefreshLayout (Context context) {
        super(context);
    }

    public RefreshLayout (Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public RefreshLayout (Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setOnRefreshListener (final RefreshListener refreshListener) {
        setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh (com.scwang.smartrefresh.layout.api.RefreshLayout refreshlayout) {
                refreshListener.onRefresh((RefreshLayout) refreshlayout);
            }
        });
        setOnLoadmoreListener(new OnLoadmoreListener() {
            @Override
            public void onLoadmore (com.scwang.smartrefresh.layout.api.RefreshLayout refreshlayout) {
                refreshListener.onLoading((RefreshLayout) refreshlayout);
            }
        });
    }

    public void startRefresh () {
        super.autoRefresh();
    }

    public void startLoadMore () {
        super.autoLoadmore();
    }

    public interface RefreshListener {
        void onRefresh (RefreshLayout refreshLayout);

        void onLoading (RefreshLayout refreshLayout);
    }

    public void finishRefreshing () {
        if (!isRefreshing()) {
            return;
        }
        super.finishRefresh();
    }

    public SmartRefreshLayout finishLoadmore () {
        if (!isLoading()) {
            return null;
        }
        return super.finishLoadmore();
    }
    .......
    省略部分配置程式碼
    .......
}複製程式碼

  可能有的同學一看這程式碼就瘋了,說,這不是閒的蛋疼嘛,為什麼要自己繼承一次,而且還把父類的方法重新封裝了一次?

  其實一開始我已經說得很清楚了,對於第三庫的封裝就是要避免外界直接引用第三庫的任何類或方法,試想一下,如果我們一開始就是這麼寫的,當你去掉了這個重新整理庫的依賴時哪裡還會報錯?沒錯,如果你遵照著上面的原則進行封裝,你會發現僅僅只有這個類會報錯,其他任何類和xml佈局都不會報錯,不報錯也就說明其他地方不用做任何修改,你只需要接入你的新庫,然後修改自己這個封裝類,也許你的新庫中重新整理方法不叫autoRefresh,就叫refresh,不過沒什麼關係,你只需要把super.autoRefresh改成super.refresh就行了。其他的類其實依然是呼叫你的封裝方法startRefresh,所以立即就能生效了。

  上面說的是對第三方控制元件的封裝,下面來說說對第三方工具類型別庫的封裝,其實原理都一樣,只要具備了上面的封裝思想,再來封裝工具類也就舉一反三了。下面以一個圖片選擇庫為例:
  github.com/LuckSiege/P…

  public static void chooseImage(Activity activity,int maxCount){
        PictureSelector.create(activity)
                .openGallery(PictureMimeType.ofImage())//全部.PictureMimeType.ofAll()、圖片.ofImage()、視訊.ofVideo()
//                .theme()//主題樣式(不設定為預設樣式) 也可參考demo values/styles下 例如:R.style.picture.white.style
                .maxSelectNum(maxCount)// 最大圖片選擇數量 int
                .minSelectNum(0)// 最小選擇數量 int
                .imageSpanCount(4)// 每行顯示個數 int
                .selectionMode(PictureConfig.MULTIPLE)// 多選 or 單選 PictureConfig.MULTIPLE or PictureConfig.SINGLE
                .previewImage(false)// 是否可預覽圖片 true or false
                .isCamera(false)// 是否顯示拍照按鈕 true or false
                .isZoomAnim(true)// 圖片列表點選 縮放效果 預設true
                .sizeMultiplier(0.5f)// glide 載入圖片大小 0~1之間 如設定 .glideOverride()無效
                .setOutputCameraPath("/CustomPath")// 自定義拍照儲存路徑,可不填
                .enableCrop(false)// 是否裁剪 true or false
                .compress(true)// 是否壓縮 true or false
                .compressMode(PictureConfig.LUBAN_COMPRESS_MODE)//系統自帶 or 魯班壓縮 PictureConfig.SYSTEM_COMPRESS_MODE or LUBAN_COMPRESS_MODE
//                .glideOverride()// int glide 載入寬高,越小圖片列表越流暢,但會影響列表圖片瀏覽的清晰度
                .withAspectRatio(1,1)// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定義
                .hideBottomControls(true)// 是否顯示uCrop工具欄,預設不顯示 true or false
                .isGif(true)// 是否顯示gif圖片 true or false
                .freeStyleCropEnabled(true)// 裁剪框是否可拖拽 true or false
                .circleDimmedLayer(false)// 是否圓形裁剪 true or false
                .showCropFrame(true)// 是否顯示裁剪矩形邊框 圓形裁剪時建議設為false   true or false
                .showCropGrid(false)// 是否顯示裁剪矩形網格 圓形裁剪時建議設為false    true or false
                .openClickSound(false)// 是否開啟點選聲音 true or false
                .cropCompressQuality(80)// 裁剪壓縮質量 預設90 int
                .compressMaxKB(200)//壓縮最大值kb compressGrade()為Luban.CUSTOM_GEAR有效 int
//                .compressWH() // 壓縮寬高比 compressGrade()為Luban.CUSTOM_GEAR有效  int
//                .cropWH()// 裁剪寬高比,設定如果大於圖片本身寬高則無效 int
                .rotateEnabled(false) // 裁剪是否可旋轉圖片 true or false
                .scaleEnabled(true)// 裁剪是否可放大縮小圖片 true or false
                .forResult(PictureConfig.CHOOSE_REQUEST);//結果回撥onActivityResult code
    }複製程式碼

  好了,完了。這也叫封裝?沒錯,這也叫封裝了。只是相對來說,我們這種封裝算是比較死的,因為各種屬性都沒有給出修改的方法,但是你要相信就算是你僅僅這樣簡單封裝了一下,也絕對好過你到處去引用第三方庫的類很多。原因嘛就不再重複了,上面已經說得很清楚了,這個封裝雖然簡陋,但是你想讓他變靈活的話大可以自己修改下。對於像這種屬性比較多的庫建議使用builder模式進行封裝,這樣呼叫和修改都比較簡單,具體封裝程式碼這裡就不再寫了,有興趣的同學可以自己嘗試下。

注意:並不是所有庫都一定要進行封裝,有些庫可能會由於某些原因沒有辦法進行很好的封裝,比如Butter knife,rxjava。當然我這裡說的不能很好的封裝並不是指都完全不封裝,而是指可能無法完全按照我上面說的幾個原則進行封裝,比如rxjava,他涉及很多類和方法,你要完全達到使用時不直接呼叫它的類基本上要封裝很多程式碼,這時我們可以退而求其次,只封裝一些通用配置屬性。而像Butter knife這個庫則基本上處於完全無法封裝的狀態。
  說句題外話,在有其他解決方案的時候強烈不建議使用類似於Butter knife這種庫。Butter knife除了能偷懶少些幾個findView以外,基本上毫無卵用,但是這種庫最大的問題就在於耦合很高,你的專案一旦加入了這種東西,想再去掉就很麻煩了,這也是我為什麼一直不用Butter knife的原因,如果你真的很討厭寫findView,我建議你使用官方的dataBinding或者使用kotlin開發,他們都能解決findView的問題,而且效果遠比Butter knife好多了。當然了,這裡純屬個人見解,如果你覺得Butter knife好用的話,那就繼續用吧

  如何判斷一個第三方庫該不該封裝?能不能封裝?很簡單,如果一個庫需要的封裝程式碼過多時,就可以考慮只簡單封裝一下,讓外界引用幾個第三方庫的類也沒關係。當然了,你要會風險評估,如果這個庫有大概率會被替換掉的話,就要一開始就封裝好。目前來說,像rxjava這種,完全封裝的話,意義就不大,首先它涉及的東西太多,要封裝肯定需要自己寫很多東西,其次這種庫基本沒什麼可替代的,所以對於你來說一般只會考慮用不用,如果你本來用了,現在要去掉它的話,不管你封不封裝,基本上都要大面積修改程式碼,那花大力氣封裝也就是浪費時間了。

2.專案中的邏輯封裝

  這裡來說說對專案中部分邏輯的封裝,其實有了上面對第三方庫的封裝,那麼對自己專案中什麼東西該封裝,大家心裡應該都有點數了,還是直接舉個栗子,請看如下程式碼:

        EditText nameTV = findView(R.id.name);
        EditText sexTV = findView(R.id.sex);
        EditText ageTV = findView(R.id.age);
        nameTV.setFocusable(false);
        nameTV.setClickable(false);
        sexTV.setFocusable(false);
        sexTV.setClickable(false);
        ageTV.setFocusable(false);
        ageTV.setClickable(false);複製程式碼

  這是一個很常見的需求,某些介面可能既是編輯頁面又是詳情介面,這時可能就需要我們動態改變輸入框的可點選狀態,上面這麼寫看上去似乎除了比較醜陋以外並沒有太大問題,但是現在需求又雙叒變了,進入編輯時我們要清空每個輸入框文字並更改提示文字,好嘛,你可能不得不這麼改:

        EditText nameTV = findView(R.id.name);
        EditText sexTV = findView(R.id.sex);
        EditText ageTV = findView(R.id.age);
        nameTV.setFocusable(false);
        nameTV.setClickable(false);
        nameTV.setText("");
        nameTV.setHint("請輸入");
        sexTV.setFocusable(false);
        sexTV.setClickable(false);
        sexTV.setText("");
        sexTV.setHint("請輸入");
        ageTV.setFocusable(false);
        ageTV.setClickable(false);
        ageTV.setText("");
        ageTV.setHint("請輸入");複製程式碼

  有木有感覺很蛋疼?沒有?那我告訴你,我這個介面其實有15個輸入框,好嘛,你去改改看。那麼我們換一種方法來寫試試:

        EditText nameTV = findView(R.id.name);
        EditText sexTV = findView(R.id.sex);
        EditText ageTV = findView(R.id.age);
        editTextConfig(nameTV,sexTV,ageTV);

=========================分割線=======================================

 private void editTextConfig (EditText... editTexts) {
        if (editTexts == null || editTexts.length == 0) {
            return;
        }
        for (EditText editText : editTexts) {
            editText.setFocusable(false);
            editText.setClickable(false);
            editText.setText("");
            editText.setHint("請輸入");
        }
    }複製程式碼

  這下你想加任何配置都只需要修改editTextConfig方法即可,你15個控制元件也罷,只需要在引數列表傳入就行。可以看出,這裡我們僅僅是封裝一個簡單的內部方法,就大大提高了程式碼的可維護性,相信大家看了這個例子已經明白封裝有多麼重要了。

  好了,該講的也差不多講完了,專案中的封裝,這個真的要靠自己去實踐,我這裡也只能分享一個簡單的例子,希望大家能夠舉一反三。最後就說說我個人覺得專案中那些東西是必須封裝的:

  • 複雜的邏輯程式碼。把相同的複雜邏輯程式碼到處寫,改的是後絕對虐哭你。
  • 出現頻率非常高的程式碼。例如對專案中的所有金額進行小數兩位四捨五入,這個邏輯本身很簡單,程式碼也只有一行的樣子,當時如果你到處寫的話,突然產品經理抽風要精確到3位,你怎麼辦?所以說,出現頻率很高的程式碼,哪怕只有一行,也應該封裝一次。
  • 特殊的業務邏輯程式碼。某些專案中特有的業務邏輯,可能本身出現次數不是太多4到5次吧,邏輯也不復雜,20行左右吧,你是不是又想偷懶不封裝?是不是覺得反正才那麼幾處,大不了就是複製貼上嘛。ok,你改完上線,然後又雙叒被產品經理罵了,原來這塊邏輯本來出現在5個地方,結果你只改了4個,漏掉了一個地方沒改。

  以上的就是基本上來講必須要封裝的,不過最後一條的定義其實比較模糊,這個只能自己根據經驗去判斷。現在你應該清楚了,程式設計師封裝不是僅僅為了偷懶,少些程式碼。不封裝的話,多複製幾遍都只是小問題,冰山一角而已,真正後期維護才是整個冰山。


  好了,本文到這裡就結束了,以上觀點純屬個人看法,如有不同見解歡迎指教。後面我還會繼續分享我的踩坑日記,歡迎大家持續關注

  我的開源專案:MVP框架庫

相關文章