ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java三大特性:封装、继承、多态的核心原理与工程实践

Java三大特性:封装、继承、多态的核心原理与工程实践 1. 三大特性为什么是Java的根基1.1 从热搜词里看到的信号这不是“八股文”那么简单先聊聊我最近注意到的一个现象。Java的“封装、继承、多态”这几个词常年挂在技术社区的热搜榜上和它们一起出现的往往是“java面试题”“java八股文”“java基础面试题”这类关键词偶尔还有“java学习路线”和“java后端完整成长路线”。很多人一看就笑了这不就是背八股吗三种特性背下来不就行了我在这个行业写了十几年Java面试过几百个候选人也带过不少刚入行的新人。可以很负责任地说能流利背出“封装是隐藏细节继承是代码复用多态是同一接口不同实现”这句话的人一抓一大把但真正能在写代码的时候把这三个特性用对、用活、用到位的十个人里能有两三个就算不错了。因为这三个特性不是三个孤立的知识点而是一套完整的设计哲学。封装解决的是“怎么组织代码”继承解决的是“怎么复用代码”多态解决的是“怎么扩展代码”。它们之间是层层递进的关系没有封装继承就失去了数据安全的地基没有继承多态就没有了类型体系的支撑没有多态继承就退化成单纯的代码复制。这篇文章我打算把三个特性掰开揉碎讲清楚不光讲它们的定义和语法更重要的是结合我实际开发里的经验、踩过的坑、面试官真正想听的东西把它们串成一条线。不管你是刚学Java的新手还是准备面试的求职者或者已经工作几年想回来补基础的老兵这篇都值得你花点时间仔细看。1.2 三大特性到底解决了什么问题先做一个生活化的类比。想象一下你在一个公司上班。封装就像公司的规章制度每个员工不需要知道其他所有人在干什么只需要通过规定的流程提交工作、获取信息就行——这叫“最小知识原则”。继承就像岗位继承管理层定的KPI体系、汇报流程这些基础规则新来的员工不用重新发明一遍只管在此基础上补充自己的职责——这叫“复用已有体系”。多态就像公司里下达同一个指令“提交周报”不同岗位的人执行方式完全不同财务交的是报表开发交的是代码进度销售交的是客户跟进——这叫“同一动作不同表现”。用Java的术语翻译一遍就是这样封装把对象的数据和行为绑定在一起对外暴露有限的访问入口内部实现可以自由修改而不影响调用方继承允许一个类基于另一个类构建自动获得父类的字段和方法同时可以扩展或修改多态允许一个引用类型在不同时刻指向不同对象调用同一个方法却产生不同的行为。这个设计不是拍脑袋想出来的它是为了解决真实工程里的三组矛盾第一需求一直在变但调用方不应该跟着变所以要封装第二代码不能没完没了地复制粘贴所以要继承第三新的功能要加进来但老的代码不应该改来改去所以要有多态来支撑扩展。2. 封装隐藏细节暴露接口2.1 封装的核心逻辑不是“把字段私有化”这么简单很多人理解封装就是“private私有化加getter/setter”。这种理解太浅了甚至可以说不完全对。封装的核心思想是信息隐藏也就是把一个对象的内部状态和实现细节藏起来只通过有限的公开方法来访问和操作。为什么这么做核心原因有三条。第一条保护数据完整性。如果字段直接public暴露调用方就可以随便赋一个非法值进去。比如有一个年龄字段如果直接public别人赋个-1你拦都拦不住。但封装之后你在setter方法里可以写校验逻辑小于0的直接抛异常或者给默认值。第二条降低耦合方便变更。封装之后字段的变化只影响类的内部不影响外部调用方。经典案例是你有一个类内部存的是int类型的数量字段后来需求变了要把数量改成long甚至BigDecimal。如果字段是直接暴露的所有调用方都要改但如果封装好了你只需要改类的内部实现和getter的返回值处理外部代码一行都不用动。这在真实的项目里尤其重要因为十几个人维护同一个代码库每一行外部的改动都可能引入新的问题。第三条控制访问权限这是安全层面的考虑。有些数据是内部计算用的中间值比如缓存里的临时数据、配置加载的状态标识这些数据不能随便被外面读到。通过private把它们藏起来就相当于锁上了门。我见过太多刚入行的开发把类的字段全部写成public图省事结果后来需求一变整个代码库像多米诺骨牌一样倒下去。封装不是代码洁癖是工程上的防御工事。2.2 访问修饰符选择不是越严越好Java提供了四个访问修饰符它们从宽到窄分别是public、protected、默认包私有、private。选哪个不是拍脑袋而是取决于字段的语义和谁需要访问它。画一张表来对比修饰符同类同包子类不同包全局典型用途public可见可见可见可见对外API、工具方法protected可见可见可见不可见供子类扩展的钩子方法默认可见可见不可见不可见包内协作的辅助方法private可见不可见不可见不可见内部字段、内部实现实际开发中我的经验是字段一律private这没什么好商量的方法优先private只有当它明确要被外部调用时才往上升权限protected要克制使用因为它允许子类访问用多了会让继承体系变得难以维护默认包私有权限其实很有用在同一个包内做协作开发时可以适度使用但它对包外是封闭的。一个容易被忽略的点是getter/setter不是封装的必备品。如果一个字段只在内部使用外部根本不需要读和写那就不应该给它配getter/setter。我看到很多代码把所有字段都配上getter/setter哪怕这个字段连外部访问的场景都没有这反而破坏了封装——暴露了不该暴露的内部状态。封装的本质是“能不暴露就不暴露”而不是“把所有字段都包一层皮”。2.3 构造器的封装技巧不可变对象与构建器模式字段私有化只是封装的第一步真正精妙的设计在于如何让对象的创建和使用变得更安全。一种非常实用的设计是“不可变对象”。比如String类就是不可变的它的所有字段都是private final不提供任何修改方法。不可变对象的好处是天然线程安全、可以安全地作为Map的key、可以作为常量池共享。我在写配置类、值对象Value Object、数据传输对象DTO时都会优先考虑做成不可变的。实现不可变对象有几个要点所有字段声明为private final不提供setter构造器里完成全部赋值如果字段是可变对象比如List、Date要对传入的值做防御性复制不能直接把外部引用存进来。举个例子public final class User { private final String name; private final ListString roles; public User(String name, ListString roles) { this.name name; this.roles new ArrayList(roles); // 防御性复制 } public String getName() { return name; } public ListString getRoles() { return new ArrayList(roles); // 返回副本防止外部修改 } }这里有两个小细节值得注意。第一个是构造器里的new ArrayList(roles)如果不做这步复制外部传入的list和对象内部的list就是同一个引用外部一改对象内部的数据就跟着被污染了。第二个是getRoles()也返回副本否则外部拿到内部list的引用后照样可以往里加元素不可变性就破了功。还有一类常见需求是对象字段太多构造器参数又长又难记。这时候用Builder模式比写一堆重载构造器要优雅得多。Builder模式本质也是一种封装——它把复杂的构造过程包起来对外只暴露语义清晰的方法调用。比如Lombok的Builder注解大家可能天天在用但很少去想这背后做的事情就是封装了对象的创建过程。2.4 封装在真实项目中的落地场景理论知识说完了看看封装在真实项目里都用在哪些地方。首先是分层架构。在典型的Controller-Service-DAO三层架构里每一层对外暴露的是接口内部实现完全封闭。Controller不知道Service怎么查数据库的Service不知道Controller怎么解析参数的。这就是封装在架构层面的体现——层与层之间通过方法调用协作实现细节互相不可见。其次是DTO、VO、Entity这些对象的设计。这些类型本质上就是封装的具体产物Entity封装数据库表的行数据DTO封装接口传输的数据VO封装视图层需要展示的数据。很多时候三门需要用到的字段不一样不能塞在一个类里到处传得分别封装、通过转换器互相转换。我再举一个真实项目的例子。之前我在做一个支付系统用户发起退款后要给用户的第三方账户打款。这个打款动作涉及很多步骤校验账户、计算手续费、调外部接口、记录流水。如果把这些步骤的逻辑全部摊开放到Controller里那代码会非常臃肿而且任何一个步骤变了都要改Controller。我们的做法是设计了一个RefundService对外只暴露一个refund(RefundRequest request)方法内部走完整流程。外部调用方根本不需要知道流程怎么走的它只知道“调这个方法就能退款”。这就是封装的价值——内部再复杂对外永远是简单的接口。封装方面还有个很常见的实操点就是常量管理。代码里不要直接写魔法数字和裸字符串应该封装成枚举或者常量类。比如状态字段用OrderStatus.PAID而不是直接用1支付渠道用PayChannel.WECHAT而不是直接用wx。这种做法看起来只是代码规范本质上也是封装思想的体现把这些值背后的含义和可能的校验逻辑封装在一个地方使用方不需要关心“这个1代表什么”这样的细节。3. 继承复用与扩展3.1 继承的底层机制从super到构造器链继承在Java里的语法很简单一个extends关键字就完事了。但它底层的机制很值得细讲因为面试官特别爱在这里刨根问底。继承能拿到什么可以继承父类的非private字段和方法。private成员虽然在内存中是真实存在的但对子类不可见、不能直接访问。这就像你爸有笔私房钱但你不知道密码也碰不到——它存在但对你不可用。protected成员是向子类开放的这是继承体系中很重要的设计有些东西不对外公开但对子类可以开放。super关键字有几种用法。第一在子类构造器第一行调super(参数)来显式调用父类构造器第二用super.xxx()来调用被重写的父类方法第三用super.xxx访问父类的字段。这里有个硬性规则如果父类没有无参构造器子类构造器必须显式调用super(参数)否则编译直接报错。因为Java要求在创建子类对象时必须先完整创建好父类部分叫父类构造器先执行这是由JVM的对象内存布局决定的——子类对象在内存中就是“父类部分子类部分”父类部分必须先初始化完毕。构造器链是继承里非常常考的点。看下面这段代码class Animal { Animal() { System.out.println(Animal构造器); } } class Dog extends Animal { Dog() { System.out.println(Dog构造器); } } public class Demo { public static void main(String[] args) { new Dog(); } }输出顺序是什么先输出“Animal构造器”再输出“Dog构造器”。这是因为JVM在创建Dog对象时会先递归地调用父类构造器从最顶层的Object开始逐层往下。整个调用链是Object构造器 - Animal构造器 - Dog构造器。还有一个面试常问的细节父子类的静态代码块、实例代码块、构造器之间的执行顺序。这个顺序是父类静态块 - 子类静态块 - 父类实例块 - 父类构造器 - 子类实例块 - 子类构造器。静态块只在类加载时执行一次实例块每次创建对象都执行。这个顺序如果你能在面试中口述清楚并且能解释为什么基本就能让面试官觉得你是真的懂继承而不是在背答案。3.2 方法重写规则与误区方法重写override是继承中最常出问题的环节。它指的是子类重新实现父类的方法要求方法签名一致满足“两同两小一大”规则。“两同”是指方法名相同、参数列表相同。“两小”是指子类方法的返回值类型小于等于父类可以缩小称为协变返回类型抛出的受检异常也小于等于父类声明的。“一大”是指子类方法的访问权限大于等于父类也就是说父类是public子类不能改成private否则编译报错。为什么会这样因为子类必须能够替代父类的使用场景。如果父类的公共方法在子类里变成私有的那所有通过父类引用调用这个方法的代码就全崩了——这直接破坏了多态性的根基。重写的时候有个常见误区是重载和重写混淆。重载是同一个类里方法名相同但参数列表不同重写是父子类之间方法签名完全一致。很多人在代码里写着写着不小心把重写写成了重载比如父类是void doSth(String s)你想着要重写结果写成void doSth(String s, int x)这其实是新增了一个方法根本不是重写。如果要重写一定要加Override注解这样写错了编译器会直接报错而不是让你在运行期才发现走的是父类的逻辑。另外一个实用性很强的点调用被重写方法时的规则。如果一个方法在子类中被重写了那么通过父类引用调用这个方法时执行的也是子类的版本——这个规则叫动态绑定也叫运行时多态。我在实际开发中就踩过类似坑父类构造器里调用了一个方法而这个方法被子类重写了结果创建子类对象时父类构造器里调用的却是子类还没完全初始化时的实现——这是非常经典的“构造器里调用可重写方法”陷阱。正确的做法是构造器里只调用private或final方法杜绝被重写的可能。3.3 继承的常见陷阱从“继承复用”到“组合优先”继承最大的优势是代码复用但继承被滥用的后果也相当严重。这里我想好好聊一聊因为这是搜索引擎里“不同的继承方式”“多继承”“c多态”等热词背后大家真正关心的问题。Java只支持单继承一个类只能有一个直接父类。为什么因为多继承会导致菱形继承问题——C类同时继承A类和B类而A和B都继承了同一个父类DD里有一个方法A和B各自重写了它那C.getInstance().dosth()到底调用A的还是B的这个冲突在多继承的语言里非常难解决。Java的取舍是类的继承只允许单继承但接口可以多实现用接口来弥补多继承的能力。后面讲多态的时候细说。继承还有一个问题就是打破封装。子类继承父类的内部实现时如果父类的实现细节改动子类很可能在不知情的情况下崩溃。经典例子是父类的方法A调用了一个受保护的方法B子类重写了B结果父类的A方法的行为完全变了。这种情况下父类的逻辑不再是它看上去的样子子类的重写间接改变了父类的行为这在维护中非常难排查。所以现在工程界的主流观点是“组合优先于继承”如果一个类需要复用另一个类的功能优先考虑把这个类作为自己的字段组合而不是去继承它。组合的好处是松耦合我可以随时替换内部实现的对象而不会影响外部的使用。继承更像是“强绑定”一旦继承了父类和子类的关系一辈子改不了。那我什么时候用继承我的经验是当你确认子类和父类之间是纯粹的“is-a”关系子类确实需要父类的全部公共行为和受保护行为并且子类不会去修改父类的核心逻辑只是做一些扩展时才考虑继承。举个例子ArrayList和AbstractList的关系就是继承——ArrayList是一个List并且在AbstractList基础上扩展了实现。但如果只是为了复用某个工具方法而去继承一个类这就大错特错了应该改成组合加一个工具类调用。热搜词里有一条“企微继承异常”这是我真实的项目里出现过的问题。有一个类继承了企业微信SDK的某个消息处理基类用于自定义消息类型。结果SDK升级之后基类的构造函数签名变了子类调用的super参数不匹配编译期还看不出问题一运行就抛异常。这种问题在继承体系里很常见尤其是继承了外部框架的类时。每次依赖框架升级都要重新审视自己的子类是否还兼容——这就是继承带来的维护成本。3.4 继承与构造器重载的配合还有一个实操细节我觉得很值得展开父类有很多重载构造器时子类应该如何使用。假设父类有三个构造器参数分别是无参、单参数、双参数。子类设计构造器时不一定要为父类的每个构造器都提供对应的重载。子类可以根据自己的需要在构造器里明确调用父类的某个构造器即可。但这里有一个非常容易出问题的场景父类没有无参构造器只提供了一个带参数的构造器。子类如果不显式调用父类的带参构造器编译直接报错。很多新手在这里抓狂为什么我的子类明明什么都没写编译就是过不去。原因就是编译器默认调用父类的无参构造器而父类的无参构造器不存在。解决办法是在子类构造器第一行显式调用super(参数)。我建议在写父类时尽量提供一个无参构造器作为兜底。即使你目前所有的构造器都带参数也建议补一个无参的。因为你无法预料未来的子类需要什么样的构造方式。当然如果你的类设计上就必须依赖某个参数才能工作、不允许无参使用那就不提供了让编译器强制子类必须显式调用带参构造器——这种强约束也是一种设计意图的表达。4. 多态面向接口编程4.1 多态的底层机制动态绑定与虚方法表多态是三大特性中最抽象、最“面向对象”的一个。通俗地说多态就是“同一个引用类型指向不同的对象调用同一个方法表现出不同的行为”。Java中多态的实现依赖两个前提一是必须有继承或实现关系二是必须有方法重写。用个例子来说Animal animal new Dog(); animal.makeSound(); // 输出“汪汪” Animal animal2 new Cat(); animal2.makeSound(); // 输出“喵喵”编译时animal的类型被声明为Animal但运行时它实际指向的对象是Dog。调用makeSound()时JVM执行的是Dog重写后的版本。这个能力叫动态绑定也叫运行时多态或晚绑定。JVM是怎么做到动态绑定的这里要提到虚方法表vtable的概念。每个类在加载后JVM会给它生成一个方法表里面记录了该类所有方法的实际入口地址。子类在继承父类时会复制父类的方法表然后把重写的方法替换成自己的实现。当调用一个方法时JVM根据对象的实际类型去查那个类的方法表找到对应的方法入口来执行。这个查表过程非常快是Java方法调用性能高的一个重要原因。很多人在面试时说“多态能提高代码灵活性和可扩展性”这不难答。但如果你能把动态绑定、虚方法表、重写方法查找这些底层机制说出来效果就是完全不一样的。4.2 重载与重写绕不开的对比重载和多态的关系常被误解。正确来说重载是编译时多态重写是运行时多态。一个在编译期就确定调用哪个方法一个要等到运行期才能确定。重载的判断依据是方法的参数列表比如print(int a)和print(String s)是两个不同方法编译器根据传入的参数静态决定调用哪一个。运行时多态发生在继承体系中子类重写了父类的方法调用哪个版本取决于对象实际运行时类型编译期无法确定。看一个经典面试题class A { public void show() { System.out.println(A); } } class B extends A { public void show() { System.out.println(B); } } public class Test { public static void main(String[] args) { A a new B(); a.show(); // 输出“B” } }为什么输出“B”因为a虽然声明为A类型但实际指向的是B对象调用show()时JVM根据实际类型去方法表里查找找到了B重写的版本。这里有一个新手容易混的点引用类型决定你“能调什么方法”对象的实际类型决定你“调出的方法具体执行什么逻辑”。还有一个容易被考的细节是父类引用调用子类独有方法。比如B类里有一个onlyB()方法A a new B(); a.onlyB();能编译吗不能。因为编译期看到的是A类型的引用A类型里面没有onlyB()这个方法编译直接报错。这时需要向下转型((B) a).onlyB()。但向下转型有风险如果实际对象不是B类型运行期会抛ClassCastException。安全的下转方式是用instanceof先判断if (a instanceof B) { ((B) a).onlyB(); }4.3 多态的经典应用不只存在于语法里多态在实际项目中无处不在最关键的应用就是面向接口编程。接口在Java中天然就是多态的载体。一个接口可以有无数个实现类调用方依赖接口而不依赖具体实现类。比如搜索热词里有“接口封装”我就可以用接口的多态性来解释它的意义。定义一个支付接口PaymentService三个实现类分别处理支付宝、微信、银行卡支付。业务代码只需要注入PaymentService接口不需要关心当前用户选的是哪个支付渠道调用pay()方法时自动根据实际对象执行对应的支付逻辑。这样新增一个支付渠道时只需要新写一个实现类业务代码一行不动。这就是开闭原则——对扩展开放对修改关闭。策略模式是多态最常见的设计模式。比如我要实现一个促销折扣功能折扣策略可能是“满减”“折扣”“无优惠”三种。如果用if-else写每新增一种策略都要改原来的代码用多态来写定义一个DiscountStrategy接口三个实现类各自封装自己的计算逻辑调用方持有接口引用在运行时传入具体的策略对象即可public interface DiscountStrategy { double calculate(double price); } public class FullReductionStrategy implements DiscountStrategy { Override public double calculate(double price) { return price 300 ? price - 50 : price; } } public class PercentDiscountStrategy implements DiscountStrategy { private double percent; // 比如 0.8 表示八折 public PercentDiscountStrategy(double percent) { this.percent percent; } Override public double calculate(double price) { return price * percent; } }调用方代码public void checkout(double price, DiscountStrategy strategy) { double finalPrice strategy.calculate(price); System.out.println(实付: finalPrice); }像这样调用方只依赖DiscountStrategy这个接口具体的策略对象由上层创建传入。后续加新策略时checkout方法一行都不用改。这就是多态带的架构红利。工厂模式也是建立在多态之上的。工厂方法返回的是接口类型内部决定返回哪个实现类。调用方拿到的是接口引用只调用接口方法。自己需要封装什么功能时想一想工厂模式往往能让代码干净很多。4.4 多态的一些进阶思考多态用好了可以让代码非常优雅但它不是银弹也有一些边界要把握。第一多态是面向对象语言的魅力所在但滥用会导致类型关系复杂难以维护。一个接口下面几十个实现类虽然有灵活性但排查问题时你根本不知道运行到哪一条分支。应对方式是把实现类数量控制在合理范围实在太多可以考虑用枚举加策略组合或者注册表模式来管理。第二重写方法是多态的触发条件但在重写时如果父类方法做了一些必要的数据准备子类重写时如果不调用super.method()那父类的准备工作就全部丢失。这是一个常见的Bug来源。重写时优先考虑是否需要super调用如果需要放在什么位置调用都会影响执行顺序。第三多态与构造器的交互是个大坑。前面我在讲继承时提过“父类构造器里调用可重写方法”。在构造器里调用被子类重写的方法在Java里是合法的但运行结果往往出人意料——因为父类构造器执行时子类的字段还没初始化如果子类重写的方法读取了这些字段读到的都是默认值比如null或0。这在多态体系里是个经典的陷阱导致的bug非常难查。我的原则就是构造器里绝不调用任何非final、非private的方法。第四多态是“面向抽象编程”的核心但它不等于完全不用具体类。在一些只属于单一实现的地方直接用具体类是合理的。过度抽象反而是工程灾难。这个度的把握靠的是经验和业务理解。5. 面试怎么答、项目怎么落地避开套话的实用指南5.1 面试官真正想听什么回到很多人关心的“八股文”问题。封装继承多态这题面试官问出来表面是考基础实际是考三个层次第一层你知不知道这三个概念说的是什么。这个回答不难能说出“封装隐藏细节、继承实现复用、多态实现扩展”即可过关。第二层你能不能说出三个特性之间的关联和边界。比如问“继承和组合如何选择”“重写和重载的区别和联系”“多态是不是只能在继承关系中实现”这类问题答得好说明你真的理解而不只是背定义。第三层你能不能结合自己的项目说清楚“我在哪里用到了多态”“你是如何理解封装的边界”。如果项目中设计过一个策略模式、把公共逻辑抽到抽象类、用private字段加getter控制访问都可以作为例子。面试官要的不是名词解释是工程判断力。每当看到面试者在背诵“封装继承多态体现了面向对象的思想”这句套话我都会追问一句什么思想要有好的回答建议准备一两个自己在实际项目中用这三个特性的具体场景和决定过程。比如可以说“我当时在一个订单处理模块里发现不同渠道的订单状态流转逻辑差异很大但又共用一套基础校验流程所以我把公共逻辑放到了抽象基类不同渠道各写一个子类然后通过多态来调用。这既复用了公共代码也给后续新增渠道留了扩展空间。”这种回答立刻就能和背答案的人区别开。5.2 常见高频题目和易错点我整理几道面试中出现频率最高的题作一个快速清单题目核心考点易错点重写和重载的区别编译时多态 vs 运行时多态搞混两者判断依据为什么Java不支持多继承菱形继承、方法冲突不理解接口弥补的原因构造器里能否调用被重写方法动态绑定、初始化顺序不知道子类字段还没初始化抽象类和接口如何选择is-a vs can-do机械背语法而不是讲清楚语义向上转型和向下转型引用类型与实际类型不注意instanceof判断而强转继承和组合如何选择耦合度、维护成本认为继承代码量少就优先用这里我想展开说一下抽象类和接口的选择因为热搜词里也有“接口封装”相关的内容。很多新人搞不清楚什么场景用抽象类、什么场景用接口。我的经验是抽象类适合描述“是一个什么东西”强调的是代码复用适合多个类共享同一个实现骨架的情况比如模板方法模式接口适合描述“能做什么”强调的是行为契约适合定义功能规范、支持不同实现的替换。比如我现在要描述动物这个体系哺乳动物、鸟类都是“是动物”的关系公共字段名字、年龄和公共行为呼吸可以放在抽象基类里。但如果我要描述的是一个“飞行能力”鸟能飞、飞机能飞它们共用的是一个行为能力而不是一个父类体系就应该定义Flyable接口。实际工程里接口的优先级通常更高优先考虑面向接口编程在有明确的代码复用时才考虑抽象类。5.3 实操中三个特性的配合案例说了这么多理论最后放一个综合案例看三个特性怎么在真实代码里配合起来。我现在要写一个消息推送模块支持短信、邮件、站内信三种推送方式。用三大特性来设计第一步封装。定义一个Message类字段全部private提供带校验的setter比如收件人不能为空、内容不能为空。另外定义一个PushResult类封装推送结果数据——是否成功、错误码、耗时。这些类把数据和行为绑定在一起外部通过构造器和getter使用不能随意篡改内部状态。第二步继承。建一个抽象基类AbstractPushService里面有公共的流程先做参数校验再记录日志再真正发送最后统计耗时。其中发送逻辑定义成抽象方法交给子类实现。短信、邮件、站内信三个子类分别继承这个抽象基类各自实现doSend()方法。第三步多态。定义一个PushService接口抽象基类实现这个接口。业务调用方通过PushService接口注入不同的实现或者通过工厂根据用户配置返回具体的推送服务对象。调用方只管push(Message message)至于走短信还是邮件由运行时传入的对象类型决定。这整段代码跑下来你会发现它好维护的点在哪要加一种新的推送渠道时只需要新写一个子类其他代码不动某个推送渠道的实现细节变了只影响它自己的类调用方完全不感知底层渠道是怎么工作的。这就是三大特性叠加起来的工程威力。写到这里我想分享一点个人经验。面试的时候、项目里评审代码的时候我都会先看人写的类是不是封装得干净、继承关系是不是合理、是不是在用多态处理变化。三大特性会被算法题、微服务、大数据冲淡存在感但无论框架换了多少代这三板斧永远是Java后端代码的质量基石。每次处理线上问题兜兜转转最后定位到的往往就是当时写代码时一个小小的设计选择没有尊重这三大特性。能把这三件事做对后面接再多复杂框架都有底气。
返回列表