五大基本原则之单一职责原则

SOLID原则简介

SOLID是一种缩写,是面向对象设计的五项基本原则。

在未来一段时间,我将会深入研究每一个原则,解释它的含义以及它与Android开发的关系。

SOLID原则背景

SOLID是在2000年早期由Robert Martin (AKA: Uncle Bob)和Michael Feathers领导的团队一起提出的。这五个面向对象设计的基本原则,能够极大的帮助开发人员,增强系统的可维护性以及可扩展性。

如果你不熟悉Uncle BobMichael 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);
    }

你可能会想:“这很容易啊,这难道不是很简单么?”。难道总是那么容易么?就像大部分软件的答案一样,“这取决于....”。

让我们更深入一些....

u=1687116067,4001588048&fm=27&gp=0.jpg

“职责”的含义是什么呢?

我相信很难找到比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...

MVP在RecyclerView中的使用

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

推荐阅读更多精彩内容