Java设计模式:工厂模式(简单工厂模式、工厂方法模式、抽象工厂模式)

❤ 作者主页:欢迎来到我的技术博客😎
❀ 个人介绍:大家好,本人热衷于Java后端开发,欢迎来交流学习哦!( ̄▽ ̄)~*
🍊 如果文章对您有帮助,记得关注点赞收藏评论⭐️⭐️⭐️
📣 您的支持将是我创作的动力,让我们一起加油进步吧!!!🎉🎉

文章目录

  • 一、为什么需要工厂模式
  • 二、什么是工厂模式
  • 三、未使用工厂模式的案例
  • 四、简单工厂模式
    • 1. 简单工厂模式的定义
    • 2. 简单工厂模式的结构
    • 3. 简单工厂模式的实现
    • 4. 简单工厂模式的优缺点
  • 五、工厂方法模式
    • 1. 工厂方法模式的定义
    • 2. 工厂方法模式的结构
    • 3. 工厂方法模式的实现
    • 4. 工厂方法模式的优缺点
  • 六、抽象工厂模式
    • 1. 抽象工厂模式的定义
    • 2. 抽象工厂模式的结构
    • 3. 抽象工厂模式的实现
    • 4. 抽象工厂模式的优缺点
  • 七、工厂模式总结

一、为什么需要工厂模式

在软件开发中,创建对象实例最常用的方式就是通过 new 操作符直接生成对象,但是有时候需要根据不同的条件来创建不同类型的对象,而直接在代码中使用 new 关键字创建对象会导致代码的耦合度增加,不利于系统的维护和扩展。在这种情况中,创建对象不仅仅只是一个 操作,而是有一个复杂的 过程

因此,对于出现的这种的情况,我们怎么做到就是轻松方便地构造对象实例,而不用去关心对象实例的细节以及创建中的复杂流程呢?这时候我们引入 工厂类 来负责对象的创建。


二、什么是工厂模式

在工厂模式中,通常会定义一个 抽象工厂接口,该接口声明了创建对象的方法,具体的工厂类 实现了这个接口,并负责实际 创建对象的过程。客户端代码通过使用工厂接口来创建对象,而不直接使用 new 关键字实例化对象,从而实现 对象创建和使用的解耦

工厂模式可以分为三类:

  • 简单工厂模式(Simple Factory)
  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)

这三种模式从上到下逐步抽象,并且更具一般性。在《设计模式》这本书中将工厂模式分为两类:工厂方法模式和抽象工厂模式,将简单工厂模式看作是工厂方法模式中的一种特例,在实际开发中,简单工厂模式反而更像是一种 编程习惯


三、未使用工厂模式的案例

在引入简单工厂模式之前,我们先来演示一下没有使用工厂模式的业务场景。

在没有使用工厂模式的情况下,当客户前往手机店购买苹果手机时,手机店必须亲自进行苹果手机的制造工作;同样,如果客户想购买小米手机,手机店也必须自行进行小米手机的制造工作。

类图如下:
在这里插入图片描述

具体的类设计如下:

手机抽象类:

public abstract class Phone {

    public abstract  String getName();

    //外观定制
    public void addAppearance() {
        System.out.println("外观定制");
    }


    //硬件配置
    public void addConfiguration() {
        System.out.println("硬件配置");
    }

}

苹果手机类:

public class ApplePhone extends Phone {
    @Override
    public String getName() {
        return "苹果手机";
    }
}

小米手机类:

public class XiaomiPhone extends Phone {
    @Override
    public String getName() {
        return "小米手机";
    }
}

手机店类:

public class PhoneStore {


    public Phone orderPhone(String type) {

        //声明Phone类型的变量,根据不同类型创建不同的phone子类对象
        Phone phone = null;
        if ("Apple".equals(type)) {
            phone = new ApplePhone();
        } else if ("Xiaomi".equals(type)) {
            phone = new XiaomiPhone();
        } else {
            throw new RuntimeException("暂没有该手机型号");
        }

        //配置
        phone.addAppearance();
        phone.addConfiguration();

        return phone;
    }

}

客户端类:

public class Client {

    public static void main(String[] args) {

        //1. 创建手机店类
        PhoneStore store = new PhoneStore();

        //2. 购买手机
        Phone phone = store.orderPhone("Apple");

        System.out.println(phone.getName());

    }

}

至此,客户就可以在客户端中购买所需要什么品牌的手机,然后由手机店自己去制造手机后交付于客户。

