[譯]單一職責(zé)原則在Android中的實踐

今天看書的時候摘錄下一句很有意思的話,共勉之。

Adding features means adding new code instead of modifying the old code.

原文鏈接--S is for Single Responsibility Principle——by Donn Felker

(譯者注:這次原文很長濒析。我建議先看代碼部分惫谤。Uncle Bob的書的確值得多讀汰蓉。實現(xiàn)這個系列的最好方法:找一個以前寫的老Activity康铭,審視命名規(guī)范蹋宦,內(nèi)存泄漏披粟,是否符合設(shè)計原則,這樣進步飛快)妆档。

這是SOLID原則五部曲的第一步僻爽。

SOLID是面向?qū)ο蟮奈鍌€設(shè)計原則的縮寫:

  • 單一職責(zé)原則 (Single Responsibility Principle)
  • 開/閉原則 (Open-Close Principle)
  • Liskov 替換原則 (Liskov Substitution Principle)
  • 接口分離原則 (Interface Segregation Principle)
  • 依賴翻轉(zhuǎn)原則 (Dependency Inversion Principle )

接下來幾周,我們會深入了解各個原則贾惦,解釋它們的含義胸梆,如何與Android結(jié)合。所有課程結(jié)束后须板,你會抓住原則的精髓碰镜,了解到作為Android攻城獅,在日常的開發(fā)中運用這些原則是如此重要习瑰。

SOLID的歷史

SOLID是Rober Martin(Uncle Bob)在2000年與Michael Feathers共同提出的绪颖。結(jié)合運用這五項基本原則,能快速構(gòu)建出可維護甜奄,高拓展性的系統(tǒng)柠横。

如果不熟悉Rober Martin或者Michael Feathers,高度推薦他們寫的書:《敏捷軟件開發(fā)课兄,原則 模式與實踐》牍氛,《代碼整潔之道》是軟件社區(qū)的精神食糧。Michael Feathers的《修改代碼的藝術(shù)》是我如果作為開發(fā)組長烟阐,必須要求每個開發(fā)成員都讀的書搬俊。它能幫助你整理優(yōu)化舊代碼的思路,重構(gòu)出更易維護的代碼蜒茄。更重要的是唉擂,它們能改變你對“優(yōu)雅”的準確定義,比如檀葛,你的代碼有單元測試嘛玩祟?沒有?呵呵驻谆。

閱讀這些書的確對我的職業(yè)有巨大的影響卵凑,我極度推薦開發(fā)者去閱讀,買本實體書放柜子里胜臊,經(jīng)常重溫勺卢。

我記得自己使用SOLID原則是在2003年.NET的項目上,那時我的代碼缺乏組織架構(gòu)引導(dǎo)象对,搞得一團糟黑忱。這并不僅僅發(fā)生在.NET身上,新生的技術(shù)往往會經(jīng)歷混沌,例如Android甫煞。最終新技術(shù)會因擁抱SOLID而變得更成熟菇曲。

最近Rober Martin的演講 - Clean Architecture又一次沖擊了Android社區(qū),正是解釋基礎(chǔ)原理的時候抚吠,下面讓我們進入正題常潮。

第一部分-單一職責(zé)原則

單一職責(zé)原則很容易理解,它說的是

A class should have only one reason to change.

RecycleViewAdapter作為例子楷力,如你所知喊式,RecycleView是一個展示數(shù)據(jù)的可拓展的View像捶。為了顯示數(shù)據(jù)师脂,需要使用RecycleView.Adapter抵窒。

Adapter從數(shù)據(jù)集中取出數(shù)據(jù)调窍,綁定到View中。最昂貴的開銷莫過于onBindViewHolder方法(有時可能是ViewHolder白筹,為了簡潔我們只關(guān)注onBindViewHolder)劝篷。RecycleView.Adapter有一個職責(zé):把數(shù)據(jù)適配到View中颠焦,并展示在屏幕上何址。

假設(shè)類和Adapter寫成這樣:

    public class LineItem {
        private String description; 
        private int quantity; 
        private long price; 
        // ... getters/setters
    }
    
    public class Order {
        private int orderNumber; 
        private List<LineItem> lineItems = new ArrayList<LineItem>();  
        // ... getters/setters
    }
    
    public class OrderRecyclerAdapter extends RecyclerView.Adapter<OrderRecyclerAdapter.ViewHolder> {
     
        private List<Order> items;
        private int itemLayout;
     
        public OrderRecyclerAdapter(List<Order> items, int itemLayout) {
            this.items = items;
            this.itemLayout = itemLayout;
        }
     
        @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
            return new ViewHolder(v);
        }
     
        @Override public void onBindViewHolder(ViewHolder holder, int position) {
            // TODO: bind the view here 
        }
     
        @Override public int getItemCount() {
            return items.size();
        }
     
        public static class ViewHolder extends RecyclerView.ViewHolder {
            public TextView orderNumber;
            public TextView orderTotal;
     
            public ViewHolder(View itemView) {
                super(itemView);
                orderNumber = (TextView) itemView.findViewById(R.id.order_number);
                orderTotal = (ImageView) itemView.findViewById(R.id.order_total);
            }
        }
    }

