[译]依赖反转在Android中的实践

原文地址——D is for the Dependency Inversion Principle——Donn Felker

欢迎来到SOLID在Android中的实践最后一章。最后,我们来介绍SOLID的D字母,它代表了依赖反转原则(The Dependency Inversion Principle ——DIP)。

如果你错过了前面的篇章:

不再浪费时间,第五个也是最后的原则介绍:
依赖反转原则告诫开发者必须遵守两项原则:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

简单地,依赖反转可以这样解释:

Depend on Abstractions. Do not depend on concretions.

迁移实现依赖反转

为了完全理解这个原则,重点需要一下介绍传统软件的分层模式。让我们先回顾传统分层架构,接着做出一些修改使其符合依赖反转原则。

在传统软件的分层架构中,高层次的模块需要依赖于低层次模块来实现功能。例如下面这个常见的结构(或者你的应用也在使用):

Android UI → Business Rules → Data Layer

这是三层架构,UI层(UI Layer,例如Android UI)包括我们所有的控件——listtextview,动画等界面相关的东西。之后是业务层(Business Layer),它是实现核心功能逻辑的一层,有时候也被叫做Domain Layer或者Service Layer。最后是数据层(Data Layer),它是所有应用数据存储的地方,有可能是数据库,API或者文件等负责储存和取出数据。

假设我们做一个记账应用,可以帮助用户记录开销。使用传统的架构,当用户进行一次记账时,我们需要三种不同的工作:

  • UI层——输入数量
  • 业务层——校验数据合法性
  • 数据层——持久化数据

代码这样写

findViewById(R.id.save_expense).setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
        ExpenseModel expense = //... create the model from the view values
        BusinessLayer bl = new BusinessLayer();
        if (bl.isValid(expense)) {
           // Woo hoo! Save it and Continue to next screen/etc
        } else {
           Toast.makeText(context, "Shucks, couldnt save expense. Erorr: " + bl.getValidationErrorFor(expense), Toast.LENGTH_SHORT).show();
        }
    } 
});

在业务层这样写一些伪代码

// in the business layer, return an ID of the expense
public int saveExpense(Expense expense) {
    // ... some code to check for validity ... then save
    // ... do some other logic, like check for duplicates/etc
    DataLayer dl = new DataLayer(); 
    return dl.insert(expense);  
    }

这些代码的问题在于违反了依赖反转原则:高层次的模块依赖不应该依赖于低层次的模块,它们都应该依赖于抽象。下面这行代码,UI层依赖于业务层的实例:

    BusinessLayer bl = new BusinessLayer();

它把UI层与业务层永远耦合在一起,一旦脱离了业务层,UI层就无法正常地工作。

同样,业务层也违反了依赖反转原则——依赖于数据层的实例,看这行代码:

DataLayer dl = new DataLayer();

如何打破这个依赖链呢?如果高层次模块不依赖于低层次模块,那如何工作呢?

我们不需要一个万能类,记住SOLID的第一条原则——单一职责

幸运的是,在应用中我们可以通过抽象来连接各个层级,这符合依赖反转原则。把传统的层级架构转换成依赖反转架构,需要使用控制反转。

实现控制反转

控制反转并不是把架构翻转过来,低层次模块肯定不能依赖于高层次模块。我们要从两端把整个关系倒置过来。

怎么做呢?抽象。

Java语言中,有几种方式可以创建抽象,例如抽象类和接口。我更推荐使用接口,使得层级之间的连接更整洁。接口定义了一份所有可能实现方法的协议

因此,每个层级都能依赖于接口,也就是抽象,而不是依赖于具体实现。

Android Studio里很容易实现,假设有个数据层长这样:

solid-5-datalayer-concrete.png

如果需要依赖于抽象,可以从类中抽取接口,像这样:

solid-5-extract-interface.gif

现在你就可以依赖于接口了。然而,DataLayer的具体实现仍旧被业务层使用。回到业务层,在其构造方法中使用依赖注入:

    public class BusinessLayer {
        
        private IDataLayer dataLayer;
    
        public BusinessLayer(IDataLayer dataLayer) {
            this.dataLayer = dataLayer;
        }
    
        // in the business layer, return an ID of the expense
        public int saveExpense(Expense expense) {
            // ... some code to check for validity ... then save
            // ... do some other logic, like check for duplicates/etc
            return dataLayer.insert(expense);
        }
    }

业务层这样就能依赖于抽象了——IDataLayer接口。数据层通过构造方法进行依赖注入的方法叫Constructor Injection

简单来说,为了创建BusinessLayer对象,要先创建继承于IDataLayer的实例。业务层并不关心是谁继承的,只需要这个实现类就够了。

那数据层从哪里来呢?它是由创建业务层的对象生成的。在这个例子中,就是Android UI创建了业务层对象。在上面的例子中,UI层因为创建了业务层的具体实现产生了耦合,所以需要业务层也是一个抽象。

这时我进行相同的步骤Refactor–>Extract–>Extract Interface ,同样在Android UI层创建了这样一个抽象

// This could be a fragment too ... 
public class MainActivity extends AppCompatActivity {

    IBusinessLayer businessLayer; 
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

最终,高层次的模块依赖于抽象(接口),更进一步,抽象并不依赖于实现,而是依赖于抽象。

记住,UI层依赖于业务层的抽象接口,业务层依赖于数据层的抽象接口。拥抱抽象吧。

在Android中聚集

有个难题是,应用存在一个入口,典型的是ActivityFramgent(Application是不行的,我们只关注活动的屏幕会话)。你也许在想,如果Android UI是最顶层,那它怎么依赖于抽象呢?

嗯,这里有几种方法可以解决,例如使用Android的创建模式 factoryfactory method pattern,或者其他依赖注入框架。

我建议使用依赖注入框架,这样就无需手动创建对象,可以这样写代码

    public class MainActivity extends AppCompatActivity {
    
        @Inject IBusinessLayer businessLayer;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            
            // businessLayer is null at this point, can't use it.  
            getInjector().inject(this); // this does the injection
            // businessLayer field is now injected valid and NOT NULL, we can use it
            
            // do something with the business layer ... 
            businessLayer.foo()
        }
    }

建议使用 Dagger 作为依赖注入框架,有很多视频和教程指导如何实现依赖注入。

如果不使用任何模式或框架,代码看起来长这样

public class MainActivity extends AppCompatActivity {

    IBusinessLayer businessLayer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        businessLayer = new BusinessLayer(new DataLayer());
        businessLayer.foo()
    }
}

当然这里看起来并不是很糟,最终你会发现对象会变得臃肿,初始化实例会容易出错,容易违反SOLID原则,而且改动代码的时候会很容易出错。如果没有使用框架,UI层最后还是会违背依赖反转原则。

接口隔离模式

有两种方法,可以根据喜好决定:

  • 把接口写在离类很近的地方并实现
  • 把接口写在特定的包里

第一种方法比较简单,易于理解,缺点是共享接口比较麻烦。
第二种方法是将所有的抽象接口放进自己的包,让你的实现者引用这个包来访问的接口。这具有更高的扩展性。缺点是多一个包需要维护可能实现的类,增加了复杂性。
决定权还是在应用构建的模式。

结论

依赖反转原则是我写应用必须实践的原则。每个应用最终都使用了框架,像Dagger,帮助管理对象的生命周期。依赖抽象让我写出了简单,更易于测试的应用程序代码。

推荐学习并使用Dagger,学成之日,就是你掌握依赖反转原则之时。

一旦你越过依赖注入和依赖倒置的鸿沟,你会想知道以往没有他们是如何都能够度日的。

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

推荐阅读更多精彩内容