news 2026/8/20 19:47:23

写好Java代码,先理解这五个核心原则

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
写好Java代码,先理解这五个核心原则

同事把一段两百行的Service类甩给你,里面塞了十二个public方法,既有订单计算,又有日志推送,还顺手做了权限校验。你盯着那段代码,脑子里冒出的念头不是“这写得真烂”,而是“我该从哪里开始改”。这样的瞬间在Java开发者的日常里反复上演,而真正拉开代码质量差距的,往往不是语法技巧,而是你是否在落笔之前,想清楚了一套关于“职责”与“关系”的底层逻辑。

本文要聊的五个核心原则,不是来自某本畅销书的教条,也不是面试官用来筛选简历的八股文。它们是你写完一段代码后,运行起来、测试起来、三个月后再次打开时,能真实感受到的那股“舒适感”的来源。理解了它们,你写出的Java代码才谈得上“会呼吸”。

一个类只应该有一个被修改的理由

单一职责原则是被误解得最深的一条。很多人把它理解成“一个类只做一件事”,但“一件事”的粒度极其模糊。一个用户类做了用户校验、用户存储、用户展示,算不算一件事?在业务眼里可能算,但在维护者眼里,这三者改变的原因截然不同:校验规则跟着安全需求变,存储结构跟着数据库迁移变,展示字段跟着前端交互变。如果它们挤在同一个类里,任何一方的需求变更都会导致这个类被重新编译、重新测试、重新部署

判断职责是否单一,更可靠的标准是“被修改的理由”。问自己:这个类如果不修改,是否就无法完成某个独立的业务变化?如果答案是“必须改”,那这个类就承担了那一条变化轴。类应当沿着变化的方向拆分,而不是沿着业务名词的边界划分。你见过那种因为加了一个字段,导致五个方法都要改动的类吗?那就是职责在开始时就长歪了。

实操上,一个立刻见效的做法是:方法名里出现“and”或者“以及”,就考虑拆分。saveUserAndSendNotification这种命名,已经在向你招供它违背了单一职责。让每个类只有一个“老板”,只有一类“老板”改需求时你才动它,其他时候它老老实实待在原地,这就是单一职责给你的最大的自由。

对扩展保持慷慨,对修改保持吝啬

开闭原则看起来矛盾:既要允许行为增加,又不准改动已有代码。但Java的接口和抽象类天生就是为这个矛盾准备的。遇到需求变化,你倾向于往现有类里加if-else,还是新增一个实现类?前者的路径是“修改”,后者的路径是“扩展”。每写一行新的if-else,你都是在把未来的可扩展性提前透支掉

举个例子:一个支付处理器,最初的版本只支持微信支付。后来需要支持支付宝,你在方法里加了个if(type.equals("alipay")),能跑,但很丑。再后来要支持银联、云闪付、外币卡,这个if开始变得像一棵圣诞树,每个分支都挂满了对参数格式和返回值的特判。这时候你才想起来,如果当初定义了一个PaymentStrategy接口,每种支付方式是一个实现类,新增一种支付不过是多一个类而已。开闭原则的本质是:把你的代码改造成一个“插槽”,而不是一片“工地”

但“对扩展开放”不意味着无脑加接口。真正意义上的开,是允许替换行为;而“闭”是禁止改动已有验证过的逻辑。用策略模式、模板方法模式、装饰器模式这些GoF老套路,都是为了这个目的。你越是在写代码时克制住“改一改就能用”的冲动,越是会在未来收到“加一个就能用”的回馈

子类想要替换父类,请先治好自己的隐疾

里氏替换原则听起来很理论:凡是用父类的地方,都能用子类替换而不产生错误。但在实际Java开发里,它更多是在提醒你:继承不是拿来白嫖方法的,而是用来建立可替代的抽象关系的。见过太多这样的代码:子类继承父类,覆盖了一个方法,但覆盖后直接抛异常——因为父类的那个方法对它而言“不适用”。这就是典型的替换原则破坏者。

比如你有一个Bird类,里面有fly()方法。然后你来了个Penguin extends Birdfly()实现里抛出UnsupportedOperationException。看起来挺符合生物学的,但你的业务代码里凡是处理Bird的地方,都潜伏着运行时异常。父类的契约被破坏,子类的所作所为就不再是扩展,而是背叛

