以Android实战的视角了解设计模式原则

500 阅读15分钟

之前我写过几篇文章,都是写的架构,设计原则,写了一大堆的宏观层面和苦涩难懂的理论,我想了一下,应该结合日常写代码的视角来写这篇文章,就更接地气一些。

比如如何使用设计原则写好代码,如何正确使用DataSource,Repository等,应用如何分层等等。

用日常开发的实战经验的视角,就更通俗易懂的了解设计原则的重要性!!!

为何说设计模式的原则很重要,这里我简称 设计原则。

设计原则是整个软件的原子,实现 高内聚低耦合 的目标,让代码 可维护性、可读性、简洁性、可扩展性、灵活性、可复用性、可测试性。

image.png

高内聚 指导类本身的设计,低耦合 指导类与类之间依赖关系的设计。

创建型、结构型、行为型出 等模式都是招式,设计原则是心法,告诉你为什么要这么做。

设计原则包含了 最基本的 SOLID,然后 LOD原则、DRY原则、KISS原则、YAGNI原则 作为补充。

SOLID+LOD 告诉你如何做对(规范类与类的关系),KISS/YAGNI告诉你别做过头(避免过度设计),DRY 别重复(消除冗余)。

image.png

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 等等。

image.png

写在后面

无论新出的Comose,flow,mvi、Hilt如何变化,无论你开发的是IOS、Android、Web等,不变的是内功心法——设计原则,以不变应万变。