说明:
在上述代码中 Phone 类被声明为抽象类,在抽象类中声明了一个抽象方法 getName(),但是没有具体的实现方法,而具体的实现留给子类去完成。其他两个方法是具体的实现方法,子类继承直接默认获得这两个功能,不需要再去实现。

这时候我们可以发现,这不就和接口特别相似,但是两者却有着不同之处,接下来我们讲一下 抽象类接口 区别:

抽象类:

  • 可以包含抽象方法和具体方法;
  • 可以包含字段(成员变量);
  • 不能被实例化,需要通过继承来使用;
  • 一个类只能继承一个抽象类;

接口:

  • 只能包含抽象方法;
  • 不能包含字段(成员变量);
  • 所有方法默认为 public,所有字段默认为 public, static, final
  • 可以被多个类继承(支持多重继承);

四、简单工厂模式

通过上面的案例,我们看到手机店不仅要卖手机还要自己去制造手机。因此我们引入了工厂模式,我们可以使用简单工厂模式来创建一个工厂类,由工厂类去实现手机的制造,而用户只需要在客户端传入工厂类的参数,工厂类就会去制造相对应的手机出来,手机店只需去卖给客户需要的手机即可,不需要关心手机是如何被制造出来的整个过程。

1. 简单工厂模式的定义

简单工厂模式的核心是定义一个创建对象的接口,将对象的创建和本身的业务逻辑分离,降低系统的耦合度,当需要改变的时候,只需要修改工厂类即可。


2. 简单工厂模式的结构

简单工厂模式包含以下的角色:

  • 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品 :实现或者继承抽象产品的子类
  • 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。

3. 简单工厂模式的实现

使用简单工厂模式对上面案例进行改造,类图如下:
在这里插入图片描述
 
具体的类设计如下:

对于手机抽象类、苹果手机类、小米手机类、客户端这几个类我们都不需要进行修改,我们只需要修改手机店类以及创建一个工厂类即可。

工厂类:

public class SimplePhoneFactory {

    public Phone createPhone(String type) {

        //声明Phone类型的变量,根据不同类型创建不同的phone子类对象
        Phone phone = null;
        if ("Apple".equals(type)) {
            phone = new ApplePhone();
        } else if ("Xiaomi".equals(type)) {
            phone = new XiaomiPhone();
        } else {
            throw new RuntimeException("暂没有该手机型号");
        }

        //配置
        phone.addAppearance();
        phone.addConfiguration();

        return phone;
    }
}

手机店类:

public class PhoneStore {

    public Phone orderPhone(String type) {

        //创建工厂类对象
        SimplePhoneFactory factory = new SimplePhoneFactory();
        //调用制造手机的方法
        Phone phone = factory.createPhone(type);

        //配置
        phone.addAppearance();
        phone.addConfiguration();

        return phone;
    }

}

4. 简单工厂模式的优缺点

优点:
简单工厂模式封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。

缺点:
每次添加新产品就需要修改工厂类,不符合 “开闭原则”。在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展维护,并且工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。


对上面提到的 开闭原则 进行讲解,开闭原则(Open/Closed Principle,OCP)是面向对象设计中一个重要原则,它包含了两个核心概念:

  1. 对扩展开发: 意味着在不修改现有代码的情况下,可以通过添加新的功能来扩展系统的功能。新功能的添加应该是通过添加新的类、模块、接口等方式进行的,而不是通过修改已有的代码。
  2. 对修改关闭: 意味着已经存在的代码不应该被修改,因为修改可能引入错误,破坏原有的稳定性。系统的行为可以通过扩展来改变,但不应该通过修改已有的代码来实现。

五、工厂方法模式

1. 工厂方法模式的定义

针对上述简单工厂模式中的缺点,使用 工厂方法模式 就可以完美解决,完全遵循开闭原则。

工厂方法模式将工厂抽象化,并定义一个创建对象的接口。每增加新产品时,只需增加该 产品类 以及该产品对应的 具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码。


2. 工厂方法模式的结构

工厂方法模式包含以下的角色:

  • 抽象工厂 : 提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
  • 具体工厂 : 主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品 : 定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品 : 实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一 一对应。

3. 工厂方法模式的实现

使用工厂方法模式对上面案例进行改造,类图如下:
在这里插入图片描述
 
具体的类设计如下:
对于手机抽象类、苹果手机类、小米手机类这几个类不需要修改,而手机店类和客户端类需要修改,并且新增抽象工厂(手机工厂)、具体工厂(苹果手机工厂、小米手机工厂)。

