如何用 Clean 架构开发 Android 应用
ho3638
8年前
<p>自我开始开发 Android 应用以来就有一种感觉——我可以把它做得更好。在我的职业生涯中,我看到过不少烂代码,其中一些还是我自己写的。Android 的复杂性和烂代码势必造成大问题。所以,从错误中汲取教训并持续改善十分重要。在多次尝试寻找更好的开发方式后,我遇到了 <strong>Clean 架构(</strong> <strong>简洁架构)</strong> 。于是我将其应用在了 Android 开发中,并结合我的开发经验做了调整,写出了这篇我觉得较为实用、值得分享的文章。</p> <p>最近我用 Clean 架构为客户构建了 app,并收到了很好的反馈。因此,在这篇文章中我会手把手教你如何用 Clean 架构开发 Android 应用。</p> <h3>什么是 Clean 架构?</h3> <p>有许多文章已经对 Clean 架构的概念做过介绍。在此我讲一讲 Clean 架构的核心内容。</p> <p>通常所说的 Clean,是指代码被分为像洋葱状的多个层,其 <strong>规则基础</strong> :内层不需要知道外层在干什么。即 <strong>向内依赖</strong> 。</p> <p>这是上一段内容的直观呈现:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/42569eaf16a7477ad87f84e9ac5f1cea.jpg"></p> <p style="text-align:center"><em> 简洁架构极佳的视觉表现。图片来自</em> <em> Uncle Bob 。 </em></p> <p>文中提到的 Clean 架构会给代码提供一下属性:</p> <ul> <li> <p>不依赖框架。</p> </li> <li> <p>可测试。</p> </li> <li> <p>不依赖 UI。</p> </li> <li> <p>不依赖数据库。</p> </li> <li> <p>不依赖其它外部力量</p> </li> </ul> <p>我希望你能理解这几点在下面的示例中是如何体现的。更多关于 Clean 架构的解释,我推荐你看看这篇 <a href="/misc/goto?guid=4959642350959208187" rel="nofollow,noindex">文章</a> 和这个 <a href="/misc/goto?guid=4959628847444832162" rel="nofollow,noindex">视频</a> 。</p> <p>这在 Anroid 中意味着什么</p> <p>一般来说,你的应用可以有任意数量的层,除非你的 Android 应用包含企业级的业务逻辑,最常见的是3层:</p> <ul> <li> <p>外层:实现层</p> </li> <li> <p>中间层:接口适配层</p> </li> <li> <p>内层:业务逻辑层</p> </li> </ul> <p>实现层是框架要求所有事情发生的地方。构架代码 <strong>包括每行代码都不是在解决你要解决的问题</strong> ,比如所有 Android 开发者都喜欢创建的 Activity 和 Fragment,发送 Intent,以及其它网络和数据库相关的框架代码。</p> <p>接口适配层的目标是连接业务逻辑和框架代码。</p> <p>最重要的问题是 <strong>业务逻辑层</strong> 。这里是你的应用中实际解决问题的地方。这里不会有框架代码,你 <strong>应该能在没有模拟器支持下运行这部分代码</strong> 。这样你的业务逻辑代码才 <strong>容易测试、开发和维护</strong> 。这是 Clean 架构的主要优势。</p> <p>核心层之上的每一层都需要为下一层转换模型结构。内层不会引用外层的模型,但外层可以使用内层的模型。这也是前面提到的 <strong>依赖规则</strong> 。虽然这样做会导致更大的开销,但能确保各层代码之间的解耦。</p> <p><strong>为什么需要模型转换?</strong> 举个例子,当逻辑层的模型不能直接很优雅地展现给用户,或是需要同时展示多个逻辑层的模型时,最好创建一个 ViewModel 类来更好的进行 UI 展示。这样可以在外层使用转换器类将业务模型转换成合适的 ViewModel。</p> <p>另一个例子:假设你要从外部数据层的 <strong>ContentProvider</strong> 得到一个 <strong>Cursor</strong> 对象,外层要先把它转换成内层的业务模型,再送给你的业务逻辑层进行处理。</p> <p>文末我会给出更多相关资源,以便你了解更多相关信息。现在我们已经了解 Clean 架构的基本原理,接下来我们需要用代码示例进行说明:用 Clean 架构构建一个示例功能。</p> <h3>怎样开始构建一个 Clean 应用?</h3> <p>我做了一个 样板项目 ,它为你提供了所有的底层命令。这是一个 <strong>Clean </strong> <strong>启动包</strong> ,在设计之初就包含最常用的一些工具包。你可 <strong>免费</strong> 下载和修改,还能用它建立自己的应用程序。</p> <p>你可以在这里找到入门项目: Android Clean Boilerplate</p> <h3>开始编写新用例</h3> <p>本节将解释所有需要编写的代码,你可通过上一节提供的样板文件使用 Clean 方法创建一个示例。 一个示例只代表应用程序中的部分独立功能。 用户(例如,在点击时)可以选择启用或不启用。</p> <p>首先我们来解释这种方法的结构和术语。这里要说的是我如何构建应用程序,其方法并不固定,你可根据你的需求组织不同的结构。</p> <p>结构</p> <p>一般的 Android 应用结构如下:</p> <ul> <li> <p>外层包:UI、Storage、Network 等。</p> </li> <li> <p>中层包:Presenters, Converters</p> </li> <li> <p>内层包:Interactors、Models、Repositories、Executor</p> </li> </ul> <p>外层</p> <p>上面已经提到过,这里是框架的细节。</p> <p>UI —包括 Activite、Fragment、Adapter 和其它用户界面相关的代码。</p> <p>Storage —数据库相关代码,实现 Interactor 需要使用的接口,用于访问和存储数据。包含如 <a href="/misc/goto?guid=4959665208256396530" rel="nofollow,noindex"> <strong>ContentProviders</strong> </a> 或者像 <strong> <a href="/misc/goto?guid=4958973332368857376" rel="nofollow,noindex">DBFlow</a> </strong> 这样的 ORM。</p> <p>Network —类似 <strong> <a href="/misc/goto?guid=4958837204152834453" rel="nofollow,noindex">Retrofit</a> </strong> 的网络操作。</p> <p>中层</p> <p>粘合代码层,将实现细节与业务逻辑连接起来。</p> <p>Presenters —处理来自 UI 的事件(比如用户单击)或者常用作内层(Interactor)的回调。</p> <p>Converters —转换器对象负责把内部模型转换为外部模型,反之亦然。</p> <p>内层</p> <p>核心层包含大部分高等级代码。 <strong>这里的所有类都是 POJO</strong> 。这一层中的类和对象都不是特定运行在 Android 应用中,可以非常容易的移植到其它 JVM 运行。</p> <p>Interactors- 这些是实际 <strong>包含业务逻辑代码</strong> 的类。这些类在后台运行,并使用回调向上层传递事件。在一些项目中,它们也被称为用例(可能是一个合适的名称)。在您的项目中可能有很多小的用于解决特定问题 Interactor 类,这属正常现象。可以说,它符合 <a href="/misc/goto?guid=4958987564349588981" rel="nofollow,noindex"> <strong>单一责任原则</strong> </a> ,而且这样的理解更容易让人接受。</p> <p>Models- 这些是您在业务逻辑中处理的业务模型。</p> <p><strong>Repositories</strong> - 此包仅包含数据库或其他外层实现的接口。Interactors 使用这些接口来访问和存储数据。也称为 <a href="/misc/goto?guid=4959731310801182451" rel="nofollow,noindex">仓库模式</a> 。</p> <p>Executor- 此包包含用于调用工作线程执行器在后台执行 Interactors 的代码。这个包一般不需要你修改任何部分。</p> <p>一个简单的示例</p> <p>在这个示例中,我们的用例是: <strong> <em>“在 app 启动时读取存储在数据库中的消息并展示。“</em> </strong> 此示例将会展示如何使用下面三个程序包来完成用例的功能:</p> <ul> <li> <p>presentation 包(展示包)</p> </li> <li> <p>storage 包(存储包)</p> </li> <li> <p>domain 包(主包)</p> </li> </ul> <p>前两个属于外层实现,最后一个属于内部/核心层实现。</p> <p>Presentation 包主要负责所有与屏幕显示相关的部分——包括全部的 MVP 栈,即包括 UI 和 presenter 这两个不同层的组件。</p> <h3>编写新的 Interactor (内部/核心层)</h3> <p>事实上你可以从架构的任意层开始编码,但是我还是推荐你首先从核心业务逻辑开始。因为逻辑代码写好之后可以测试,不需要 activity 也可以正常运行。</p> <p>所以我们先从创建一个 Interactor 开始。Interactor 是用例主逻辑实现的地方。 所有的 Interactors 都运行在后台线程,因此应该不会对 UI 展示造成影响。 我们在这里新建一个 Interactor,叫做 <strong>WelcomingInteractor</strong> 。</p> <pre> <code class="language-java">public interface WelcomingInteractor extends Interactor { interface Callback { void onMessageRetrieved(String message); void onRetrievalFailed(String error); } }</code></pre> <p>Callback 负责和主线程中的 UI 交互,我们之所以将其放在 Interactor 接口中是因为我们不需要将其重新命名为 WelcomingInteractorCallback——用于将其与其他回调区分。下面让我们实现取回消息的逻辑。假设我们有一个 Interactor 的 <strong>MessageRepository</strong> ,可以给我们发送欢迎消息。</p> <pre> <code class="language-java"> MessageRepository { String getWelcomeMessage(); }</code></pre> <p>下面让我们参考业务逻辑实现 Interactor 接口。 我们的实现必须扩展自 <strong>AbstractInteractor,这样代码就能在后台执行了。</strong></p> <pre> <code class="language-java">public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor { ... private void notifyError() { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onRetrievalFailed("Nothing to welcome you with :("); } }); } private void postMessage(final String msg) { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onMessageRetrieved(msg); } }); } @Override public void run() { // retrieve the message final String message = mMessageRepository.getWelcomeMessage(); // check if we have failed to retrieve our message if (message == null || message.length() == 0) { // notify the failure on the main thread notifyError(); return; } // we have retrieved our message, notify the UI on the main thread postMessage(message); }</code></pre> <p>WelcomingInteractor 运行方法。</p> <p>这里尝试获取了数据,并发送消息或者错误码到 UI 层用于显示。我们通过 Callback 通知 UI,这个 Callback 扮演的是 presenter 的角色。 这段代码是我业务逻辑的关键。其他框架都是依赖于框架本身。</p> <p>让我们看一下 Interactor 究竟有哪些依赖:</p> <pre> <code class="language-java">import com.kodelabs.boilerplate.domain.executor.Executor; import com.kodelabs.boilerplate.domain.executor.MainThread; import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor; import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor; import com.kodelabs.boilerplate.domain.repository.MessageRepository;</code></pre> <p>正如你所看到的,这里 <strong>没有提到任何 Android 代码,</strong> 这就是 Clean 架构的 <strong>主要好处</strong> 。你可以看到 <strong>框架</strong> <strong>的独立性</strong> 。 另外,我们不需要关注 UI 或数据库的细节,我们只是调用外层实现的接口方法。</p> <h2>测试 Interactor</h2> <p>现在我们可以 <strong>脱离仿真器运行并测试 Interator</strong> 。来写个简单的 <strong>JUnit</strong> 测试确保它有效。</p> <pre> <code class="language-java">... @Test public void testWelcomeMessageFound() throws Exception { String msg = "Welcome, friend!"; when(mMessageRepository.getWelcomeMessage()) .thenReturn(msg); WelcomingInteractorImpl interactor = new WelcomingInteractorImpl( mExecutor, mMainThread, mMockedCallback, mMessageRepository ); interactor.run(); Mockito.verify(mMessageRepository).getWelcomeMessage(); Mockito.verifyNoMoreInteractions(mMessageRepository); Mockito.verify(mMockedCallback).onMessageRetrieved(msg); }</code></pre> <p>这个 Interactor 代码并不知道它会用在 Android 应用中。这证明了上面提到的第二点——我们的业务逻辑是 <strong>可测试的</strong> 。</p> <h2>编写展现层</h2> <p>展现代码属于简洁框架的 <strong>外层</strong> 。它由向用户呈现界面的框架代码组成。我们使用 <strong>MainActivity</strong> 类在用户回到应用的时候向用户显示欢迎信息。</p> <p>我们从 <strong> Presenter</strong> 和 <strong>View </strong> 开始写界面。视图需要干的唯一一件事情就是显示欢迎信息:</p> <pre> <code class="language-java">public interface MainPresenter extends BasePresenter { interface View extends BaseView { void displayWelcomeMessage(String msg); } }</code></pre> <p>那么,用户回到应用的时候,应该如何开始 Interactor 呢?一切不严格相关的东西都应该放在 Presenter 类中。这有助于组织 <strong> <a href="/misc/goto?guid=4959630663646375669" rel="nofollow,noindex">离散的关系</a> </strong> 并防止 Activity 变得臃肿。这包括所有用 Interator 运行的代码。</p> <p>在 <strong>MainActivity</strong> 类中重载 <strong> <em>onResume()</em> </strong> 方法:</p> <pre> <code class="language-java">@Override protected void onResume() { super.onResume(); // let's start welcome message retrieval when the app resumes mPresenter.resume(); }</code></pre> <p>所有 <strong>Presenter</strong> 对象都要在实现 <strong>BasePresenter</strong> 的时候实现 <strong> <em>resume()</em> </strong> 方法。</p> <p><strong>注意:</strong> 有些敏锐的读者会发现我在 BasePresenter 接口中添加了 Android 的生命周期方法,即使 Presenter 在较低层。Presenter 不会获知 UI 层的任何内容——比如它的生命周期。然而,我并没有指定 Android 特定的 * <em>事件</em> * ,因为每个 UI 都需要向用户展示。想像一下,我调用的是 <strong>onUIShow()</strong> 而不是 <strong>onResumt(),</strong> 结果会怎么样呢。一切运行良好,不是吗?:)</p> <p>所有的 Presenter 在继承 BasePresenter 时都要实现 resume() 方法。我们在 <strong>MainPresenter</strong> 的 <em> <strong>Resume()</strong> </em> 方法中启动 Interactor。</p> <pre> <code class="language-java">@Override public void resume() { mView.showProgress(); // initialize the interactor WelcomingInteractor interactor = new WelcomingInteractorImpl( mExecutor, mMainThread, this, mMessageRepository ); // run the interactor interactor.execute(); }</code></pre> <p><em>execute()</em> 方法会在后台线程中执行 <strong>WelcomingInteractorImpl</strong> 的 <strong> <em>run()</em> </strong> 方法。而 <strong> <em>run()</em> </strong> 方法在 <strong> <em>编写新的 I</em> </strong> <strong> <em>nteractor</em> </strong> 一节中会有介绍。</p> <p>你可能注意到 Interactor 的行为与 <strong>AsyncTask</strong> 相类似,都是在提供所需东西后运行。那为什么不使用 AsyncTask 呢?因为这是 <strong>Android 代码</strong> ,需要模拟器才能运行或测试。</p> <p>我们为 Interfactor 提供下列属性:</p> <ul> <li> <p>ThreadExecutor实例负责在后台线程中执行 Interactor。我通常会使用单例模式。这个类实际驻留在 <strong>域</strong> 包中,不需要在外层实现。</p> </li> <li> <p>MainThreadImpl实例负责在主线程上从 Interactor 发送可运行对象。主线程可以使用框架代码访问,因此这个类需要在外层实现。</p> </li> <li> <p>你可能注意到我们向 Interactor 提供了 <strong> <em>this ,</em> </strong> 因为 <strong>MainPresenter</strong> 也是一个 Callback 对象,Interactor 会用它在事件回调中更新 UI。</p> </li> <li> <p>WelcomeMessageRepository实现了 Interactor 用到的 <strong>MessageRepository</strong> 接口,所以我们提供了它的实例。 <strong>WelcomeMessageRepository</strong> 会在 <strong> <em>编写存储层</em> </strong> 一节中详述。</p> </li> </ul> <p><strong>注意:</strong> 因为每次都需要向 Interactor 提供许多属性,将 Dagger 2 依赖注入框架会提供不少帮助。简明起见,此处没有将其注入。你可根据实际情况选择使用。</p> <p>为什么 <em> <strong>this</strong> </em> 也是 Callback 呢?,因为 <strong>MainActivity</strong> 的 <strong>MainPresenter </strong> 实现了 Callback 接口:</p> <pre> <code class="language-java">public class MainPresenterImpl extends AbstractPresenter implements MainPresenter, WelcomingInteractor.Callback {</code></pre> <p>我们监听的事件来自于 Interactor 。这段代码来自于 <strong>MainPresenter:</strong></p> <pre> <code class="language-java">@Override public void onMessageRetrieved(String message) { mView.hideProgress(); mView.displayWelcomeMessage(message); } @Override public void onRetrievalFailed(String error) { mView.hideProgress(); onError(error); }</code></pre> <p>在代码段中我们看到的 View 其实就是实现了 MainPresenter.View 接口的 <strong>MainActivity</strong> :</p> <pre> <code class="language-java">public class MainActivity extends AppCompatActivity implements MainPresenter.View {</code></pre> <p>它负责显示欢迎信息:</p> <pre> <code class="language-java">@Override public void displayWelcomeMessage(String msg) { mWelcomeTextView.setText(msg); }</code></pre> <p>这差不多就是表示层的内容了。</p> <h3>编写存储层</h3> <p>repository 中的接口就在存储层实现。所有数据库相关的代码都在这里。仓库模式只是表达数据来源。但我们的主要业务逻辑不在乎首数据的来源——不管它是来自数据库、服务器还是文本文件。</p> <p>对于复杂的数据,你可以使用 <strong>ContentProviders</strong> 或者像 <strong> DBFlow </strong> 这样的 ORM 工具处理。如果你需要从 Web 接收数据,那就会用到 <strong>Retrofit</strong> 。如果你需要简单的键值对存储,那你会用到 <strong>SharedPreferences</strong> 。不管怎样,你需要选择正确的工具。</p> <p>我们的数据库并不是真正的数据库,它只是一个简单的类,通过延迟来模拟:</p> <pre> <code class="language-java">public class WelcomeMessageRepository implements MessageRepository { @Override public String getWelcomeMessage() { String msg = "Welcome, friend!"; // let's be friendly // let's simulate some network/database lag try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return msg; } }</code></pre> <p>就 <strong>WelcomingInteractor</strong> 而言,延迟的原因可能是由真实网络或其他原因造成的,但它并不在乎,它只需要数据提供者实现 <strong>MessageRepository</strong> 接口就好。</p> <p>这个示例已经 放在 GitHub上。各个类之间的调用关系总结如下:</p> <p>MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity</p> <p>注意这个控制流程,这非常重要:</p> <p><strong>Outer — Mid — Core — Outer — Core — Mid — Outer</strong></p> <p>在一个用例中多次访问外层是很常见的事情。如果你要显示点什么,存储点什么并从 Web 访问些什么,控制流至少需要访问外层三次。</p> <p>对于我来说,这是迄今为止开发应用程序的最佳方式。解耦的代码能让人把注意力放在具体的问题上,而不受其他事件干扰。这是一个不错的 <a href="/misc/goto?guid=4959731310917732366" rel="nofollow,noindex">SOLID</a> 方法,但我们还需要一些时间适应。希望这篇文章的示例能让你对该内容有进一步了解。</p> <p>我还使用 Clean 架构建立了一个开源的成本跟踪应用,它能展示一项应用的编码。</p> <h3> </h3> <p>来自:https://www.oschina.net/translate/developing-android-apps-using-the-clean-architecture-pattern</p> <p> </p>