在上述例子中里逆,onBindViewHolder沒有具體實現(xiàn),一種我看過很多次的寫法是這樣子:

    @Override 
    public void onBindViewHolder(ViewHolder holder, int position) {
        Order order = items.get(position);
        holder.orderNumber.setText(order.getOrderNumber().toString());
        long total = 0;
        for (LineItem item : order.getItems()) {
            total += item.getPrice();
        }
        NumberFormat formatter = NumberFormat.getCurrencyInstance(Locale.US);
        String totalValue = formatter.format(cents / 100.0); // Must divide by a double otherwise we'll lose precision
        holder.orderTotal.setText(totalValue)
        holder.itemView.setTag(order);
    }

這樣的代碼違反了單一職責(zé)原則用爪。

為什么运悲?

因為Adapter.onBindViewHolder不僅把數(shù)據(jù)類型適配到View上,還計算了價格的和格式化项钮。這違反了單一職責(zé)原則。Adapter應(yīng)該只做前者的工作希停,而Adapter.onBindViewHolder卻額外多做了兩項工作烁巫。

這會有什么問題嘛?

一個包含多種職責(zé)的類會引發(fā)各種問題宠能。首先亚隙,計算訂單的邏輯與Adapter耦合了。如果你在其他地方需要同樣的計算邏輯违崇,就只能復(fù)制粘貼一份阿弃。一旦這樣做,你的應(yīng)用就會陷入重復(fù)邏輯的泥沼羞延,一旦在一個地方更新代碼渣淳,很容易忘記更新另一個地方,你懂的伴箩。

第二個問題和第一個類似入愧,把格式化數(shù)字耦合到Adapter中,萬一方法需要移動或修改呢?在一個類中做了過多工作棺蛛,會導(dǎo)致同一個地方容易引發(fā)各種Bug怔蚌。

幸運的是,這個簡單的例子可以通過把計算的邏輯遷移到Order中解決旁赊,格式話邏輯移動到合適的Format類中桦踊,依此類推。因此终畅,Order就可以使用Format啦籍胯。

更新后的Adapter.onBindViewHolder長這樣

    @Override 
    public void onBindViewHolder(ViewHolder holder, int position) {
        Order order = items.get(position);
        holder.orderNumber.setText(order.getOrderNumber().toString());
        holder.orderTotal.setText(order.getOrderTotal()); // A String, the calculation and formatting moved elsewhere
        holder.itemView.setTag(order);
    }   

我很肯定你會說,這很簡單啊声离。是不是所有情況都如此簡單呢芒炼?用一句軟件工程的術(shù)語說,看情況吧....

讓我們往深層次挖掘

“職責(zé)”的含義

Uncle Bob的理解無可比擬术徊,這里引述他的原話:

In the context of the Single Responsibility Principle (SRP) we define a responsibility as “a reason for change”. If you can think of more than one motive for changing a class, then that class has more than one responsibility.

有時候很難看透本刽,尤其是你面對這個代碼庫很長時間了。這時赠涮,應(yīng)該想到:

You can’t see the forest for the trees.

在軟件工程里子寓,你著重于實現(xiàn)而沒能落眼于抽象,例如——這個花費你巨大精力寫出來的龐然大物笋除,很難看出來它可能具有多重職責(zé)斜友。

更大的挑戰(zhàn)在于,知道時候使用SRP垃它,什么時候不用鲜屏。考慮一下Adapter的代碼国拇,可以找到各種不同修改代碼的理由和需求洛史。

    public class OrderRecyclerAdapter extends RecyclerView.Adapter<OrderRecyclerAdapter.ViewHolder> {
     
        private List<Order> items;
        private int itemLayout;
     
        public OrderRecyclerAdapter(List<Order> items, int itemLayout) {
            this.items = items;
            this.itemLayout = itemLayout;
        }
     
        @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
            return new ViewHolder(v);
        }
     
        @Override public void onBindViewHolder(ViewHolder holder, int position) {
            Order order = items.get(position);
            holder.orderNumber.setText(order.getOrderNumber().toString());
            holder.orderTotal.setText(order.getOrderTotal()); // Move the calculation and formatting elsewhere
            holder.itemView.setTag(order);
        }
    
     
        @Override public int getItemCount() {
            return items.size();
        }
     
        public static class ViewHolder extends RecyclerView.ViewHolder {
            public TextView orderNumber;
            public TextView orderTotal;
     
            public ViewHolder(View itemView) {
                super(itemView);
                orderNumber = (TextView) itemView.findViewById(R.id.order_number);
                orderTotal = (ImageView) itemView.findViewById(R.id.order_total);
            }
        }
    }

Adapter映射了View,把數(shù)據(jù)與視圖綁定酱吝,構(gòu)造ViewHolder等等也殖,這個類擁有多種職責(zé)。

應(yīng)該把這些職責(zé)分開嘛务热?

