之前我写过几篇文章,都是写的架构,设计原则,写了一大堆的宏观层面和苦涩难懂的理论,我想了一下,应该结合日常写代码的视角来写这篇文章,就更接地气一些。
比如如何使用设计原则写好代码,如何正确使用DataSource,Repository等,应用如何分层等等。
用日常开发的实战经验的视角,就更通俗易懂的了解设计原则的重要性!!!
为何说设计模式的原则很重要,这里我简称 设计原则。
设计原则是整个软件的原子,实现 高内聚低耦合 的目标,让代码 可维护性、可读性、简洁性、可扩展性、灵活性、可复用性、可测试性。
高内聚 指导类本身的设计,低耦合 指导类与类之间依赖关系的设计。
创建型、结构型、行为型出 等模式都是招式,设计原则是心法,告诉你为什么要这么做。
设计原则包含了 最基本的 SOLID,然后 LOD原则、DRY原则、KISS原则、YAGNI原则 作为补充。
SOLID+LOD 告诉你如何做对(规范类与类的关系),KISS/YAGNI告诉你别做过头(避免过度设计),DRY 别重复(消除冗余)。
1.SOLID
1.1 单一职责(SRP:Single Responsibility Principle)
A class or module should have a single reponsibility. (单一职责原英文描述)
一个 类/模块/函数/功能 只负责一项职责。代码开发中比较常见的一种职责,是最简单的。
类/模块/函数/功能 要设计粒度小,功能单一。
这里举个例子,手就是手,脚就是脚,只做自己职责范围的事情。如果造物主,将人类的头,即可走路,又可拿东西,还用来排泄,这里的职责就很混乱,不够单一。
比如常见错误,在 Activity或fragment 的 create里面,写了很多代码。应该将里面的代码,分成几个函数,比如 initVariables:初始化变量;initViews:加载布局文件,初始化控件,挂上事件;loadData:获取数据 等等。
// ❌ 错误:DataSource 做了太多事
class DataSource {
fun getRemoteDatas() { ... }
fun getLocalDtas() { ... }
}
// ✅ 正确,职责单一并且更清晰
class RemoteDataSource { ... }
class LocalDataSource { ... }
为何Android存在 ViewModel、UseCase、Repository、DataSource,他们的存在也是为了将职责划分的更单一,更清晰。
| 名称 | 职责 |
|---|---|
| DataSource | 纯粹的 CRUD,只负责将数据“拿出来”或“塞回去”,不带任何业务感情。“脏活累活我来干”:比如跟硬件、Binder、数据库、网络、SDK接口等直接打交道。保证上层代码的绝对纯净。 |
| Repository | >数据策略的“决策者”,做策略调度,决定数据从哪里来,一个数据的有很多来源(比如数据库、缓存、网络、底层等)。举个栗子:先看本地有没有缓存,没有就网络获取,无网络又如何?什么时候去同步。>原始数据的"精修和包装”。比如数据来源返回的原始数据 转换为 干净的数据模型。举个栗子:某个接口返回0x0, 0x1,我们封装成OPEN,CLOSE的枚举类. >数据流的最后一道防线,像安全沙盒,比如来源异常,会在这一层拦截处理。举个栗子:比如将网络错误封装成NetWorkError抛给上层。 |
| UseCase | 把业务拆成了极小的、单一的业务单元。一个 UseCase 类,名字是什么,它就只干什么。举个栗子:XXXUseCase,业务需求,用户点击设置某个状态,需要电量为>10 并且 不锁屏 才能去设置。(有了这个XXXUseCase.invoke(),其它地方也可以复用) |
| ViewModel | 把界面点击变成业务指令发给下层,同时把下层送上来的数据,包装成最容易渲染的 UI 状态递给屏幕。 |
为了符合 主线程安全(Main-safety),切换线程的事情交给 Data Layer 切换,这里也是为了让职责更单一。
一句话总结下 Repository: 数据策略(缓存/网络选择、同步策略、降级等策略) + 数据转换(DTO → Domain、数据清洗、聚合数据) + 异常隔离(网络异常、数据库异常、底层异常等)
Repository补充:
- Repository 是数据访问的统一抽象入口,负责协调多个 DataSource, 并根据业务策略决定数据从哪里获取、何时刷新以及失败后的降级策略。
- 同时,Repository 可以对 DataSource 返回的数据进行转换、聚合和封装, 将底层的 DTO、数据库实体等转换成上层使用的 Domain Model。
- Repository 还可以隔离数据源的具体实现和异常,让 ViewModel/UseCase 不需要关心数据究竟来自网络、数据库、缓存还是底层系统。
// 这里写个小Demo
interface LocalDataSource {
suspend fun getDataYYYY():String
}
interface NetWorkDataSource {
suspend fun getDataXXXX():String
}
class LocalDataSourceImpl : LocalDataSource {
suspend fun getDataYYYY():String...
}
class NetWorkDataSourceImpl : NetWorkDataSource {
suspend fun getDataYYYY():String {
// 这就是我们将的 主线程安全(Main-safety),切换线程的事情交给 Data Layer 切换
return withContext(Dispatchers.IO) {
... ...
}
}
}
// 演示DEMO代码,伪代码
class QiuRepository(
private localDataSource:LocalDataSource,
private netWorkDataSource:NetWorkDataSource,
) {
suspend fun getData():User {
// 数据决策者
val localData = localDataSource.getDataYYYY()
... ...
// 去决策返回LocalDataSource的数据,还是NetWorkDataSource
... ...
// 数据模型转换/聚合者,将之前返回的String转换为 Data User
return user
}
// 另外一种写法
fun getData(): Flow<User> { ... ... }
}
// 演示DEMO代码,伪代码
class QiuViewModel(
private repository:QiuRepository
) : ViewModel {
private val _qiuState = MutableStateFlow(User())
override val qiuState: StateFlow<User> = _qiuState.asStateFlow()
fun getData() {
viewModelScope.launch {
val user = repository.getData()
_qiuState.value = user
}
}
// 另外一种写法
val qiuState: StateFlow<User> = repository.getData().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User()
)
}
// 对于 launch 与 withContext 概念不能搞混.
1.2 开闭原则(OCP:Open Closed Principle)
software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification. (开闭原则原英文描述)
对扩展开放,对修改关闭。
这里举个例子,笔记本的预留的内存条插口/USB-A/USB-C接口,就避免了你去 修改 主板,根据预留的接口你可以去 扩展 内存条,鼠标,键盘等等。
// ❌ 错误:每次新增支付方式都要改这个函数
fun processPayment(type: String, amount: Double) {
when (type) {
"alipay" -> processAlipay(amount)
"wechat" -> processWechat(amount)
// 新增支付宝? 改这里 → 可能引入新bug
"unionpay" -> processUnionPay(amount)
}
}
// ✅ 正确:新增支付方式只需扩展新类
interface PaymentProcessor { fun process(amount: Double) }
class AlipayProcessor : PaymentProcessor { ... }
class WechatProcessor : PaymentProcessor { ... }
// 新增银联? 直接加新类,旧代码不动
class UnionpayProcessor : PaymentProcessor { ... }
//
fun processPayment(processor: PaymentProcessor, amount: Double) {
processor.process(amount)
}
日常开发中,我想很多人都写过这样的代码
// ❌每增加一个游戏平台,都要修改原来的代码
fun launchGame(game: Game) {
if (game.platform == "NES") {
launchNes(game)
} else if (game.platform == "SNES") {
launchSnes(game)
} else if (game.platform == "PS1") {
launchPs1(game)
} else if (game.platform == "GBA") {
launchGba(game)
}
}
// ✅ 抽取公共接口,每个平台单独去实现这个接口
interface EmulatorCore {
val platform: Platform
fun launch(game: Game)
}
// 比如写了公共基础库,提供给别个调用的函数
// 今天A同事要求新增一个参数width,明天B同事要求新增一个hegiht,
fun yyyy(int x, int y) {
}
1.3 里氏替换原则(LSP:Liskov Substitution Principle)
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。(里氏替换原则原英文描述)
子类对象(object ofsubtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
子类继承父类后,可以扩展父类的功能,也可以实现父类抽象的方法,但不能改变父类原有已实现的功能;(子类可以对父类进行扩展,但不能改变父类现有的功能)
这里举个例子(不要纠结真实科学),会飞的鸟类基因编辑,从父基因复制了一个子基因,然后改变了子类基因的飞行,导致不能飞了,这里就是破坏了父基因的飞行。
// 父类:鸟
open class Bird {
open fun fly() {
println("鸟正在飞翔")
}
}
// 基因编辑后的子类,修改了飞行能力
class Penguin : Bird() {
override fun fly() {
// 基因突变,无法飞行
println("变异物种正在走路")
}
}
1.4 接口隔离原则(ISP:Interface Segregation Principle)
Clients should not be forced to depend upon interfaces that they do not use。(接口隔离原则原英文描述)
客户端不应该强迫依赖它不需要的接口。
应将臃肿庞大的接口拆分为更小、更具体的接口,让每个接口只包含调用者感兴趣的方法。
// ❌ 错误:一个臃肿的接口,强迫所有类实现所有方法
interface Worker {
fun work()
fun eat()
fun sleep()
}
class Robot : Worker {
override fun work() { /* OK */ }
// 机器人不需要
override fun eat() { throw UnsupportedOperationException() }
// 机器人不需要
override fun sleep() { throw UnsupportedOperationException() }
}
// ✅ 正确:接口拆小,各取所需
interface Workable {
fun work()
}
interface Feedable {
fun eat()
fun sleep()
}
ISP 的颗粒度问题,何时进行拆分。需要具体问题,具体分析,根据实际的业务情况来定。
这里举一个项目实战经验,我想大家肯定遇到这种情况,本来代码写的好好的,之前也进行了小重构,后面随着项目时间点紧,业务又增加了,接口的职责开始出现了坏味道。
一开始只有Bluetooth(蓝牙)的相关操作的接口,后面赶项目进度,需要支持Serial(串口)、USB,这就导致了去实现这个接口的时候,出现了空函数,职责混乱,我就TODO了下,后续有空进行重构。(我这里只是伪代码,不能将公司代码拷贝上来)
interface ICommunicationDataSource {
fun connect()
fun disconnect()
fun send(data: ByteArray)
fun receive(): Flow<ByteArray>
fun setBaudRate(rate: Int)
fun scanDevices(): Flow<List<Device>>
fun pair(device: Device)
}
// Bluetooth:connect、disconnect、send、receive、scan、pair、
// USB:connect、disconnect、send、receive
// Serial:connect、disconnect、send、receive、setBaudRate
// 进行接口的拆分
interface IDataTransport {
fun connect()
fun disconnect()
fun send(data: ByteArray)
fun receive(): Flow<ByteArray>
}
interface IBluetooth {
fun scanDevices(): Flow<List<Device>>
fun pair(device: Device)
}
interface ISerial {
fun setBaudRate(rate: Int)
}
1.5 依赖倒置(反转)原则(DIP:Dependency Inversion Principle)
高层模块(调用者)不应该依赖低层模块(被调用者/实现层),两者都应该依赖抽象
这里举个例子,程序员ONE/TWO/FOUR开发应用,设计师/领导/老板等想改个需求(高层模块/调用者),就必须去找具体的程序员(底层模块/实现层),沟通成本高,人员混乱,这样耦合就很严重。怎么办?公司引入了 "需求规范流程接口(抽象)",瞬间解耦,老板向需求流程接口提出需求A,他不用管谁实现。
[老板/领导/设计](高层模块/调用者)
|
需求规范流程接口(抽象契约)
|
[程序员ONE/TWO/FOUR](底层模块/实现层)
//需求规范流程接口(抽象契约)
interface RequirementTask {
fun execute()
}
// 高层模块(老板):只面向“需求接口”提需求,管你是谁做!
class Boss {
// 老板把需求提交到流程系统,不需要知道背后是哪个程序员
fun submitRequirement(task: RequirementTask) {
task.execute() // 触发执行
}
}
// 低层模块(程序员):实现“需求接口”
class ColorTask(private val developerName: String) : RequirementTask {
override fun execute() {
println("$developerName 接收到标准需求,开始评审...编写代码...")
}
}
// 运行阶段:自由插拔,随时替换!
val boss = Boss()
// 今天是 ONE 来实现这个需求
boss.submitRequirement(ColorTask("程序员 ONE"))
// 明天 ONE 请假,FOUR 随时无缝接管,老板的代码一行都不用改!
boss.submitRequirement(ColorTask("程序员 FOUR"))
去获取天气,心知天气、国家气象信息中心-中国气象数据网,这里有两个API,又要考虑扩展性,你要如何去设计?
interface 天气DataSrouce {
suspend fun getData()
}
class 必知天气DataSourceImpl : 天气DataSrouce {
override suspend fun getData() {
return withContext(Dispatchers.IO) {
"返回必知天气数据"
}
}
}
// 这样的好处就是,不管以后,还有必会,必来等等天气API,高层模块只管调用接口的getData。
总结一句话:DIP是设计原则,下面讨论到的 控制反转(IOC) 是设计思想/模式,依赖注入(DI)是具体的实现手段。
1.5.1 控制反转(IoC: Inversion of Control)
在 Android的ViewGroup自定义控件中,ViewGroup 本身就是一个典型的 IoC 容器——它掌控着子 View 的测量(Measure)、布局(Layout)和绘制(Draw)的控制权。
class NewViewGroup(...
...
override fun onMeasure(...
for (i in 0 until childCount) {
measureChild(child, widthMeasureSpec, heightMeasureSpec)
}
}
...
}
1.5.2 依赖注入(DI: Dependency Injection)
为何要使用 Hint 注入框架?你有思考过这个问题吗?
根据前面的单一职责里面的代码,我们能看到,创建起来很繁琐,如果DataSource还要传东西,就更繁琐了,这就是 Hint存在的意义,你能少写很多下面这种代码。
class QiuActivity : AppCompatActivity() {
private val viewModel: QiuViewModel by viewModels {
QiuViewModelFactory(
QiuRepository(
LocalDataSource(),
NetworkDataSource()
)
)
}
}
有人会反驳我,我直接在 QiuRepository 新建 Local/NetWork,不就完了,那这样就大错特错了,小DEMO可以这样干,实际项目开发不建议,为何?
违反了设计原则(SRP、DIP),无法进行单元测试,Mock/Fake/Stub更难了,职责不清晰(Repository的知识点去前面复习下),代码耦合度高,扩展性也差。
// Demo代码,不要在意细节, Hint 负责创建、组装、管理生命周期
@HiltAndroidApp
class QiuApplication : Application()
class LocalDataSourceImpl @Inject constructor() : LocalDataSource
class NetworkDataSourceImpl @Inject constructor() : NetworkDataSource
class QiuRepository @Inject constructor(
@HiltViewModel
class QiuViewModel @Inject constructor(
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
abstract fun bindLocalDataSource(
localDataSource: LocalDataSourceImpl
): LocalDataSource
@Binds
abstract fun bindNetworkDataSource(
networkDataSource: NetworkDataSourceImpl
): NetworkDataSource
}
@AndroidEntryPoint
class QiuActivity : AppCompatActivity() {
private val viewModel: QiuViewModel by viewModels()
}
2. 补充
2.1 迪米特法则(LOD:Law of Demeter)
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers. ——每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己 的朋友“说话”(talk),不和陌生人“说话”(talk)。
LOD原则(Law of Demeter)在软件开发中又称迪米特法则,也常被称为最少知识原则。
其核心思想是:一个对象应该对其他对象有最少的了解。
- 直接朋友:出现在成员变量、方法参数、方法返回值的类,都是当前类的直接朋友。
- 陌生人:出现在局部变量或方法链式调用内部的类,属于陌生人。
- 核心原则:不要使用链式调用(如:a.b().c().method()),类只需关心自己直接依赖的对象提供的方法,而不需要知道底层组件的复杂内部结构。
// ❌ 错误:链式调用,深入了解内部结构
class OrderService {
fun getCity(order: Order): String {
// Order → Customer → Address → City → name
// 陌生人知道了太多内部细节,一旦结构变就全崩
return order.customer.address.city.name
}
}
之前有个同事,给我封装了一个类使用,它让我调用 apiClass.execption来获取错误信息,然后再去调用 apiClass.start 来启动。如果以后它要改了,那所有调用它API的人都要改,后面我建议它改成 apiClass.start 直接返回错误码就行。好的 API 应该封装“过程”,暴露“结果”。
2.2 DRY 原则(DRY:Don't Repeat Yourself)
DRY原则,Don't Repeat Yourself,不要写重复的代码。
代码的可复用性,复用不仅指 模块、类、函数,还有框架、类库、组件等。
// ❌ 错误:相同的校验逻辑散落在多个地方
fun registerUser(email: String) {
if (!email.contains("@") || email.length < 5) {
throw IllegalArgumentException("邮箱格式错误")
}
... ...
}
fun updateEmail(email: String) {
if (!email.contains("@") || email.length < 5) {
throw IllegalArgumentException("邮箱格式错误")
}
... ...
}
// ✅ 正确:逻辑集中,单一来源
fun validateEmail(email: String) {
if (!email.contains("@") || email.length < 5) {
throw IllegalArgumentException("邮箱格式错误")
}
}
比如 Launcher 与 SystemUI 有同一个弹窗与相同的逻辑,它们就应该抽取出来,统一去使用。
比如 很多应用 都使用同样的资源,一级,二级标题的字体大小,颜色,还有它们的圆角、阴影等等,这样就可以统一抽取主题库,这样就可以减少维护成本。其实你会发现,应用架构的问题,很多时候都在解耦,高内聚。
为何会存在KMP这个玩意?它的职责也是为了将 业务模型、数据库、网络、Repository、UseCase等模块 共享给 Android 与 IOS 使用,就不需要IOS又重新写一套。
Android官方架构 + Clean Architecture 的 UseCase,你有没有想过?什么时候使用的场景使用它会更好?
- 多个 ViewModel 存在重复的业务逻辑
- 需要组合/聚合多个数据源(Repositories)
// ❌ 多个 ViewModel 存在重复的业务逻辑,违反 DRY 原则
class TestAViewModel(
private val aaaRepository: AAARepository,
private val bbbRepository: BBBRepository
) : ViewModel() { ... }
class TestBViewModel(
private val aaaRepository: AAARepository,
private val bbbRepository: BBBRepository
) : ViewModel() { ... }
// ✅ 业务逻辑封装进 Domain/GetMainUseCase
class GetMainUseCase(
private val aaaRepository: AAARepository,
private val bbbRepository: BBBRepository
) { ... }
class TestAViewModel(...GetMainUseCase...
class TestBViewModel(...GetMainUseCase...
// ❌ 业务逻辑需要垮 Repository 组合,职责太重,违反 SRP
class TestViewModel(
private val aaRepository: AARepository,
private val bbRepository: BBRepository,
private val ccRepository: CCRepository
) : ViewModel() { ... }
// ✅ 业务逻辑封装进 Domain/UseCase,在invoke处理自己的逻辑,可以合并,顺序等操作。
class MainUseCase(
private val aaRepository: AARepository,
private val bbRepository: BBRepository,
private val ccRepository: CCRepository
) { ... operator fun invoke() ... }
// ✅ ViewModel 变得非常干净
class TestViewModel(
private val mainUseCase: MainUseCase // 只依赖一个 UseCase
) : ViewModel() { ... }
2.3 KISS 原则(KISS:Keep It Simple and Stupid)
Keep It Simple — 保持简单,避免不必要的复杂度。
代码足够简单(代码行数少不代表简单,需要考虑复杂度、实现难度、代码的可读性等),也意味着很容易阅读,Bug容易发现,修复也简单。
其实在面对新技术的时候,要不要引入?这个点争议就很大,你要引入了新技术,其它同事看不懂,维护直线上升。
2.4 YAGNI 原则(YAGNI:You Aren't Gonna Need It)
You Aren't Gonna Need It — 不需要的就不要做
聚焦现在,摒弃过度设计。这个原则鼓励我们抵制过度设计的诱惑,只关注当前真正需要的功能,而不是那些"可能"在未来会用到的功能。
如何避免过度设计,需要具体问题具体分析,考虑实际的业务需求。
举个例子,用户信息里面包含了地址相关的,按道理来说,应该抽取出来,但现在业务比较简单,就没有必要,如果那天 有订单相关,还有会员等等,就需要将地址相关的抽取出来。
我的需求很简单,那就没有必要将简单的应用进行分层架构,然后还要使用MVI,写了一堆堆的Datasource、Repository、UseCase、State 等等。
写在后面
无论新出的Comose,flow,mvi、Hilt如何变化,无论你开发的是IOS、Android、Web等,不变的是内功心法——设计原则,以不变应万变。