面向对象基础:2. 类与类之间的关系
搞懂这6种类关系你的面向对象设计才算入门很多初学者学完了类、对象、方法这些基础概念后就卡在了怎么把这些类组织起来这一步。写出来的代码要么耦合度极高牵一发而动全身要么结构混乱自己过一个月都看不懂。其实问题的根源就在于没有真正理解类与类之间应该建立什么样的关系。先记住一个总原则所有类关系本质上都是在描述两个类之间的耦合程度。耦合度越高两个类的绑定就越紧密一个类的改动对另一个类的影响就越大。所以一个优秀的面向对象设计永远遵循能使用弱关系就绝不使用强关系。6种关系按耦合度从弱到强排序依赖 关联 聚合 组合 实现 继承1. 依赖关系Dependency临时用一下最弱的关系。一个类在某个方法的执行过程中临时用到了另一个类用完就没关系了。核心特点关系描述uses-a使用一个生命周期仅存在于方法执行期间表现形式方法参数、局部变量、静态方法调用通俗比喻你Person开车Car去上班。在开车的这段时间里你依赖这辆车但开完车之后你和这辆车就没有任何关系了。你不会一直带着这辆车走。代码示例classCar{publicvoidrun(){System.out.println(汽车在行驶);}}classPerson{// 依赖关系Car作为方法参数传入publicvoidgoToWork(Carcar){System.out.println(准备去上班);car.run();// 临时使用Car的方法System.out.println(到达公司);}}设计建议这是最推荐使用的关系。如果两个类之间只是临时协作就用依赖。它的耦合度最低改动一个类对另一个类的影响最小。2. 关联关系Association长期持有平等的长期关系。一个类作为成员变量持有另一个类的引用这种关系会持续整个对象的生命周期。核心特点关系描述has-a有一个生命周期与持有方对象同生命周期方向可以是单向的也可以是双向的通俗比喻你Person有一部手机Phone。手机是你的个人物品你会一直带着它。你和手机之间是长期的持有关系但你们是两个独立的个体。你换手机了你还是你手机被你扔了手机还是手机。双向的关联关系彼此知道对方的存在但是并不负责对方的生命周期。在语义层面A has B。在代码层面上可以使用指针或者引用。在类图的画法就是一根实心直线。单向的关联关系A知道B的存在但是并不负责对方的生命周期。在语义层面A has B。在代码层面上可以使用指针或者引用。在类图的画法从A指向B的实线箭头。代码示例classPhone{publicvoidcall(Stringnumber){System.out.println(正在拨打number);}}classPerson{// 关联关系Phone作为成员变量长期持有privatePhonephone;// 通过构造方法注入依赖publicPerson(Phonephone){this.phonephone;}publicvoidmakeCall(Stringnumber){phone.call(number);// 使用持有的Phone对象}}设计建议当两个类需要长期稳定的协作时使用关联关系。但要注意避免双向关联双向关联会增加代码的复杂度和维护成本。3. 聚合关系Aggregation整体与部分可分离特殊的关联关系。它描述的是整体和部分的关系但部分可以脱离整体独立存在。核心特点关系描述has-a整体包含部分关键区别部分与整体生命周期不同步表现形式成员变量通常通过构造方法或setter注入通俗比喻班级Class和学生Student。班级是整体学生是部分。一个班级由多个学生组成但学生可以脱离班级独立存在。班级解散了学生还在学生转班了班级也还在。代码示例classStudent{privateStringname;publicStudent(Stringname){this.namename;}publicStringgetName(){returnname;}}classClass{// 聚合关系持有Student的列表privateListStudentstudents;// 学生是在外部创建好之后再传入班级的publicClass(ListStudentstudents){this.studentsstudents;}publicvoidshowStudents(){for(Studentstudent:students){System.out.println(student.getName());}}}设计建议聚合关系在实际编码中很难从语法上区分于普通的关联关系。它更多的是一种语义上的区别。当你想表达整体包含部分但部分可以独立存在的语义时就用聚合。4. 组合关系Composition整体与部分不可分离最强的包含关系。同样是整体和部分的关系但部分完全依赖于整体不能脱离整体独立存在。整体销毁部分也会跟着销毁。核心特点关系描述contains-a包含一个关键区别部分与整体生命周期完全同步表现形式在整体的构造方法中直接创建部分对象通俗比喻人Person和心脏Heart。心脏是人的一部分不能脱离人独立存在。人出生的时候心脏就跟着长好了人死了心脏也就没用了。代码示例classHeart{publicvoidbeat(){System.out.println(心脏在跳动);}}classPerson{// 组合关系持有Heart的引用privateHeartheart;// 心脏是在创建人的时候在内部直接创建的publicPerson(){this.heartnewHeart();}publicvoidlive(){heart.beat();System.out.println(人活着);}}设计建议组合关系的耦合度比聚合高但它也带来了更强的封装性。当部分对象完全属于整体对象且没有独立存在的意义时就使用组合。面试必考点聚合 vs 组合这是面试中最常问的问题。记住一句话聚合你有我也有你无我还有可分离组合你有我才有你无我也无同生共死5. 实现关系Implementation遵守契约接口与实现类之间的关系。一个类实现了一个接口就必须遵守接口定义的所有契约重写所有抽象方法。核心特点关系描述like-a像一个作用定义行为规范实现多态耦合度中等偏高通俗比喻劳动合同Flyable接口和员工Airplane类。劳动合同规定了员工必须完成的工作fly()方法。任何签订了这份合同的员工都必须完成这些工作。代码示例// 接口定义飞行的契约interfaceFlyable{voidfly();}// 实现类遵守飞行契约classAirplaneimplementsFlyable{Overridepublicvoidfly(){System.out.println(飞机在万米高空飞行);}}// 另一个实现类同样遵守飞行契约classBirdimplementsFlyable{Overridepublicvoidfly(){System.out.println(小鸟在天空中飞翔);}}设计建议实现关系是面向接口编程的基础。它将行为的定义和行为的实现分离开来极大地提高了代码的灵活性和可扩展性。优先使用接口来定义类型。6. 继承关系Inheritance父子传承耦合度最高的关系。一个类子类继承另一个类父类自动拥有父类的所有非私有属性和方法并且可以扩展自己的属性和方法。核心特点关系描述is-a是一个作用代码复用实现多态耦合度最高通俗比喻父亲Animal和儿子Dog。儿子继承了父亲的基因和财产属性和方法但儿子也可以有自己的个性扩展方法。代码示例// 父类动物classAnimal{protectedStringname;publicAnimal(Stringname){this.namename;}publicvoideat(){System.out.println(name在吃东西);}}// 子类狗继承自动物classDogextendsAnimal{publicDog(Stringname){super(name);// 调用父类构造方法}// 扩展自己的方法publicvoidbark(){System.out.println(name在汪汪叫);}}设计建议慎用继承继承是耦合度最高的关系父类的任何改动都会直接影响到所有子类。只有当两个类之间确实存在是一个的关系时才使用继承。否则优先使用组合来实现代码复用。经典设计原则优先使用组合而非继承Favor composition over inheritance一张表总结所有关系为了方便大家记忆和复习我把6种关系整理成了一张对比表关系类型英文核心语义耦合度生命周期代码表现典型例子依赖Dependencyuses-a使用最低方法级方法参数、局部变量人开车关联Associationhas-a持有中等对象级成员变量人有手机聚合Aggregationhas-a整体-部分可分离中高不同步成员变量外部注入班级有学生组合Compositioncontains-a整体-部分不可分离高同步成员变量内部创建人有心脏实现Implementationlike-a像一个高-implements关键字飞机实现飞行接口继承Inheritanceis-a是一个最高-extends关键字狗是动物写在最后类与类之间的关系是面向对象设计的基石。理解了这些关系你才能设计出低耦合、高内聚的优秀代码。最后再强调一遍设计的黄金法则能用依赖解决的就不用关联能用关联解决的就不用聚合能用聚合解决的就不用组合能用组合解决的就不用继承面向接口编程而不是面向实现编程