原文地址——D is for the Dependency Inversion Principle——Donn Felker@H_403_2@
欢迎来到SOLID在Android中的实践最后一章。最后,我们来介绍SOLID的D字母,它代表了依赖反转原则(The Dependency Inversion Principle ——DIP)。@H_403_2@
如果你错过了前面的篇章:@H_403_2@
不再浪费时间,第五个也是最后的原则介绍:
依赖反转原则告诫开发者必须遵守两项原则:@H_403_2@
High-level modules should not depend on low-level modules. Both should depend on abstractions.@H_403_2@ @H_403_23@
和@H_403_2@
Abstractions should not depend on details. Details should depend on abstractions.@H_403_2@ @H_403_23@
简单地,依赖反转可以这样解释:@H_403_2@
Depend on Abstractions. Do not depend on concretions.@H_403_2@ @H_403_23@
迁移实现依赖反转
为了完全理解这个原则,重点需要一下介绍传统软件的分层模式。让我们先回顾传统分层架构,接着做出一些修改使其符合依赖反转原则。@H_403_2@
在传统软件的分层架构中,高层次的模块需要依赖于低层次模块来实现功能。例如下面这个常见的结构(或者你的应用也在使用):@H_403_2@
Android UI → Business Rules → Data Layer@H_403_2@ @H_403_23@
这是三层架构,UI层(UI Layer,例如Android UI)包括我们所有的控件——
list
,textview
,动画等界面相关的东西。之后是业务层(Business Layer),它是实现核心功能逻辑的一层,有时候也被叫做Domain Layer
或者Service Layer
。最后是数据层(Data Layer),它是所有应用数据存储的地方,有可能是数据库,API或者文件等负责储存和取出数据。@H_403_2@假设我们做一个记账应用,可以帮助用户记录开销。使用传统的架构,当用户进行一次记账时,我们需要三种不同的工作:@H_403_2@
- 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层依赖于业务层的实例:@H_403_2@
BusinessLayer bl = new BusinessLayer();它把UI层与业务层永远耦合在一起,一旦脱离了业务层,UI层就无法正常地工作。@H_403_2@
同样,业务层也违反了依赖反转原则——依赖于数据层的实例,看这行代码:@H_403_2@
DataLayer dl = new DataLayer();如何打破这个依赖链呢?如果高层次模块不依赖于低层次模块,那如何工作呢?@H_403_2@
我们不需要一个万能类,记住SOLID的第一条原则——单一职责。@H_403_2@
幸运的是,在应用中我们可以通过抽象来连接各个层级,这符合依赖反转原则。把传统的层级架构转换成依赖反转架构,需要使用控制反转。@H_403_2@
实现控制反转
控制反转并不是把架构翻转过来,低层次模块肯定不能依赖于高层次模块。我们要从两端把整个关系倒置过来。@H_403_2@
怎么做呢?抽象。@H_403_2@
Java语言中,有几种方式可以创建抽象,例如抽象类和接口。我更推荐使用接口,使得层级之间的连接更整洁。接口定义了一份所有可能实现方法的协议。@H_403_2@
因此,每个层级都能依赖于接口,也就是抽象,而不是依赖于具体实现。@H_403_2@
在Android Studio里很容易实现,假设有个数据层长这样:@H_403_2@
如果需要依赖于抽象,可以从类中抽取接口,像这样:@H_403_2@
现在你就可以依赖于接口了。然而,
DataLayer
的具体实现仍旧被业务层使用。回到业务层,在其构造方法中使用依赖注入:@H_403_2@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。@H_403_2@简单来说,为了创建
BusinessLayer
对象,要先创建继承于IDataLayer
的实例。业务层并不关心是谁继承的,只需要这个实现类就够了。@H_403_2@那数据层从哪里来呢?它是由创建业务层的对象生成的。在这个例子中,就是Android UI创建了业务层对象。在上面的例子中,UI层因为创建了业务层的具体实现产生了耦合,所以需要业务层也是一个抽象。@H_403_2@
这时我进行相同的步骤Refactor–>Extract–>Extract Interface ,同样在Android UI层创建了这样一个抽象@H_403_2@
// 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); } }最终,高层次的模块依赖于抽象(接口),更进一步,抽象并不依赖于实现,而是依赖于抽象。@H_403_2@
记住,UI层依赖于业务层的抽象接口,业务层依赖于数据层的抽象接口。拥抱抽象吧。@H_403_2@
在Android中聚集
有个难题是,应用存在一个入口,典型的是
Activity
和Framgent
(Application
是不行的,我们只关注活动的屏幕会话)。你也许在想,如果Android UI是最顶层,那它怎么依赖于抽象呢?@H_403_2@嗯,这里有几种方法可以解决,例如使用Android的创建模式 factory , factory method pattern,或者其他依赖注入框架。@H_403_2@
我建议使用依赖注入框架,这样就无需手动创建对象,可以这样写代码@H_403_2@
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 作为依赖注入框架,有很多视频和教程指导如何实现依赖注入。@H_403_2@
如果不使用任何模式或框架,代码看起来长这样@H_403_2@
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层最后还是会违背依赖反转原则。@H_403_2@
接口隔离模式
- 把接口写在离类很近的地方并实现
- 把接口写在特定的包里
第一种方法比较简单,易于理解,缺点是共享接口比较麻烦。
第二种方法是将所有的抽象接口放进自己的包,让你的实现者引用这个包来访问的接口。这具有更高的扩展性。缺点是多一个包需要维护可能实现的类,增加了复杂性。
决定权还是在应用构建的模式。@H_403_2@结论
依赖反转原则是我写应用必须实践的原则。每个应用最终都使用了框架,想Dragger,帮助管理对象的生命周期。依赖抽象让我写出了简单,更易于测试的应用程序代码。@H_403_2@
推荐学习并使用Dragger,学成之日,就是你掌握依赖反转原则之时。@H_403_2@
一旦你越过依赖注入和依赖倒置的鸿沟,你会想知道以往没有他们是如何都能够度日的。@H_403_2@