遵循里氏替换原则的实践是:不要在子类中“弱化”父类的行为。如果子类不能满足父类声明的所有约定,那这个继承关系本身就该被拆散——用组合、用接口,而不是硬套继承。检验方法是写测试时把父类作为参数类型,然后传入所有子类实例,如果哪个测试挂了,而测试代码没有错,那就是你的子类出了问题。更隐蔽的问题是:子类返回了更窄的可见性、抛出了额外的受检异常、改了父类方法的语义。这些你以为的“细节”,迟早会在某个深夜变成线上告警。

接口是给调用方看的,不是给你自己秀肌肉的

接口隔离原则说:客户端不应该被迫依赖它不用的方法。可惜很多Java代码把接口当成了“收藏夹”,把所有可能用到的方法都塞进去,美其名曰“统一抽象”。一个UserService接口里有login()register()updateAvatar()sendSms()uploadFile()……你的实现类被迫写了五个空方法,因为接口里有这五个方法。这其实是用“大接口”来掩盖“职责混乱”。

更好的做法是:接口越小越好,小到每个接口只对应一个“角色”的需求。调用方需要登录,就只依赖Loginable;需要注册,就只依赖Registerable一个类可以实现多个小接口,但永远不要被迫实现它根本不需要的方法。这就是接口隔离原则——它和单一职责是同一个宇宙的两面镜子:类端看职责,接口端看隔离。

在这个原则里,最常被误用的还有“上帝接口”的变种——DTO(数据传输对象)和VO(视图对象)使用一个全字段大对象。前端要三个字段,你却把整个数据库表映射的对象传过去。表面上没有接口定义问题,但实际上你让调用方“依赖”了它根本不需要的字段,每一次数据库结构调整,都会波及前端接口。隔离的边界不只是类与方法,更是数据的形态。定义细粒度的接口和DTO,你的代码结构才会拥有“细胞膜”,而不是一锅粥。

依赖靠抽象,不靠具体;靠约定,不靠打听

依赖倒置原则是SOLID的压轴戏,也是最容易被忽视的。它的核心是两点:上层模块不能依赖下层模块,双方都应依赖抽象;抽象不能被具体实现绑定,具体实现应依赖于抽象。翻译成Java代码就是:你的业务逻辑层不应该直接new一个MysqlOrderRepository,而是应该依赖一个OrderRepository接口。至于这个接口的实现是MySQL还是Redis还是内存,是你在运行时通过依赖注入容器或工厂来决定的。

你越是在一个类里直接new出它依赖的其他类,你越是在拒绝未来的任何变化。为什么要依赖抽象?因为抽象是稳定的、可替换的。当你把OrderRepository定义成接口的时候,你就给整个系统画上了一条“约定线”:业务层只跟约定对话,不跟某家数据库厂商结婚。

但倒置这个动词还有一层更深的含义:控制流的方向和依赖声明的关系。在传统过程式代码里,高层调用低层,依赖自然指向低层;倒置之后,高层定义“我需要什么”,低层去实现“我如何满足”。真正优秀的Java代码,读起来像是一份“能力清单”,而不是一串“执行步骤”。你要在这个模块里做什么,先声明依赖接口,再编写逻辑,最后通过配置把这个接口绑定到某个实现上——这就是Spring容器派上用场的哲学根源。

理解了这五个原则之后,你会发现它们其实不是孤立的五条纪律,而是在告诉你同一件事:代码是写给读它的人看的,而“人”包括六个月后的你自己。单一职责教你怎么切分业务变化,开闭教你怎么应对需求演进,里氏替换教你怎么安放继承关系,接口隔离教你怎么划定调用边界,依赖倒置教你怎么建立稳定连接。这五把刀一旦在你脑海里合璧,你再看Java代码,看到的就不再是一行行语法,而是一张张由职责、边界、约定与抽象织成的网。

下次你再打开一个类,先别急着写public。停下来问一句:这个类为什么存在?它的行为会因什么样的需求而改变?有没有那条接口隔离线?我依赖的是名字,还是本质?当你开始问这些问题时,你不是在写代码,你是在设计一座能够持续生长的建筑。而那个建筑的地基,恰好就是这五个看起来朴素、做起来需要定力的核心原则

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!