手机工厂:

public interface PhoneFactory {
    
    Phone createPhone();
}

苹果手机工厂:

public class ApplePhoneFactory implements PhoneFactory {
    @Override
    public Phone createPhone() {
        return new ApplePhone();
    }
}

小米手机工厂:

public class XiaomiPhoneFactory implements PhoneFactory {
    @Override
    public Phone createPhone() {
        return new XiaomiPhone();
    }
}

手机店类:

public class PhoneStore {

    private  PhoneFactory factory;

    public void setFactory(PhoneFactory factory) {
        this.factory= factory;
    }

    public Phone orderPhone() {

        Phone phone = factory.createPhone();

        //配置
        phone.addAppearance();
        phone.addConfiguration();

        return phone;
    }

}

客户端类:

public class Client {

    public static void main(String[] args) {

        //创建手机店对象
        PhoneStore store = new PhoneStore();

        //创建对象
//        ApplePhoneFactory factory = new ApplePhoneFactory();
        XiaomiPhoneFactory factory = new XiaomiPhoneFactory();
        store.setFactory(factory);

        //购买手机
        Phone phone = store.orderPhone();

        System.out.println(phone.getName());

    }

}

4. 工厂方法模式的优缺点

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
  • 在系统增加新的产品时只需要添加 具体产品类对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;

缺点:

  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

六、抽象工厂模式

首先,我们上述案例再新增点需求,比如我们还要生产苹果手表和小米手表等产品,按照工厂方法模式的话我们需要去创建每个产品对应的产品类以及对应的具体工厂类,如果新增的产品很多的话,那么很容易发生类爆炸情况。

我们试想一下可不可以这样实现,就是属于同一品牌的产品可以由同一工厂类去创建,比如创建一个苹果工厂类,它就可以同时生产出苹果手机和苹果手表等都属于苹果品牌的产品出来。因此,我们就引入了 抽象工厂模式

1. 抽象工厂模式的定义

定义:抽象工厂模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到 同族的不同等级的产品 的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产 一个等级 的产品,而抽象工厂模式可生产 多个等级 的产品。
 

使用抽象工厂模式一般要满足以下条件:

  • 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品;
  • 系统一次只能消费其中某一族产品,即同族的产品一起使用;

在介绍抽象工厂模式前,我们先理解一下产品等级和产品族这两个在软件设计中经常出现的概念:

产品等级

  • 产品等级指的是具有共同特性的产品集合,它们通常是在某个方面上具有共性或者属于同一类别的产品。
  • 例如,在汽车制造中,轿车、卡车和摩托车都可以构成不同等级的产品。它们都是交通工具,但在功能、结构和用途上有所不同。

产品族

  • 产品族是指不同等级产品中相关联的一组产品集合,它们之间可能存在某种内在的联系或者依赖关系。
  • 比如,在汽车制造中,汽车可能有多种不同型号,每种型号都有不同的配置和特点,比如轿车产品族可能包括轿车、跑车、SUV等不同类型。

为了方便理解,我们看一下产品等级和产品族的示意图:

在这里插入图片描述


2. 抽象工厂模式的结构

抽象工厂模式包含以下的角色:

  • 抽象工厂 : 提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • 具体工厂 :主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品 : 定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品 : 实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

3. 抽象工厂模式的实现

使用抽象工厂模式对上面的案例进行改造,类图如下:
在这里插入图片描述

 
具体的类设计如下:
对于手机抽象类、苹果手机类、小米手机类这几个类不需要修改,而客户端类、抽象工厂类需要修改,并且新增手表抽象类、苹果手表类、小米手表类、苹果工厂类、小米工厂类。

手表抽象类:

public abstract class Watch {

    public abstract String getName();
}

苹果手表类:

public class AppleWatch extends Watch {

    @Override
    public String getName() {
        return "苹果手表";
    }
}

小米手表类:

public class XiaomiWatch extends Watch {

    @Override
    public String getName() {
        return "小米手表";
    }
}

抽象工厂类:

public interface PhoneAndWatchFactory {

    //生产手机的功能
    Phone createPhone();

    //生产手表的功能
    Watch createWatch();
}

苹果工厂类:

public class AppleFactory implements PhoneAndWatchFactory {

    @Override
    public Phone createPhone() {
        return new ApplePhone();
    }

    @Override
    public Watch createWatch() {
        return new AppleWatch();
    }
}

小米工厂类:

public class XiaomiFactory implements PhoneAndWatchFactory {
    @Override
    public Phone createPhone() {
        return new XiaomiPhone();
    }

