今天看书的时候摘录下一句很有意思的话,共勉之。
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.
以RecycleView和Adapter作为例子,如你所知,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 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
定义一系列用于交互的Interface
,presenter
会负责逻辑处理。在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。
享受生活,享受编程。
期待下一篇,开/闭原则。