[译]单一职责原则在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。

享受生活,享受编程。

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值