    @Override
    public Watch createWatch() {
        return new XiaomiWatch();
    }
}

客户端类:

public class Client {

    public static void main(String[] args) {

        //创建的是苹果工厂对象
        AppleFactory factory = new AppleFactory();

        //获取苹果手机和苹果手表
        Phone phone = factory.createPhone();
        Watch watch = factory.createWatch();

        System.out.println(phone.getName());
        System.out.println(watch.getName());

    }
}

4. 抽象工厂模式的优缺点

优点:

  • 抽象工厂模式能够确保创建的产品族是相互关联、一致的。这意味着通过抽象工厂创建的对象能够相互配合使用,形成一个完整的系统。
  • 由于客户端通过抽象接口使用产品,因此更换产品系列相对容易。只需要更改具体工厂的实例,而不需要修改客户端代码。
  • 新增一个产品族的实现,只需扩展抽象工厂和具体工厂,而不需要修改已有代码,符合开闭原则。

缺点:

  • 当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

七、工厂模式总结

简单工厂模式: 让一个工厂类负责创建所有对象;但没有考虑后期扩展和维护,修改违背开闭原则,静态方法不能被继承。

工厂方法模式: 主要思想是继承,修改符合开闭原则;但每个工厂只能创建一种类型的产品。

抽象工厂模式: 主要思想是组合,本质是产品族,实际包含了很多工厂方法,修改符合开闭原则;但只适用于增加同类工厂这种横向扩展需求,不适合新增功能方法这种纵向扩展。

其实这三种工厂模式在形式和特点上都非常相似,甚至存在一定的内在联系,而且最终目的都是解耦。在使用时,我们不必去在意这个模式到底工厂方法模式还是抽象工厂模式,因为它们之间也是可以灵活转变的。


 
非常感谢您阅读到这里,如果这篇文章对您有帮助,希望能留下您的点赞👍 关注💖 分享👥 留言💬thanks!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/259278.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

正向代理和反向代理的区别与联系

一、代理 代理就相当于中间商,本来A和B是可以直接连接的,但是此时添加了一个C在中间,A跟B不直接连接,而是通过C作为中介进行连接。最常见的例子就是二手东,其实很多我们租房子时签约的人不是房子的真正房东,而是房东委托的中介,房东不想管事或者房子太多,只靠自己无法进行管理,…

LVS负载均衡器(DR模式)+nginx七层代理+tomcat多实例+php+mysql 实现负载均衡以及动静分离、数据库的调用!!!

目录 前言 一、nfs共享存储,为两个节点服务器提供静态网页共享 二、nginx作为lvs的后端节点服务器,完成lo:0网卡配置,以及内核参数设置,还有设置路由表 步骤一:先完成nfs共享存储挂载 步骤二:完成lo:0网…

七:爬虫-数据解析之正则表达式

