1. 整体设计与思路拆解
1.1 三层架构到底分的是什么
先聊一个我特别想纠正的误区。很多人一听说三层架构,第一反应就是"表现层、业务层、数据层"这三个词背下来,然后开始往项目里套目录。但真正干过几年项目的人都知道,三层架构最难的不是分层,而是"知道你正在写的这行代码应该属于哪一层"。
我见过太多项目,包名看起来是标准的model、view、presenter三层,结果点进去一看,presenter里写满了JSON解析,view里直接操作SQLite,model层躺着几百行网络请求。这种项目不是没分层,是分了个寂寞。
三层架构的本质,是把"界面怎么显示"、"业务怎么处理"、"数据从哪来"这三件完全不同的事拆开。表现层只负责把用户看到的东西画出来,把用户的操作传出去;业务层只负责业务流程、逻辑判断、状态流转,它不关心按钮长什么样;数据层只负责从网络、数据库、文件里把数据取出来或者存进去,它不关心界面上的事。
这个拆分的核心逻辑是"变化点隔离"。界面是变化最快的,今天改个样式,明天加个弹窗;业务规则是变化较慢的,偶尔改改判断逻辑;数据来源是最容易被替换的,今天用远程接口,后天可能改成本地缓存。如果这三类东西混在一起,任何一个变化都会牵一发动全身。反过来,如果分开了,界面变化不影响业务,换数据源也不影响业务,这才是分层真正的价值。
对于MVP模式来说,三层架构的映射其实很自然:View对应表现层,Presenter对应业务层,Model对应数据层。但要注意,这里的Model不是"数据模型类",而是"数据访问与数据源管理"的统称。很多人写MVP时把Model理解成只放几个JavaBean,这是不对的。Model应该包含数据获取的完整链路:网络请求、数据库操作、缓存策略、数据转换,甚至仓库模式的封装。
1.2 MVP在三层架构里的定位
MVP在Android项目中的核心作用,是把View和业务逻辑彻底解耦。说的直白一点,Activity和Fragment本质上就是View,它们不应该去关心"数据怎么来的"和"业务规则是什么",只需要做三件事:初始化界面、接收用户操作并告诉Presenter、根据Presenter的回调更新界面。
Presenter承上启下。它从View接收事件,然后调用Model去拿数据,拿到结果后再决定View应该展示什么状态。这样一来,业务逻辑就被从Activity里"掏"了出来,放到了一个普通类里。这个普通类不依赖Android生命周期,可以单元测试,可以复用,这就是MVP最大的红利。
有人会问,那MVP和三层架构是不是重复了?不是。MVP解决的是表现层内部的"View和业务逻辑的分离",三层架构解决的是整个App的"界面、业务、数据"的层次划分。两者是配合关系:MVP让表现层内部不再臃肿,三层架构让整个项目从上到下都有清晰的归属。
我还见过一种混乱的写法,就是MVP三层各自为政,View直接new一个Model去拿数据,绕过了Presenter。这么写短期看起来省事,但长期一定会出问题:回调写在View里,导致View层代码爆炸;业务逻辑散落在数据回调里,改一个需求要动三个文件。MVP模式的约束恰恰是"单向依赖"——View只依赖Presenter,Presenter只依赖Model接口,Model不知道View和Presenter的存在。破坏了这个约束,MVP就名存实亡。
1.3 为什么标准化比分层本身更重要
分层的思路谁都能理解,但把分层做成"标准化"的,就不是每个人都能做到了。什么叫标准化?就是同一个团队里的任何一个人,拿到一个新需求,能清楚地知道代码写在哪、方法起什么名、接口怎么定义、数据走什么流程,不需要看文档也能靠"惯例"找到该改的文件。
标准化分层解决的是一个团队协作效率问题。我接手过一些个人开发者写的项目,架构不可谓不好,包结构清晰,命名也有讲究,但只有作者本人能维护。为什么?因为缺少统一约定。张三写的Presenter叫MainPresenter,李四写的叫HomePagePresenter,王五写的叫MainActivityPresenter,三个人写的代码风格完全不一样,协作起来成本极高。
标准化的核心是把"规范"沉淀为"代码骨架"和"文件模板"。比如规定所有Activity必须继承BaseActivity,所有Presenter必须继承BasePresenter,所有View接口必须继承BaseView,所有数据访问必须走Repository。这些约定一旦在项目里落地,新成员上手速度会快很多,review代码时也不用花大量时间讨论"这样写行不行"。
另外我要多说一句,标准化不是为了限制灵活性,而是为了把常规的、重复性的决策从日常开发中剥离出去。每天纠结"这个接口的命名风格"、"这个回调该放在哪一层"是非常消耗精力的事。标准化之后,这些决策都有了默认答案,开发者的精力可以投入到真正需要思考的业务问题上。
2. 核心细节解析与实操要点
2.1 一个可以直接复制的包结构
讲标准化的第一步,先给出一套我用了很久的包结构。这套结构在多个项目里验证过,不能说完美,但至少能让你在项目规模膨胀到几十个模块时,还能较快定位到目标文件。
com.example.project ├── base // 基类与通用组件 │ ├── BaseActivity.java │ ├── BasePresenter.java │ ├── BaseView.java │ └── BaseRepository.java ├── data // 数据层 │ ├── local // 本地数据源(数据库、SharedPreferences、文件) │ ├── remote // 远程数据源(网络接口) │ ├── model // 数据实体(Entity/DTO) │ └── repository // 仓库实现(对外暴露的统一数据入口) ├── ui // 表现层 │ ├── main // 按业务页面分包 │ │ ├── MainActivity.java │ │ ├── MainContract.java │ │ ├── MainPresenter.java │ │ └── MainAdapter.java │ ├── login │ └── profile └── utils // 工具类(非业务相关)注意几个关键点:第一,ui下面按页面分包而不是按类型分包,也就是说不要用activity包、adapter包、fragment包这种组织方式。按页面放的好处是,改一个页面功能时所有的相关文件都在同一个目录里,不需要跳来跳去。第二,data/remote只放接口定义和网络相关的封装,真正的数据拼装在repository里做。第三,model里放的是纯数据类,它们不依赖任何框架,可以被各层引用。
这套结构本质上是对三层架构的目录映射:ui对应表现层,presenter和contract属于业务逻辑的入口,data对应数据层。有同学会问,业务层到底体现在哪?MVP模式下,业务逻辑主要写在Presenter里,所以ui包下面的Presenter其实承载的就是业务层职责。如果你觉得业务逻辑太重,可以在ui和data之间加一个domain包专门放用例(UseCase),但对大多数中小型项目来说,Presenter直接面向Repository已经够用了,再加一层反而冗余。
2.2 命名规范背后的可维护性逻辑
命名规范是标准化里最容易被忽视却最影响长期维护的部分。我见过最崩溃的项目是:写一个获取用户信息的接口,有人叫getUserInfo,有人叫queryUserData,有人叫fetchUserFromServer,还有人叫getUserInfoById,这些方法放在同一个类里,功能几乎一样,只是参数略有不同。
我建议的命名规则是这样的。View接口里的方法用描述性动宾短语,比如showLoading、showEmptyView、setUserListData。Presenter里的方法用业务动作命名,比如loadUserList、submitOrder、handleLoginClick,尽量不要用onClick这种与UI控件绑定太死的名字,因为处理逻辑可能不只来自一个控件。Repository里的方法用数据动作命名,比如fetchUserList、saveOrderToLocal、deleteCache,让调用方一看就知道数据是从哪来的、要做什么。
Contract接口的命名也值得规范。我通常在一个页面级的功能模块里,把View接口和Presenter接口合并放在一个Contract类里,比如MainContract,内部定义interface View和interface Presenter。这样做的价值在于:打开MainContract,就能看到View和Presenter之间的所有交互契约。这在团队协作时特别有用,新人接手一个页面,只要看Contract就能知道页面有哪些交互、Presenter提供哪些能力。
还有一个小技巧:Model类不叫Model,而是根据数据来源定义后缀。网络返回的叫Response,本地表结构对应的叫Entity,跨层传递的业务对象叫Bean。有些人会觉得这样太麻烦,但在数据层出现混淆时,这些后缀能帮你快速判断"这个对象能不能往界面上传"。能往界面上传的只有Bean,Response和Entity都必须在数据层内部转换成Bean后再往外传。
2.3 依赖关系的硬性约束
分层设计的核心约束就是依赖方向:表现层依赖业务层,业务层依赖数据层,数据层谁都不依赖。在MVP的语境里,这句话会拆成更具体的几条规则。
第一,View不能直接操作数据层。我在代码review中经常看到Activity里直接写网络请求回调的代码,这表面上是"绕过了Presenter",实质上是把网络回调的线程切换和错误处理逻辑泄漏到了UI层。正确的做法是:View把用户操作"告诉"Presenter,Presenter决定去调哪个Repository方法,数据回来后由Presenter决定View该怎么显示。
第二,Presenter不能持有View的强引用。这个坑几乎每个写过MVP的人都踩过——Activity销毁了,但Presenter还在执行异步任务,回调时View已经不存在了,轻则空指针,重则内存泄漏。解决办法有两个:一是让Presenter持有View的弱引用,二是在生命周期结束时主动解绑。两种做法不互斥,我建议同时做,具体实现后面会详细讲。
第三,Model层不能反向依赖Presenter或View。很多人写数据层时,会把回调接口直接定义在Presenter内部,然后让Repository去持有这个回调,这就是反向依赖。正确的做法是:数据层只通过接口回调返回数据,这个接口的定义不依赖任何业务层面的东西,Presenter去实现这个接口来接收结果。如果有一天你发现共同的Model被两个业务模块复用,而Model里只有一份数据逻辑,说明你的分层设计是健康的。
依赖关系还需要在工程层面做保障。比如模块化项目里,可以用Gradle依赖配置限制模块之间的引用关系,比如data模块不能依赖ui模块。单一项目里则可以通过包名检查和code review来约束。我见过一个比较有效的做法是:在CI脚本里加一个依赖检查任务,扫描代码里是否出现了跨包引用,如果data包里的文件import了ui包下的类,构建直接失败。这种做法看着粗暴,但对维持架构纪律非常有效。
3. 实操过程与核心环节实现
3.1 Base层封装:把共性的东西沉淀下来
标准化的第一步是先搭好Base层。Base层是项目的地基,所有业务模块都要继承它,所以设计一定要慎重,不要图省事把所有东西都塞进去。我的经验是:Base层只放"跨模块可以复用且逻辑一致"的东西,如果一个方法只有一个页面用,就不要进Base。
BaseActivity是最常见的封装对象。我通常会在里面做这几件事:初始化布局的抽象方法、初始化View的抽象方法、提供一个全局的上下文和安全的事件分发。但有一点要注意,BaseActivity不要长成一个"万能类",不要在里面写公共的联网方法、公共的刷新方法,那些应该由各层的Base去解决。BaseActivity的生命周期回调要尽量精简,把逻辑留给子类覆盖。
BaseView接口是所有View接口的父接口,里面放的是所有页面都可能用到的通用UI状态方法,比如showLoading、hideLoading、showToast、showErrorPage。这样做的好处是,BasePresenter在调用这些方法时不需要知道具体的View实现类,只要面向BaseView接口编程就行。
BasePresenter负责两件事:绑定View和解除View绑定。我在BasePresenter里维护一个WeakReference的View引用,提供attachView和detachView两个方法。Presenter初始化时不需要传View,Activity在onCreate时调用attachView,在onDestroy时调用detachView。所有子类Presenter通过getView()方法来获取View引用,这样能保证在异步回调时,即使View已经销毁,也不会出现强引用导致的内存泄漏。
BaseRepository封装数据层的通用操作。比如统一的错误解析、线程调度、请求取消。但这里要克制,只放数据层共有的逻辑,不要把某个具体页面的接口放在这里。Repository更像是一个统一的数据出口门面,它内部组合了多个数据源,对外暴露的是业务模块需要的领域方法。
3.2 Presenter与View的桥接:Contract接口的设计
Contract接口是MVP标准化里最容易被低估的部分。很多人只把Contract当作一个声明了方法名的壳,并没有真正理解它在依赖治理中的作用。Contract其实定义了Presenter和View之间的一纸契约:View能做什么,Presenter能提供什么,都在里面说清楚了。
我的设计模式是一个页面一个Contract接口,内部放两个子接口。下面是一个典型示例:
public interface LoginContract { interface View extends BaseView { void showLoginSuccess(UserBean user); void showLoginError(String message); void setSubmitButtonEnabled(boolean enabled); } interface Presenter extends BasePresenter { void login(String username, String password); } }LoginActivity实现LoginContract.View,LoginPresenter实现LoginContract.Presenter。Activity持有一个Presenter实例,创建Presenter后调用attachView(this)。这样Activity就可以直接调用presenter.login(),而Presenter可以通过getView().showLoginSuccess()来回调。
为什么要把View接口和Presenter接口放在同一个文件而不是分开建两个文件?主要是为了方便。打开LoginContract,一眼就能看到这个页面的所有交互点:View要展示哪些状态,Presenter要提供哪些操作。对新人来说,这比去翻两个文件轻松得多。
需要注意的是,业务场景变化时Contract要同步更新。如果一个页面新增了一个下拉刷新,先改Contract接口再加实现,这个顺序不能反。先改Contract再实现的好处是,强迫你提前梳理清楚"这个操作应该由谁发起、结果由谁展示",而不是直接把代码写到Activity里补丁式地实现。
3.3 生命周期绑定与线程安全
MVP在Android里最经典的问题就是生命周期管理。我总结过一条经验:只要遇到"打开页面之后马上做网络请求,然后快速退出,崩溃了"这种问题,十有八九是生命周期处理不当。
解决方案不只是用WeakReference这么简单。你还需要判断:这个异步操作在Presenter触发时,View已经有被销毁的可能了。具体地说,在Presenter回调View的方法里加一个安全判断:
protected boolean isViewAttached() { return mViewRef != null && mViewRef.get() != null; }每次在回调里使用getView()之前,先调用isViewAttached()判断一下。如果没有附加View就直接return,不再执行UI更新逻辑。这是一种"延迟安全"的做法,虽然不能完全避免逻辑执行,但至少不会因为UI操作而崩溃。
还有一种情况是:页面销毁后,异步任务还在执行,数据很快会被丢弃。这种情况更好的处理方式是"生命周期感知的取消"。如果你用的是RxJava,可以在Activity的onDestroy时调用compositeDisposable.dispose();如果你用协程,可以在Presenter里维护一个Job,页面销毁时cancel掉。如果项目里还是用早期的回调式网络库,可以启用一个请求取消标记。
我特别想提醒的是,不要在View被销毁后还在回调里做重量级操作。比如拿到数据后写数据库、更新本地缓存,这些逻辑应该在数据层完成,而不是在View的回调里做。因为View销毁后这些操作不仅没有意义,还可能因为Context引用问题导致内存泄漏。
另外一个容易被忽略的点是把回调切换到主线程。Android要求UI操作必须在主线程执行,如果你的网络回调发生在子线程,直接调用getView().showLoginSuccess()就会出现CalledFromWrongThreadException。所以Base层里应该做好线程切换:所有的View回调统一切到主线程。这也是为什么我倾向于在BasePresenter里提供runOnUiThread方法,或者在BaseView接口的实现中统一做线程切换,而不是让每个子类去处理。
3.4 数据层仓库模式的统一封装
设计数据层的时候,我推荐使用仓库模式(Repository Pattern)。这是三层架构里数据层最经典的实践,它对外屏蔽了数据的来源,让上层调用方只关心"拿到数据"这个结果。
仓库模式的核心思想是,把"数据来自网络还是来自本地缓存"这个决策,从业务层下放到数据层。业务层不需要知道数据是实时的还是缓存的,只需要告诉Repository"给我用户列表",Repository自己决定去查缓存还是发请求,以及如何用缓存回填。
这是一个简单的Repository接口和实现:
public interface UserRepository { Observable<UserBean> fetchUserInfo(); } public class UserRepositoryImpl implements UserRepository { private final UserApi mUserApi; // 远程数据源 private final UserDao mUserDao; // 本地数据源 @Override public Observable<UserBean> fetchUserInfo() { // 先读缓存 UserBean cache = mUserDao.queryUser(); if (cache != null) { return Observable.just(cache); } // 缓存没有,走网络,成功写入缓存 return mUserApi.getUserInfo() .doOnNext(user -> mUserDao.saveUser(user)); } }Repository接口的粒度要跟业务对齐,不要按网络接口一比一映射。一个网络接口可以被多个业务场景复用,但Repository方法应该根据业务来定义。比如getUserInfo和getUserInfoFromNetwork是两个不同的方法,前者可能走缓存优先策略,后者强制刷新。业务层调哪个,完全看当时的需求场景。
还有一个经验是,不要把网络调用直接露在外面让Presenter去调用。如果Presenter直接操作Retrofit创建的Api接口,那么网络层的变更(比如加公共参数、改签名机制)就会涉及到所有Presenter,改动面很大。Repository存在的意义不仅在于缓存,还在于把网络层的实现细节隔离在数据层内部。
4. 从单一模块到项目级实践的要点
4.1 多模块MVP的协作模式
前面讲的都是单一模块内部的标准化,但项目规模大了之后,单一App模块会逐渐拆成多个Gradle模块,比如:app模块、common模块、data模块、business模块等。三层架构和MVP在这个阶段依然有效,但需要做一层适配。
app模块是入口,负责初始化各种框架和配置。ui相关的页面根据业务拆分成多个模块,比如login模块、home模块、profile模块。每个模块内部依然是MVP三层:Contract、Presenter、View、Adapter都在模块内部,模块对外暴露的只是一个Fragment或者Activity的入口类。
data模块是一个独立的Gradle模块,里面只放数据层的东西:网络接口、数据库、实体、Repository。ui模块之间的通信不直接依赖具体页面,而是依赖data模块暴露的接口。这种模式下,common模块承载Base层的东西,ui模块们懒加载注入Presenter的实现。
这种模式的好处是模块之间的依赖关系变成了"编译期可见"的,如果你在home模块中直接引用了login模块的类,编译就会报错。架构破坏在编译阶段就被拦截了,这就是前面说的依赖约束从"约定"上升到了"工程保障"。
4.2 标准化分层的落地步骤与迁移策略
如果你正在维护一个没有分层的项目,现在想引入三层架构+MVP,不建议一口气全部重写。一口气重写意味着大量的回归风险,业务测试要全部重跑,出问题后很难定位是新架构引入的bug还是原逻辑本身就有的问题。我建议走渐进式改造路线。
第一步,把与UI无关的业务逻辑从Activity/Fragment中搬出来,放到新建的Presenter里。这时候不需要急着建Contract接口,先做到"Activity只做View的事",把业务逻辑从Activity里剥离出去。做这一步时,先把最复杂、最常改动的页面拿出来改,比如首页、登录页、购物车页,不要先改一些边缘页面。
第二步,为每个改造完成的页面补一个Contract接口,把View的方法和Presenter的方法固定下来。这一步的意义是明确"交互边界"。当代码规模变大以后,这一步是防止"写着写着又回到Activity里写逻辑"的关键。
第三步,整理数据层。把散落各处的Retrofit、OkHttp、数据库操作统一收拢到data包的Repository中。这一步要注意,网络请求方法尽量先定义接口,再在Impl中实现,方便以后替换数据源时不影响上层业务。
迁移过程中最需要警惕的是"过渡期混乱":项目里既有旧的直连式代码,又有新的MVP代码。这种混搭会降低开发效率。我的建议是:在过渡期间,新写的代码必须走新的分层规范,旧的代码只有在被修改时才顺手迁移。不要专门安排一个大版本去重构全部旧代码,除非你有一个相对空闲的版本周期。
4.3 新项目如何导入这个规范
如果你是从零开始一个新项目,导入这套规范就简单得多。但你依然要避免一个坑:不要一上来就把所有抽象都建好,什么BaseViewModel、BaseRepositoryCallBack、IBaseListPage都写给子孙后代留着。过度设计是标准化最大的敌人。
我建议先按照最小可用的规范启动,比如:UI按页面分包,页面内MVP三层三个文件+Contract,数据层Repository两层(接口、实现),Adapter放在页面包内。先跑起来,在真实需求中逐渐补充AbstractPage、BaseList等更上层的抽象,而不是事先把它全都写出来。
项目初期,一个团队最需要的是"简单的规范"和"严格的自律"。"简单"意味着新人进来成本低,"自律"意味着三个人也要遵守一致的代码风格。很多新项目走上正轨之后的第一个难题不是功能写不出来,而是代码风格迅速就分叉了——今天张三写了一个方便快捷的直连网络,明天李四照着写,后天项目就变回了老样子。
从第一天就要建立code review的习惯,而且在最开始的一两个版本里,review的重点不要只放在业务正确性上,架构是否符合分层规范同样重要。一旦项目中已经出现了违反架构的代码并被合并,后面再纠正的阻力会成倍上升。
5. 常见问题与排查技巧实录
5.1 内存泄漏排查:从崩溃到预防
MVP项目里最典型的问题就是内存泄漏。表现通常是:页面反复进出几次后内存明显上涨,或者LeakCanary弹窗提醒MainActivity泄漏。
泄漏根源绝大多数出在Presenter的异步回调上。你的Activity虽然onDestroy了,但Presenter还活着,执行着网络回调,回调里又持有了Activity的引用,Activity就永远无法被回收。这也就是为什么我在前文反复强调WeakReference和isViewAttached()检查的原因。
要排查现有的泄漏,可以先靠LeakCanary定位泄漏的引用链,找到持有Activity的对象。然后检查这个对象是不是Presenter,是不是回调接口,是不是Handler。一旦确认是异步回调导致的,改法就明确了:在onDestroy时解绑或者取消回调。
额外提醒一个角落:静态变量。Project里经常会有为了"图方便"而写的静态工具类或静态Manager,里面持有Context或者View的引用。这种泄漏比MVP的弱引用问题更隐蔽。标准化的Base层设计里,禁止在静态变量中直接持有Activity或View的引用,应该持有Application级别的Context或者使用弱引用。
5.2 View层臃肿与Contract爆炸的对策
MVP使用一段时间后,有两种典型的坏味道。一种是View接口里方法越来越多,从showLoginSuccess到showUserNameValidationError到showNetworkError再到showUploadProgress,渐渐地一个页面的View接口可能有三十多个方法。另一种是Presenter也臃肿,login、logout、register、forgotPassword、checkToken全部堆在一个LoginPresenter里。
这两种情况其实是一个问题的两面:你在一个页面里塞了太多职责。解决思路是把页面的子模块拆成多个Contract和Presenter,而不是试图用一个Presenter撑起整个页面。
举个例子,一个订单详情页由订单基本信息、订单物流、售后申请三个部分组成,如果我们用一个OrderDetailContract的接口来定义所有交互,那么这个接口一定很庞大。正确的拆法是把订单信息、物流信息、售后申请分别抽成三个子模块,每个子模块都有自己的Contract和Presenter,它们挂在一个Fragment里。这样每个Presenter的职责单一,View接口也各自独立,改动物流模块不会碰售后逻辑。
拆分之后还有个细节要注意:各个子Presenter和Fragment的生命周期绑定要清晰。Fragment的onDestroy只解绑Fragment自身对应的Presenter,不要把所有子Presenter一次性清掉,否则会误伤还在执行的任务。
5.3 数据层混乱的表现与修复
数据层的混乱代码写起来很快,清理起来极难。最常见的三种症状是:一是数据源获取逻辑散落在很多页面里,同一个网络接口在三个页面里被重复调用了三次;二是Repository里全是转发方法,一个方法只是简单地把网络接口返回给Presenter,没有做任何缓存或者数据转换;三是缓存策略严重不一致,一个页面读缓存,另一个页面不读,同一个接口有的页面强制刷新,有的页面优先缓存。
修复的第一步是梳理所有网络接口和缓存需求的清单,想想这个数据的时效性要求如何:实时性要求高的就走网络,容忍延迟的就走缓存优先策略。第二步是明确哪些数据必须在Repository层做缓存,哪些必须实时获取,把这些决策集中写在Repository的实现里,而不是散落在各个Presenter中。第三步是统一数据转换的入口,Response转Bean的逻辑只在Repository层做,禁止在View层做。
行到此处,数据层就慢慢恢复了秩序。但后面要注意,新接口上线时不要在页面里直接联网发请求,必须先问一句:"Repository里有没有类似的方法?"如果没有,再新建。这个习惯能避免"数据层腐烂"。
5.4 性能与包体积的额外提醒
MVP模式的引入会增加类的数量,Base层、Contract、Presenter、Repository,一个页面多出三四个类,整个项目的类文件数量会上升。这对编译时间、包体积都有一定影响,但影响很小,现代化项目不必过度担忧。
真正需要注意的性能问题是重复创建对象。很多MVP框架会通过反射或注解动态创建Presenter和Repository实例,反射在低端机上的性能损耗虽然不算大,但也不值得在这种地方浪费。推荐的做法是用简单的工厂方法或者直接用new,先保证性能,再考虑更优雅的姿势。
包体积方面,建议对Base层做精简:不要为了放一个通用头像控件就引入一个图片库,不要把只在某个页面用到的工具类放到common模块。依赖库的收敛也和分层设计相关,如果common模块被所有模块依赖,里面引入的任何一个库都会被传到所有模块,所以common的建设要克制。
6. 最后聊几句实操心得
我做了这么多年项目,最大的感悟是:架构模式不是拿来炫耀的,是拿来省心的。MVP模式真正发挥作用的地方,不是在代码刚写完的那一刻,而是在半年后、一年后你回去改一个老需求的时候。如果那时候你发现自己打开一个页面包,看到Contract、Presenter、View各司其职、逻辑清晰,你会感谢当初那个坚持标准化的自己。
如果你正在犹豫要不要给项目引入这套规范,我的建议是:先找一个小模块试试水,比如登录功能、个人资料编辑这类边界清晰、不涉及复杂列表的页面。把这一两个页面按标准化分层做完,比较一下改造前后改需求的体验差异,再决定是否推广到全项目。不要一上来就大动干戈。
最后分享一个避免项目腐烂的小习惯:每次提交代码之前,回头看看diff里的目录结构和包引用关系,看看有没有破坏分层依赖的"私货"夹带进来,这比你背多少次架构原则都管用。架构是靠细节维护出来的。