面向对象六大基本原则:从概念到 Android 实战
不少 Android 项目在第一个版本里都很“顺”Activity 里发请求、解析 JSON、更新 UI几百行代码也能按时上线。问题往往出现在后面——接口要增加公共参数、网络库要替换、列表要同时支持缓存、埋点和重试最后一个小改动牵动十几个页面。这时我们缺的通常不是又一个设计模式而是一套判断代码边界的方法。常说的面向对象六大基本原则包括原则英文缩写一句话理解单一职责原则SRP一个模块只对一类变化负责开闭原则OCP新需求优先通过扩展完成而不是反复修改稳定代码里氏替换原则LSP子类型必须能安全替换父类型或抽象依赖倒置原则DIP业务策略依赖抽象不依赖基础设施细节接口隔离原则ISP使用方只依赖自己真正需要的最小能力最少知识原则迪米特法则LoD对象只和直接协作者沟通少了解内部结构前五项就是经典的SOLID。国内很多资料把迪米特法则加入后合称“六大原则”。它们不是必须逐条套用的教条也不是类和接口越多越好。六项原则最终都在解决同一件事把变化限制在尽可能小的范围内。一、单一职责原则一个类只做一件事吗单一职责原则Single Responsibility PrincipleSRP更准确的定义是一个模块应该只有一个引起它变化的原因。“只做一件事”容易被误解。一个 Repository 可以同时包含查询、保存和删除方法只要这些方法都服务于同一类业务变化就仍然可以职责单一。真正的问题是把不同角色关心的变化混在一起。反例无所不能的 ViewModelclassOrderViewModel:ViewModel(){funloadOrders(){// 1. 拼接 URL 和公共参数// 2. 使用 OkHttp 发请求// 3. 使用 Gson 解析 JSON// 4. 写入 Room// 5. 计算订单展示状态// 6. 更新界面并上报埋点}}这段代码至少会因为六类原因变化后端协议、网络框架、序列化格式、数据库结构、业务规则、UI 与埋点。任何一处变化都需要修改同一个类它也很难被独立测试。Android 中的拆分方式classOrderViewModel(privatevalobserveOrders:ObserveOrdersUseCase):ViewModel(){valuiState:StateFlowOrderUiStateobserveOrders().mapListOrder,OrderUiState{OrderUiState.Success(it)}.stateIn(scopeviewModelScope,startedSharingStarted.WhileSubscribed(5_000),initialValueOrderUiState.Loading)}classOrderRepositoryImpl(privatevalremote:OrderRemoteDataSource,privatevallocal:OrderLocalDataSource):OrderRepository{overridefunobserveOrders():FlowListOrderlocal.observeOrders()overridesuspendfunrefresh(){local.replaceAll(remote.fetchOrders())}}职责可以这样划分ViewModel 负责把业务状态转换成 UI 状态UseCase 负责业务流程与规则Repository 负责协调数据来源RemoteDataSource 负责远端访问LocalDataSource 负责本地持久化Mapper 负责 DTO、Entity 与领域模型转换。Android 中常见的 SRP 场景把 Activity/Fragment 中的状态管理移入 ViewModel将 Retrofit 接口、缓存策略和业务规则分开RecyclerView 的 Adapter 只负责列表展示把点击后的业务动作交给上层WorkManager 的 Worker 负责任务调度具体同步逻辑由 UseCase 承担Compose 中把有状态组件与无状态 UI 拆分。不要拆得过细如果为了 SRP 把每个三行函数都变成一个类阅读一次业务要跳转十几个文件复杂度只是从类内部转移到了类之间。判断是否需要拆分可以问这几段代码是否由不同原因、不同角色推动变化它们能否独立测试或复用拆分后是否形成清晰边界而不只是增加文件数量二、开闭原则需求变化时增加代码而不是改遍代码开闭原则Open/Closed PrincipleOCP指的是软件实体应该对扩展开放对修改关闭。“关闭”不是一行旧代码都不能改而是让已经稳定、验证过的核心流程尽量不因新增类型而被反复修改。反例不断增长的whenfuntrack(event:TrackEvent){when(event.channel){Channel.FIREBASE-firebaseAnalytics.logEvent(event.name,event.params)Channel.UMENG-MobclickAgent.onEventObject(context,event.name,event.params)Channel.INTERNAL-api.upload(event)}}每增加一个埋点平台都要修改这个分支。如果类似判断散落在多个模块中新增渠道会变成一次全局搜索。通过扩展实现变化interfaceAnalyticsTracker{funtrack(event:TrackEvent)}classFirebaseTracker:AnalyticsTracker{overridefuntrack(event:TrackEvent){// 调用 Firebase SDK}}classCompositeTracker(privatevaltrackers:SetJvmSuppressWildcardsAnalyticsTracker):AnalyticsTracker{overridefuntrack(event:TrackEvent){trackers.forEach{it.track(event)}}}接入新平台时增加一个AnalyticsTracker实现并通过 Hilt multibinding 注入集合稳定的调用流程不需要改变。Android 中常见的 OCP 场景RecyclerView 使用不同ViewHolder/delegate 扩展新的 item 类型OkHttp 通过 Interceptor 扩展鉴权、日志、重试和公共参数Retrofit 通过 Converter.Factory 扩展序列化能力图片加载库通过接口支持磁盘、内存、网络等不同数据源Compose 通过Modifier链扩展布局和交互行为支付、分享、登录等多渠道能力使用策略实现。什么时候直接修改更合适如果需求只会出现一次、变化方向尚不明确提前设计扩展点可能得到一个没人需要的抽象。OCP 的前提是已经识别到稳定部分和可变部分没有稳定边界时先写清楚、补测试再随着真实变化重构。三、里氏替换原则能编译不代表能替换里氏替换原则Liskov Substitution PrincipleLSP要求使用父类型或抽象的地方替换成任意子类型后程序仍应保持正确行为。它约束的不只是方法签名还包括行为契约输入范围不能被子类无故收紧输出承诺不能削弱异常和副作用也应符合调用方预期。一个违反 LSP 的缓存实现interfaceUserCache{suspendfunsave(user:User)suspendfunget(id:String):User?}classReadOnlyUserCache:UserCache{overridesuspendfunsave(user:User){throwUnsupportedOperationException(read only)}overridesuspendfunget(id:String):User?null}ReadOnlyUserCache虽然实现了接口却无法履行save契约。任何认为UserCache可读写的调用方在替换后都会崩溃。更合理的方式是拆开能力interfaceUserReader{suspendfunget(id:String):User?}interfaceUserWriter{suspendfunsave(user:User)}classRoomUserCache(privatevaldao:UserDao):UserReader,UserWriter{overridesuspendfunget(id:String):User?dao.find(id)?.toDomain()overridesuspendfunsave(user:User){dao.insert(user.toEntity())}}Android 中常见的 LSP 场景RecyclerView.setLayoutManager()可以接收LinearLayoutManager、GridLayoutManager等实现Repository 的生产实现与 Fake 实现应保持相同业务语义替换 Retrofit、Ktor 或本地 Mock 数据源后上层不应感知基础设施差异自定义 View 覆盖父类方法时不能破坏测量、布局或生命周期约定List与可变集合要谨慎替换不能向调用方暗中暴露可变行为。检查 LSP 的实用问题Fake 是否为了测试方便返回了线上永远不可能出现的状态某个实现是否对“不支持”的方法直接抛异常子类是否要求比抽象更严格的参数替换实现后调用方是否还要写if (impl is Xxx)如果调用方必须识别具体实现才能正确工作抽象通常已经失效。四、依赖倒置原则让业务规则站在依赖图上层依赖倒置原则Dependency Inversion PrincipleDIP包含两层含义高层策略不应该依赖低层实现两者都应依赖抽象抽象不应该依赖细节细节应该依赖抽象。这里的“高层”是业务规则“低层”是 Retrofit、Room、文件系统、第三方 SDK 等技术细节。直接依赖细节classLoginViewModel:ViewModel(){privatevalapiRetrofit.Builder().baseUrl(BASE_URL).build().create(LoginApi::class.java)funlogin(account:String,password:String){viewModelScope.launch{api.login(LoginRequest(account,password))}}}ViewModel 知道 Retrofit、请求 DTO 和服务地址测试登录规则时也不得不处理网络细节。让基础设施实现业务所需的抽象interfaceAuthRepository{suspendfunlogin(account:String,password:String):LoginResult}classLoginUseCase(privatevalauthRepository:AuthRepository){suspendoperatorfuninvoke(account:String,password:String):LoginResult{require(account.isNotBlank()){账号不能为空}require(password.length8){密码至少 8 位}returnauthRepository.login(account,password)}}classNetworkAuthRepository(privatevalapi:LoginApi):AuthRepository{overridesuspendfunlogin(account:String,password:String):LoginResultapi.login(LoginRequest(account,password)).toDomain()}依赖方向变成ViewModel - LoginUseCase - AuthRepository - NetworkAuthRepository - Retrofit ^ └------ FakeAuthRepository测试Hilt/Dagger 等依赖注入框架负责在应用启动时完成“抽象到实现”的组装但要注意依赖注入是实现 DIP 的手段不等于 DIP 本身。即使手动构造对象只要依赖方向正确也符合原则。Android 中常见的 DIP 场景Domain 层定义 Repository 接口Data 层提供实现业务代码依赖Clock而不是直接调用System.currentTimeMillis()文件上传业务依赖Uploader而不是直接依赖某云厂商 SDK导航逻辑依赖Navigator避免 ViewModel 持有 Activity日志与埋点依赖应用自己的门面接口隔离第三方 SDK。五、接口隔离原则别让调用方被迫依赖无关能力接口隔离原则Interface Segregation PrincipleISP指的是客户端不应该依赖它不需要的方法类之间的依赖应建立在最小接口上。这里的“客户端”不是手机端而是接口的使用方。反例万能媒体接口interfaceMediaController{funplay()funpause()funseekTo(positionMs:Long)funstartRecording()funstopRecording()funtakePhoto()}一个只播放音频的类被迫实现录音、拍照方法最后只能留空或抛异常。这也容易进一步违反 LSP。按使用方需要拆分interfacePlayer{funplay()funpause()funseekTo(positionMs:Long)}interfaceRecorder{funstartRecording()funstopRecording()}interfaceCameraCapture{suspendfuntakePhoto():Uri}页面只注入它实际需要的能力classPodcastViewModel(privatevalplayer:Player):ViewModel()Android 中常见的 ISP 场景将巨大回调接口拆成OnItemClickListener、OnRetryListener等小接口按业务查询拆分 DAO避免所有模块都依赖一个“万能 DAO”权限请求器只暴露当前功能需要的权限能力模块对外只暴露入口路由或用例不泄露内部 Repository、DAO 和 DTOKotlin 中用函数类型替代只有一个方法的回调接口。ISP 关注的是调用方需要什么SRP 关注的是实现方因什么变化。两者经常一起出现但观察角度不同。六、最少知识原则不要穿透对象的内部结构最少知识原则Law of DemeterLoD又称迪米特法则核心可以概括为只与直接朋友通信不要让一个对象了解协作者的内部结构。反例跨越多层取数据valcitysessionManager.currentUser.profile.address.city调用方知道SessionManager - User - Profile - Address的完整结构。任何中间层变化、可空字段或延迟加载策略都可能迫使调用方修改。提供调用方真正需要的能力classUserSession(privatevaluserStore:UserStore){funcurrentCity():String?userStore.currentUser()?.profile?.address?.city}调用方只需要valcityuserSession.currentCity()内部数据如何组织被限制在UserSession内部。Android 中常见的 LoD 场景Fragment 不直接操作 Activity 内部的 View而是通过导航、回调或共享状态协作ViewModel 不持有 Activity/Fragment更不沿着 Context 获取其他组件业务模块不穿透 Repository 直接拿 DAO 或 Retrofit ServiceAdapter 通过回调上报用户意图不直接调用页面的 Repository模块间只通过公开 API 或导航契约通信不访问对方内部类。也不要走向“层层转发”为了遵守 LoD 给每个 getter 再包一层没有真正隐藏结构只会制造大量传话方法。好的门面方法应该表达业务意图例如canPlaceOrder()而不只是把getA().getB().getC()机械改写成三个代理 getter。七、六大原则如何共同落地以图片加载为例假设页面需要显示头像数据可能来自内存、磁盘或网络。一个可演进的最小设计如下interfaceImageSource{suspendfunload(key:String):ByteArray?}classImageLoader(privatevalsources:ListImageSource,privatevaldecoder:ImageDecoder){suspendfunload(key:String):Bitmap?{valbytessources.firstNotNullOfOrNull{it.load(key)}returnbytes?.let(decoder::decode)}}六项原则在这里不是六份独立代码而是互相配合SRP获取字节、解码图片、展示图片分别负责不同变化OCP新增 CDN、资源文件等来源时增加ImageSource实现LSP任意ImageSource都遵守“找不到返回 null”的契约DIPImageLoader依赖ImageSource和ImageDecoder抽象ISP数据源只需提供load不必实现清缓存、预加载等无关能力LoD页面只调用imageLoader.load(url)不知道缓存目录或 HTTP 客户端。这也是为什么设计原则不应该被单独背诵真实设计中一个合理边界往往同时体现多项原则。八、Android 项目中的快速检查清单做 Code Review 或重构前可以用下面的问题快速扫描职责Activity、Fragment、Composable 是否混入请求、缓存和业务规则一个类是否会因为多种互不相关的原因频繁修改扩展增加一种支付、登录或列表类型是否必须修改多个when/if变化方向是否已经稳定到值得建立扩展点契约Fake 和生产实现是否保持相同的成功、失败与空数据语义某些实现是否存在空实现或UnsupportedOperationException依赖ViewModel/UseCase 是否直接依赖 Retrofit、Room 或第三方 SDKDomain 层是否反向引用 Android Framework 类型接口调用方是否被迫实现或依赖自己不用的方法模块的公开 API 是否暴露了内部 DTO、Entity 或工具类协作是否出现a.b.c.d()式的对象链和跨层调用一个模块是否知道另一个模块过多的内部结构如果其中几个问题经常同时出现通常说明边界需要调整而不是简单再加一个 Utils 类。九、原则不是目标可维护性才是六大原则能帮助 Android 项目获得更好的可测试性、可替换性与扩展性但每一层抽象都有成本更多类型、更多跳转、更复杂的依赖图也可能带来理解和构建负担。实际开发中可以遵循三个步骤先写清楚命名准确、流程直接、测试覆盖关键行为识别变化观察哪些代码确实因为不同原因反复修改在变化处抽象只隔离真实存在的易变点不为想象中的未来建框架。判断设计好坏的标准不是用了多少接口、Repository 或 UseCase而是当需求变化时修改是否集中、旧行为是否稳定、新代码是否容易验证。六大原则最后可以浓缩成一句话让稳定的业务规则远离易变的实现细节让每次变化停在它应该停下的地方。参考资料面向对象六大基本原则——网络引擎切换Robert C. Martin,Agile Software Development: Principles, Patterns, and PracticesAndroid 官方架构指南Android 依赖注入与 Hilt

相关新闻

YOLO+CLIP多模态智能视频监控:实时检测与语义检索融合架构

YOLO+CLIP多模态智能视频监控:实时检测与语义检索融合架构

简介:这份资源面向计算机视觉与智能安防方向的学习者,提供一套融合CLIP与YOLO的智能视频监控系统实现方案,重点解决实时目标检测与自然语言检索监控画面的问题。包内共11个文件,以Python脚本、zbak备份文件、zip压缩包为主&#x…

2026/9/30 6:51:55 阅读更多 →
C#实战:TSC标签打印机二维码打印源码解析与避坑指南

C#实战:TSC标签打印机二维码打印源码解析与避坑指南

简介:这份C#程序源码面向需要驱动TSC标签打印机输出二维码标签的开发人员,无论是刚接触打印指令的新手,还是希望借鉴成熟实现的经验开发者,都能从中获得可直接参考的完整方案。资源包共79个文件,约2.12MB,以…

2026/9/30 8:35:03 阅读更多 →
原生家庭的术语大全的庖丁解牛

原生家庭的术语大全的庖丁解牛

总纲:原生家庭,指一个人从小到大成长、抚养长大的家庭,主要由父母(或抚养人)、兄弟姐妹构成,区别于成年后自己组建的新生家庭。原生家庭塑造早年认知、情绪模式、亲密关系模板,但不决定人的一生…

2026/9/30 9:22:33 阅读更多 →

最新新闻

Zabbix自定义监控实战:脚本编写、Agent配置与日志服务监控

Zabbix自定义监控实战:脚本编写、Agent配置与日志服务监控

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 14:06:44 阅读更多 →
如何自制NFC耗材扫描仪:Bambuddy配套SpoolBuddy硬件开发实战指南(树莓派+PN5180)

如何自制NFC耗材扫描仪:Bambuddy配套SpoolBuddy硬件开发实战指南(树莓派+PN5180)

如何自制NFC耗材扫描仪:Bambuddy配套SpoolBuddy硬件开发实战指南(树莓派PN5180) 【免费下载链接】bambuddy Your Bambu Lab. No Cloud. Your Rules. Self-hosted command center for Bambu Lab — from one A1 to an entire print farm. 项…

2026/9/30 14:05:43 阅读更多 →
从read()到系统调用:操作系统接口设计与实现深度拆解

从read()到系统调用:操作系统接口设计与实现深度拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 14:05:43 阅读更多 →
管理统计学期末不挂科实战指南:Excel手算+高频失分点突破

管理统计学期末不挂科实战指南:Excel手算+高频失分点突破

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 14:05:43 阅读更多 →
双缝干涉:把两条缝的图样叠起来,光为什么自己跟自己打架

双缝干涉:把两条缝的图样叠起来,光为什么自己跟自己打架

一块挡板上开两条平行细缝,单色光打过去,屏幕上映出的不是两条亮线,而是一排等间距的明暗条纹。把其中一条缝遮住,条纹立刻消失、只剩一团中间亮两边暗的光斑——同一束光,只因「知道不知道它走了哪条缝」,…

2026/9/30 14:04:42 阅读更多 →
H3C与华为交换机基础配置实战:从Console到三层互通

H3C与华为交换机基础配置实战:从Console到三层互通

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 14:04:42 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/30 13:14:22 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/29 16:41:41 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/30 13:14:49 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/29 3:55:56 阅读更多 →