4. UML类图与设计模式表达:类、接口、继承、实现、依赖、关联、聚合、组合关系的UML表示

UML类图这东西,说白了就是程序员之间的「通用语言」。我刚开始带团队那会儿,发现很多人画图全凭感觉,箭头乱飞。结果呢?代码写出来跟设计图完全是两码事。嗯,今天我们就来把这几种关系彻底捋清楚。

4.1 类与接口的基本表示

先看最基础的。一个类在UML里就是个三格矩形:

  • 第一格:类名(如果是抽象类,用斜体)
  • 第二格:属性(字段)
  • 第三格:方法(操作)

接口呢?顶部加个 <<interface>> 标记,或者用「棒棒糖表示法」——一个小圆圈加一条线。我个人习惯用前者,更直观。

访问修饰符符号:

  • + 公开(public)
  • - 私有(private)
  • # 受保护(protected)
  • ~ 包内可见(package)
// 对应的Java代码
public class User {
    private String name;
    private int age;
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

public interface IUserService {
    void save(User user);
    User findById(int id);
}

4.2 继承与实现——「is-a」关系

继承用空心三角形 + 实线。实现用空心三角形 + 虚线。区别就一条线,但语义完全不同。

继承是「子类是父类的一种」。实现是「类遵守了接口的契约」。

我的经验: 我在项目中见过有人把接口实现画成继承箭头。结果新同事看了图,以为接口是个抽象类,直接在里面写字段。嗯,这种低级错误,画图时多看一眼就能避免。

// 继承
class Animal { }
class Dog extends Animal { }

// 实现
interface Flyable { }
class Bird implements Flyable { }

4.3 依赖关系——「uses-a」关系

依赖用虚线箭头。箭头指向被依赖方。这是最弱的关系——一个类临时用了另一个类。

什么时候算依赖?方法参数、局部变量、静态方法调用。说白了就是「用完就扔,不长期持有」。

避坑指南: 我曾经在重构一个支付模块时,发现类图里画了一大堆依赖箭头。仔细一看,全是把关联画成了依赖。结果代码里到处都是new对象,耦合得一塌糊涂。记住:依赖是临时的,关联是长期的。

public class OrderService {
    // 依赖:方法参数
    public void createOrder(Product product) {
        // 依赖:局部变量
        Logger logger = new Logger();
        logger.log("创建订单");
    }
}

4.4 关联关系——「has-a」关系

关联用实线箭头。表示一个类知道另一个类的存在,并且长期持有引用。最常见的体现就是成员变量。

关联可以标注多重性

标记 含义
0..1 零或一个
1 恰好一个
0..* 零到多个
1..* 一到多个
* 任意多个
public class Customer {
    // 关联:成员变量
    private List<Order> orders;  // 1..*
}

public class Order {
    private Customer customer;   // 1
}

4.5 聚合与组合——「整体-部分」关系

这两个最容易搞混。我当年面试时就被问倒过。你想想看:

  • 聚合:空心菱形 + 实线。整体和部分可以分开存在
  • 组合:实心菱形 + 实线。整体和部分同生共死

经典例子:

  • 聚合:学校(School)和老师(Teacher)。学校没了,老师还在。
  • 组合:房子(House)和房间(Room)。房子拆了,房间也没了。
// 聚合
public class School {
    private List<Teacher> teachers;  // 老师可以换学校
}

// 组合
public class House {
    private List<Room> rooms;  // 房间随房子一起销毁
    public House() {
        rooms = new ArrayList<>();
        rooms.add(new Room());
    }
}

4.6 关系强度对比

从弱到强排个序:

  1. 依赖:临时使用,最弱
  2. 关联:长期持有引用
  3. 聚合:整体-部分,生命周期独立
  4. 组合:整体-部分,生命周期绑定
  5. 继承/实现:最强耦合,慎用

我的建议: 画图时先画继承和实现,再画关联和聚合。依赖关系我一般最后才补,因为它是「运行时」才体现的,设计阶段不用太纠结。

4.7 知识体系总览

下面这张图把六种关系放在一起对比。我特意把箭头样式和语义对应起来,方便你记忆。

UML类图六种关系速查 继承(泛化) 空心三角 + 实线 实现 空心三角 + 虚线 依赖 虚线箭头 关联 实线箭头 聚合 空心菱形 + 实线 组合 实心菱形 + 实线 关系强度:依赖 < 关联 < 聚合 < 组合 < 继承/实现 画图口诀:三角是继承,菱形是整体,虚线是临时,实线是长期 公众号:蓝海资料掘金营,微信deep3321

4.8 实战中的常见误区

最后说几个我踩过的坑:

  • 滥用继承:我曾经设计一个「动物」继承体系,猫、狗、鸟都继承自动物。结果鸟需要飞,猫需要游泳,继承树越来越乱。后来全改成组合,清爽多了。
  • 忽略多重性:关联关系不标数字,代码里用List还是Set?用数组还是集合?全靠猜。画图时顺手标上,省得后期扯皮。
  • 把依赖当关联:一个类在方法里new了另一个类,就画个关联箭头。这是错的,应该用依赖。关联意味着「长期持有」,依赖只是「临时借用」。

记住: UML图是沟通工具,不是艺术品。画到别人能看懂就行,别追求完美。我见过有人花三天画一张图,结果代码写出来完全不一样。嗯,本末倒置了。

好了,这六种关系你掌握了吗?下次画类图时,对着这张表检查一遍,保证不出错。