- 原文连接:https://academy.realm.io/posts/donn-felker-solid-part-1/
- 译文出自:kailaisi的简书
- 译 者:kailaisi
SOLID原则简介
SOLID是一种缩写,是面向对象设计的五项基本原则。
在未来一段时间,我将会深入研究每一个原则,解释它的含义以及它与Android开发的关系。
SOLID原则背景
SOLID是在2000年早期由Robert Martin (AKA: Uncle Bob)和Michael Feathers领导的团队一起提出的。这五个面向对象设计的基本原则,能够极大的帮助开发人员,增强系统的可维护性以及可扩展性。
如果你不熟悉Uncle Bob或Michael Feathers,我强烈建议挑选他们的几本著作来阅读。Uncle Bob编写的《敏捷软件开发:原则、模式与实践》和《代码整洁之道》是其在软件方面的主要著作。Michael Feathers编写的《修改代码的艺术》是我在领导团队时要求每一个开发者必读的一本书。它能够帮助你重构处理旧代码并增强其可维护性。更重要的是,它能够帮助你理解“遗留代码”的真正含义。您的代码最近有测试过么?没有!?!嗯,那么你的代码可能已经.....你猜对了......是遗留代码了。
阅读这些书籍对于我的生涯具有决定性作用。我强烈建议每一个开发者能够把它们放入自己的阅读清单中,并且能够把它们放在书架上以便经常回顾。
我记得,早在2003年,我就在各种.Net工程中使用了SOLID原则。当时,SOLID原则的提出确实使我震惊,因为当时我的.Net代码正在变得越来越杂乱无章,而且毫无架构可言。这不仅仅是.Net的症状,在所有新的技术(例如移动设备-Android等)中也存在着这样的问题。新技术在SOLID原则的使用上逐渐变得成熟,这也是为何SOLID原则越来越重要。
最近关于Uncle Bob的《代码整洁之道》在安卓社区中复苏,我觉得有必要来解释一些Uncle Bob中提及的基本原则。这一系列的文章将会讨论SOLID原则以及它在安卓开发中的应用。
--------------------------------特别华丽的分割线-------------------------------
单一职责原则
单一职责原则(SRP)很容易理解。它的描述如下:
一个类应该只有一个引起它变化的原因。
我们以 RecyclerView 和它的adapter为例。正如你所知道的那样,RecyclerView是一个能够在屏幕上显示数据的灵活控件。为了将数据展示在屏幕上,我们需要一个RecyclerView.Adapter。
adapter从数据集获取数据,并将其展示在视图中。adapter中最重要的一部分可以说是onBindViewHolder方法(有时候可能是ViewHolder本身,但为了简洁我们仍然坚持是onBindViewHolder方法)。RecyclerView的adapter只有一个职责:将对象映射到的显示在屏幕上的对应视图中。
假设对象以及RecyclerView.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);
}
上面的代码就违反了单一职责原则。
Why?
adapter中的onBindViewHolder方法不仅仅将order对象映射到视图中,而且负责了价格的计算以及数据的格式处理。这违背了单一职责原则。Adatper应该仅仅负责将order对象映射到对应的视图。onBindViewHolder承担了两个额外的职责。
为什么这是一个问题?
一个类中包含了多种职责经常会引起一系列问题。
首先,order中的逻辑处理是与adapter耦合的。如果你需要在其他地方显示order的总价,你就必须复制那段逻辑。一旦发生这种情况,你的应用程序将会面临着逻辑重复问题。当你更新某一处的代码时,很可能忘记更新另一处的代码。写到这里,相信你已经抓住重点了。
第二个问题和第一个问题一样——数据的格式处理与adapter耦合了。如果需要移动或者更新格式呢?最终,我们可能会使一个类负责了远多于他应该负责的事情。由于在一个位置负责了太多的任务,应用程序更容易发生错误。
值得庆幸的是,通过将总计的计算提取到Order对象以及将货币的格式化移动到货币格式化类中,我们就可以解决这个简单的例子。格式化类能够被order类调用。
更新后的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形容的更恰当的答案了,因此我在这里引用他的话:
在单一职责原则(SRP)中我们将职责定义为:一个变化的原因。如果你能够想到多种动机来改变一个类,那么这个类就包含多种职责。
事情就是这样,有时候很难察觉到,特别是你已经写了很长时间代码之后。在这一点上,这句名言通常会浮现在脑海中:
只见树木 ,不见森林。
在软件领域,这句话意味着你太关注于代码的细节,而忽略了整体的框架。比如:你编写的代码看起来运行良好,但那是因为你在这个类上花费了很长时间以至于忽略了它身兼多职。
对于我们来说,最大的挑战是什么时候该使用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类负责绘制视图,负责将order绑定到视图中,负责创建ViewHolder等等。这个类就是一个多职责类。
这些职责应该被拆分开么?
这最终取决于应用的发展趋势。如果应用经常更改视图的展示方式以及数据的连接功能(视图的逻辑),就像Uncle Bob所描述的那样,这种设计就有一些问题,因为一种改变需要改变另一个事情。视图结构的变化需要修改adapter,从而导致这个设计变得有些死板。如果应用程序一直都不会变更这部分的需求,那么就没有必要将他们分离。在这种情况下,进行功能的隔离反而会导致不必要的复杂性。
那么,我们应该怎么做呢?
一个说明刚性的例子
假设有一个新的产品需求:如果订单的总价格是0时,视图上不再显示文本形式的总价,而是显示一张亮黄色的“Free”图片。这个逻辑应该在哪里处理?在一种形式下,你需要一个TextView,另一种形式下,你需要一个ImageView。代码有两处需要修改:
- 展示视图的地方
- 处理逻辑的地方
我所遇到的大部分的程序,都是在adapter中处理。不幸的是,当你的视图改变时,需要你修改Adapter。如果逻辑的处理也在adapter中处理,那么这种需求也会要求你必须修改adapter中关于逻辑部分的代码。这就为adapter增加了另一个职责。
这正是一些类似于MVP模式等方案中所提及的,通过必要的解耦方式,来降低类的复杂度。这种处理方式增强了代码扩展的灵活性、可组合性以及可测性。例如,View层应该实现一个接口来提供对外的交互,presenter层来执行必要的逻辑处理。persenter在MVP模式中只负责展示的逻辑,仅此而已。
将逻辑处理部分从adapter转移到presenter中,这就会使adapter更加遵循单一职责原则。
事情远不止这样.....
如果你深入研究过RecyclerView adapter,你会发现adapter做了很多事情。比如:
- 绘制视图
- 创建ViewHolder类
- 回收ViewHolder
- 提供展示数据的数量
- 等等....
由于SRP是单一原则,你可能会疑惑,adapter的这种设计是否遵守SRP原则呢?Uncle Bob Martin 是这样解释的
只有变化发生的时候,这种改变才能称得上改变。如果没有发生任何变化,那么将SRP或者任何其他原则应用在这个事件上是很不明智的。
Adapter本身就是被设计成为一种执行这些操作的类。毕竟adapter只是适配器模式的一种简单实现。在这种情况下,将视图的绘制、ViewHolder的创建在一个地方进行处理是合理的。也就是说,这个类的职责就是这样。然而,引入额外的行为(比如视图逻辑的处理)就破坏了SRP原则。可以通过MVP模式或者其他重构的方法来避免这种情况的发生。
总结
单一职责原则可能是SOLID原则中最简单的一种,因为它只有一句简单的描述:
一个类应该只有一个引起它变化的原因。
也就是说,这也是最难以应用的原则之一。过分的分析代码,很容易让你觉得有必要使用SRP原则,而一旦你这样做了,你的应用程序可能就会变得复杂。我的建议是远离代码,客观得看待它。将你对代码的感性刨除出去,用新眼光看待它。如果你这么做了,你将会发现你代码中一些不同的东西。你可能会意识到你需要使用SRP模式,或者你可能意识到你已经做的很棒。不管如何,花费一些时间来重新审视一下自己的代码。
最后,随着程序变更,你可能发现原来不需要应用SRP的地方需要使用这种原则。这种情况是完全ok的,也是建议这么处理的。
Happy coding...