七:正则表达式概述 正则表达式,又称规则表达式,(Regular Expression,在代码中常简写为regex、regexp或RE),是一种文本模式,包括普通字符(例如,a 到 z 之间的字母&#xf…

Java解决不同路径问题2

Java解决不同路径问题2 01题目 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。 现在考虑网格中…

移动云捐赠三款开源项目,加速新一代基础软件生态繁荣

随着云计算、大数据、人工智能等新领域新信息技术的发展,我国基础软件的自主可控极大程度地影响着产业链上下游的多样性和技术创新的发展空间。移动云作为中国移动涉云业务的主入口,一直坚持共享开源价值,积极推动中国开源软件生态的繁荣发展…

AWS 知识一:如何在AWS上启动云AD服务器(详细到极致)

前言: 首先这里指的云AD服务器,只是为了让读友更好理解。云AD服务器在AWS中称为目录。AWS一共提供了4种目录类别,下面我将全程使用AWS托管微软AD这种目录类别进行示例。他完全提供了和Microsoft AD的功能,包括NTLM,Ker…

机器学习基础实验(使用 Pandas 进行数据探索)

介绍 本次实验通过分析电信运营商的客户离网率数据集来熟悉 Pandas 数据探索的常用方法,并构建一个预测客户离网率的简单模型。 知识点 排列索引交叉表透视表数据探索 课程介绍 机器学习开放基础课程是蓝桥云课经由 Open Machine Learning Course 授权并制作的…

Oracle定时任务的创建与禁用/删除

在开始操作之前,先从三W开始,即我常说的what 是什么;why 为什么使用;how 如何使用。 一、Oracle定时器是什么 Oracle定时器是一种用于在特定时间执行任务或存储过程的工具,可以根据需求设置不同的时间段和频率来执行…

el-form与el-upload结合上传带附件的表单数据(后端篇)

1.写在之前 本文采用Spring Boot MinIO MySQLMybatis Plus技术栈,参考ruoyi-vue-pro项目。 前端实现请看本篇文章el-form与el-upload结合上传带附件的表单数据(前端篇)-CSDN博客。 2.需求描述 在OA办公系统中,流程表单申请人…

无约束优化问题求解笔记(1)

目录 1. 迭代求解的基本流程与停止准则1.1 迭代求解的基本流程1.2 停止准则1.3 收敛阶 2. 线搜索方法2.1 精确线搜索2.2 非精确搜索**Goldstein 准则****Wolfe 准则** 2.3 线搜索算法的收敛性 1. 迭代求解的基本流程与停止准则 1.1 迭代求解的基本流程 优化问题的解通常无法直…

[总线仲裁]

目录 一. 集中仲裁方式1.1 链式查询方式1.2 计数器查询方式1.3 独立请求方式 二. 分布式仲裁方式 总线仲裁是为了解决多个设备争用总线这个问题 \quad 一. 集中仲裁方式 \quad 集中仲裁方式: 就像是霸道总裁来决定谁先获得总线控制权 分布仲裁方式: 商量着谁先获得总线控制权 …

【六大排序详解】开篇 :插入排序 与 希尔排序

插入排序 与 希尔排序 六大排序之二 插入排序 与 希尔排序1 排序1.1排序的概念 2 插入排序2.1 插入排序原理2.2 排序步骤2.3 代码实现 3 希尔排序3.1 希尔排序原理3.2 排序步骤3.3 代码实现 4 时间复杂度分析 Thanks♪(・ω・)ノ下一篇文章见&am…

基于ssm高校推免报名系统源码和论文

网络的广泛应用给生活带来了十分的便利。所以把高校推免报名管理与现在网络相结合,利用java技术建设高校推免报名管理系统,实现高校推免报名的信息化。则对于进一步提高高校推免报名管理发展,丰富高校推免报名管理经验能起到不少的促进作用。…

智能优化算法应用:基于蜜獾算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用:基于蜜獾算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于蜜獾算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.蜜獾算法4.实验参数设定5.算法结果6.参考文献7.MA…

2023-12-20 二叉搜索树的最近公共祖先和二叉搜索树中的插入操作和删除二叉搜索树中的节点

235. 二叉搜索树的最近公共祖先 思想:和二叉树的公共最近祖先节点的思路基本一致的!就是不用从下往上遍历处理!可以利用的二叉搜索树的特点从上往下处理了!而且最近公共节点肯定是第一个出现在【q,p】这个区间的内的&…

【已解决】vs2015操作创建声明定义由于以下原因无法完成

本博文解决这样的一个问题,就是vs2015下用qt,在快速创建槽函数时给笔者报了个错误,错误的完整说法是这样子的”操作创建声明/定义“由于下列原因无法完成,所选的文本不包含任何函数签名。第一次遇到这种花里胡哨的问题&#xff0c…

【数据结构】并查集的简单实现,合并,查找(C++)

文章目录 前言举例: 一、1.构造函数2.查找元素属于哪个集合FindRoot3.将两个集合归并成一个集合Union4.查找集合数量SetCount 二、源码 前言 需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规…

算法-滑动窗口类型

6666 滑动窗口 1、大小为K的最大和子数组 给定一个数组,找出该数组中所有大小为“K”的连续子数组的平均值。 让我们用实际输入来理解这个问题: Array: [1, 3, 2, 6, -1, 4, 1, 8, 2], K51、对于前5个数字(索引0-4的子数组),平均值为:(1 3 2 6−…

贝蒂快扫雷~(C语言)

✨✨欢迎大家来到贝蒂大讲堂✨✨ ​​​​🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:贝蒂的游戏 贝蒂的主页:Betty‘s blog 引言: 扫雷相信大家小时候到玩过吧,那…

Gin之GORM多表关联查询(多对多;自定义预加载SQL)

数据库三个,如下: 注意:配置中间表的时候,表设计层面最好和配置的其他两张表契合,例如其他两张表为fate内的master和slave;要整合其对应关系的话,设计中间表的结构为master_id和slave_id最好(不然会涉及重写外键的操作) 重写外键(介绍) 对于 many2many 关系,连接表…