[译]单一职责原则在Android中的实践

今天看书的时候摘录下一句很有意思的话,共勉之。

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

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

(译者注:这次原文很长。我建议先看代码部分。Uncle Bob的书的确值得多读。实现这个系列的最好方法:找一个以前写的老Activity,审视命名规范内存泄漏是否符合设计原则,这样进步飞快)。

这是SOLID原则五部曲的第一步。

SOLID是面向对象的五个设计原则的缩写:

  • 单一职责原则 (Single Responsibility Principle)
  • 开/闭原则 (Open-Close Principle)
  • Liskov 替换原则 (Liskov Substitution Principle)
  • 接口分离原则 (Interface Segregation Principle)
  • 依赖翻转原则 (Dependency Inversion Principle )

接下来几周,我们会深入了解各个原则,解释它们的含义,如何与Android结合。所有课程结束后,你会抓住原则的精髓,了解到作为Android攻城狮,在日常的开发中运用这些原则是如此重要。

SOLID的历史

SOLID是Rober Martin(Uncle Bob)在2000年与Michael Feathers共同提出的。结合运用这五项基本原则,能快速构建出可维护高拓展性的系统。

如果不熟悉Rober Martin或者Michael Feathers,高度推荐他们写的书:《敏捷软件开发,原则 模式与实践》《代码整洁之道》是软件社区的精神食粮。Michael Feathers的《修改代码的艺术》是我如果作为开发组长,必须要求每个开发成员都读的书。它能帮助你整理优化旧代码的思路,重构出更易维护的代码。更重要的是,它们能改变你对“优雅”的准确定义,比如,你的代码有单元测试嘛?没有?呵呵。

阅读这些书的确对我的职业有巨大的影响,我极度推荐开发者去阅读,买本实体书放柜子里,经常重温。

我记得自己使用SOLID原则是在2003年.NET的项目上,那时我的代码缺乏组织架构引导,搞得一团糟。这并不仅仅发生在.NET身上,新生的技术往往会经历混沌,例如Android。最终新技术会因拥抱SOLID而变得更成熟。

最近Rober Martin的演讲 - Clean Architecture又一次冲击了Android社区,正是解释基础原理的时候,下面让我们进入正题。

第一部分-单一职责原则

单一职责原则很容易理解,它说的是

A class should have only one reason to change.

RecycleViewAdapter作为例子,如你所知,RecycleView是一个展示数据的可拓展的View。为了显示数据,需要使用RecycleView.Adapter

Adapter从数据集中取出数据,绑定到View中。最昂贵的开销莫过于onBindViewHolder方法(有时可能是ViewHolder,为了简洁我们只关注onBindViewHolder)。RecycleView.Adapter有一个职责:把数据适配到View中,并展示在屏幕上。

假设类和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没有具体实现,一种我看过很多次的写法是这样子:

    @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);
    }

这样的代码违反了单一职责原则。

为什么?

因为Adapter.onBindViewHolder不仅把数据类型适配到View上,还计算了价格的和格式化。这违反了单一职责原则。Adapter应该只做前者的工作,而Adapter.onBindViewHolder却额外多做了两项工作。

这会有什么问题嘛?

一个包含多种职责的类会引发各种问题。首先,计算订单的逻辑与Adapter耦合了。如果你在其他地方需要同样的计算逻辑,就只能复制粘贴一份。一旦这样做,你的应用就会陷入重复逻辑的泥沼,一旦在一个地方更新代码,很容易忘记更新另一个地方,你懂的。

第二个问题和第一个类似,把格式化数字耦合到Adapter中,万一方法需要移动或修改呢?在一个类中做了过多工作,会导致同一个地方容易引发各种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);
    }   

我很肯定你会说,这很简单啊。是不是所有情况都如此简单呢?用一句软件工程的术语说,看情况吧....

让我们往深层次挖掘

“职责”的含义

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.

有时候很难看透,尤其是你面对这个代码库很长时间了。这时,应该想到:

You can’t see the forest for the trees.

在软件工程里,你着重于实现而没能落眼于抽象,例如——这个花费你巨大精力写出来的庞然大物,很难看出来它可能具有多重职责。

更大的挑战在于,知道时候使用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,把数据与视图绑定,构造ViewHolder等等,这个类拥有多种职责。

应该把这些职责分开嘛?

最终取决于应用的迭代。如果需要修改View的结构和逻辑,如同Uncle Bob所说,由于两个更改会互相影响,改变View的结构同时Adapter同样需要修改,这样的设计就是过于刚性。

然而,应用的需求如果不经常变更,就没有理由去分离多重职责。在这个例子中,我们无需做无用的工作。

所以,我们应该做什么?

一个死板的例子

假设新产品上市免费试用,View需要展示"Free"图片而不是价格文字,这个逻辑写在哪里?一方面,你需要TextView,另一方面,你需要ImageView。这里有两个地方需要修改:

  • View
  • 展示的逻辑

在大多数应用中,这会写在Adapter中,不幸的是,当View改变时时,Adapter必须同时进行修改。如果把逻辑也写在Adapter中,将迫使逻辑也要改变,这增加了Adapter的职责。

这正是MVP模式带来的解耦方案,提高了可拓展性,可聚合的程度和可测试性,使类不会变得过于笨重。例如,会给View定义一系列用于交互的Interfacepresenter会负责逻辑处理。在MVP中,P只会负责展示逻辑。

把逻辑从Adapter移到Presenter中的确更符合单一职责原则。

也不完全是这样...

如果你深入了解RecycleView.Adapter,你会发现Adapter做了很多事:

  • 解析视图
  • 创建ViewHolder
  • 回收ViewHodler
  • 提供数据集等等

你会想,为什么不把这些东西抽出来,让单一职责原则实现呢?我又要引用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真的做了许多工作,事实上,它就是被这样设计的。毕竟RecycleView.Adapter适配者模式的简单应用。保持解析视图和ViewHolder的机制的确有道理,这就是这个类的职责的最好实现。然而,可以使用MVP或者其他重构手段转移逻辑代码使其符合SRP。

结论

单一职责原理是SOLID中最简单的一个。再重复一次:

A class should have only one reason to change.

也有人说,这是实践起来最难的原则之一。如果过度实践SRP,过度的分析增加了代码的复杂度。我的建议是:以面向对象的思想看待代码,隔离你的感情,用全新的目光再度审视老代码,你就会发现以前从未知道的东西。也许需要实践SRP,也许你早已做得足够好了。

当应用需要修改的时候,强烈建议在未应用SRP的地方时间SRP。

享受生活,享受编程。

期待下一篇,开/闭原则。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容