最終取決于應(yīng)用的迭代忆嗜。如果需要修改View的結(jié)構(gòu)和邏輯,如同Uncle Bob所說崎岂,由于兩個更改會互相影響捆毫,改變View的結(jié)構(gòu)同時Adapter同樣需要修改,這樣的設(shè)計就是過于剛性该镣。

然而冻璃,應(yīng)用的需求如果不經(jīng)常變更响谓,就沒有理由去分離多重職責(zé)。在這個例子中省艳,我們無需做無用的工作娘纷。

所以,我們應(yīng)該做什么跋炕?

一個死板的例子

假設(shè)新產(chǎn)品上市免費試用赖晶,View需要展示"Free"圖片而不是價格文字,這個邏輯寫在哪里辐烂?一方面遏插,你需要TextView,另一方面纠修,你需要ImageView胳嘲。這里有兩個地方需要修改:

  • View
  • 展示的邏輯

在大多數(shù)應(yīng)用中,這會寫在Adapter中扣草,不幸的是了牛,當View改變時時,Adapter必須同時進行修改辰妙。如果把邏輯也寫在Adapter中鹰祸,將迫使邏輯也要改變,這增加了Adapter的職責(zé)密浑。

這正是MVP模式帶來的解耦方案蛙婴,提高了可拓展性,可聚合的程度和可測試性尔破,使類不會變得過于笨重街图。例如,會給View定義一系列用于交互的Interface懒构,presenter會負責(zé)邏輯處理台夺。在MVP中,P只會負責(zé)展示邏輯痴脾。

把邏輯從Adapter移到Presenter中的確更符合單一職責(zé)原則。

也不完全是這樣...

如果你深入了解RecycleView.Adapter梳星,你會發(fā)現(xiàn)Adapter做了很多事:

  • 解析視圖
  • 創(chuàng)建ViewHolder
  • 回收ViewHodler
  • 提供數(shù)據(jù)集等等

你會想赞赖,為什么不把這些東西抽出來,讓單一職責(zé)原則實現(xiàn)呢冤灾?我又要引用Uncle Bob的解釋了:

An axis of change is only an axis of change if the changes actually occur. It is not wise to apply the SRP, or any other principle for that matter, if there is no symptom.

Adapter真的做了許多工作前域,事實上,它就是被這樣設(shè)計的韵吨。畢竟RecycleView.Adapter適配者模式的簡單應(yīng)用匿垄。保持解析視圖和ViewHolder的機制的確有道理,這就是這個類的職責(zé)的最好實現(xiàn)。然而椿疗,可以使用MVP或者其他重構(gòu)手段轉(zhuǎn)移邏輯代碼使其符合SRP漏峰。

結(jié)論

單一職責(zé)原理是SOLID中最簡單的一個。再重復(fù)一次:

A class should have only one reason to change.

也有人說届榄,這是實踐起來最難的原則之一浅乔。如果過度實踐SRP,過度的分析增加了代碼的復(fù)雜度铝条。我的建議是:以面向?qū)ο蟮乃枷肟创a靖苇,隔離你的感情,用全新的目光再度審視老代碼班缰,你就會發(fā)現(xiàn)以前從未知道的東西贤壁。也許需要實踐SRP,也許你早已做得足夠好了埠忘。

當應(yīng)用需要修改的時候脾拆,強烈建議在未應(yīng)用SRP的地方時間SRP。

享受生活给梅,享受編程假丧。

期待下一篇,開/閉原則动羽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末包帚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子运吓,更是在濱河造成了極大的恐慌渴邦,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拘哨,死亡現(xiàn)場離奇詭異谋梭,居然都是意外死亡,警方通過查閱死者的電腦和手機倦青,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門瓮床,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人产镐,你說我怎么就攤上這事隘庄。” “怎么了癣亚?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵丑掺,是天一觀的道長。 經(jīng)常有香客問我述雾,道長街州,這世上最難降的妖魔是什么兼丰? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮唆缴,結(jié)果婚禮上鳍征,老公的妹妹穿的比我還像新娘。我一直安慰自己琐谤,他們只是感情好蟆技,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斗忌,像睡著了一般质礼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上织阳,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天眶蕉,我揣著相機與錄音,去河邊找鬼唧躲。 笑死造挽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的弄痹。 我是一名探鬼主播饭入,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肛真!你這毒婦竟也來了谐丢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤蚓让,失蹤者是張志新(化名)和其女友劉穎乾忱,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體历极,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡窄瘟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了趟卸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹄葱。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖锄列,靈堂內(nèi)的尸體忽然破棺而出新蟆,到底是詐尸還是另有隱情,我是刑警寧澤右蕊,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站吮螺,受9級特大地震影響饶囚,放射性物質(zhì)發(fā)生泄漏帕翻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一萝风、第九天 我趴在偏房一處隱蔽的房頂上張望嘀掸。 院中可真熱鬧,春花似錦规惰、人聲如沸睬塌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽揩晴。三九已至,卻和暖如春贪磺,著一層夾襖步出監(jiān)牢的瞬間硫兰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工寒锚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留劫映,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓刹前,卻偏偏與公主長得像泳赋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喇喉,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容