元组在Java中的最佳实践

引言

在软件开发过程中,我们经常需要处理多个相关联的数据项。传统的做法是创建一个专门的类来封装这些数据,或者使用数组、集合等数据结构。然而,这些方法往往需要编写大量的样板代码,或者牺牲类型安全性。元组(Tuple)作为一种轻量级的数据结构,为这类问题提供了一种优雅的解决方案。

本文将深入探讨元组的概念、特点、适用场景,以及如何在 Java 中通过 Apache Commons 工具包来应用元组,帮助开发者在实际项目中更高效地使用这一工具。

什么是元组?

元组是一种将多个不同类型的元素组合在一起的数据结构。它源自数学中的有序集合概念,在计算机科学中被广泛应用于各种编程语言中。元组中的每个位置可以存储不同类型的数据,并且可以通过位置或名称来访问这些数据。

与列表或数组不同,元组通常具有固定的长度,并且可以包含不同类型的元素。最常见的元组类型包括:

  • 二元组(Pair):包含两个元素,通常称为左值(left)和右值(right)
  • 三元组(Triple):包含三个元素,通常称为左值(left)、中间值(middle)和右值(right)
  • 多元组:包含更多元素的元组

在函数式编程语言(如 Scala、Haskell)和动态语言(如 Python)中,元组是内置的数据类型。然而,Java 作为一种面向对象的静态类型语言,并没有内置的元组类型。幸运的是,Apache Commons Lang 库提供了元组的实现,使 Java 开发者也能够享受到元组带来的便利。

元组的特点

元组作为一种数据结构,具有以下几个显著特点:

1. 轻量级

元组是一种轻量级的数据结构,不需要定义专门的类就可以将多个不同类型的数据组合在一起。这使得元组特别适合于临时性的数据组合,或者在不想创建专门类的情况下返回多个值。

2. 类型安全

与使用 Object 数组或集合相比,元组提供了类型安全的方式来处理多个值。在 Java 中,通过泛型机制,元组可以在编译时保证类型安全,避免了运行时的类型转换错误。

3. 不可变性(可选)

许多元组实现(包括 Apache Commons 中的 ImmutablePair 和 ImmutableTriple)提供了不可变的版本,一旦创建就不能修改其元素值。这种不可变性有助于创建线程安全的代码,并简化了对象的生命周期管理。

4. 可读性

元组通过提供有意义的方法名(如 getLeft()、getRight())来访问元素,提高了代码的可读性。相比于使用数组的索引访问(如 array[0]、array[1]),元组的方法名更能表达元素的含义。

5. 实现了常用接口

在 Apache Commons 的实现中,Pair 类实现了 Map.Entry 接口,这使得它可以与 Java 集合框架无缝集成,特别是在处理 Map 的键值对时。

Java 中元组的适用场景

虽然元组在 Java 中不是内置类型,但在许多场景下,元组可以显著简化代码并提高可读性。以下是一些适合使用元组的典型场景:

1. 多值返回

Java 方法默认只能返回一个值。当需要从方法中返回多个值时,传统的做法是创建一个专门的类来封装这些值,或者使用数组、集合等数据结构。元组提供了一种更简洁的方式来实现多值返回。

例如,当需要从一个方法中同时返回最小值和最大值时,可以使用二元组:

java 复制代码
public Pair<Integer, Integer> findMinAndMax(int[] numbers) {
    // 实现查找最小值和最大值的逻辑
    return Pair.of(min, max);
}

2. 数据分页

在实现分页功能时,通常需要同时返回当前页的数据列表、总记录数和总页数。使用三元组可以优雅地实现这一需求:

java 复制代码
public Triple<List<User>, Integer, Integer> getUserPage(int pageNum, int pageSize) {
    // 实现分页查询逻辑
    return Triple.of(pageData, totalCount, totalPages);
}

3. 数据库查询结果处理

在处理数据库查询结果时,特别是涉及到多表关联查询的场景,元组可以用来组织和处理查询结果。

例如,当需要查询订单及其关联的客户信息时:

java 复制代码
public List<Pair<Order, Customer>> getOrdersWithCustomers() {
    // 实现查询逻辑
    return orderCustomerPairs;
}

4. 事件处理

在事件驱动的系统中,元组可以用来表示事件类型和事件数据的组合:

java 复制代码
public void processEvents(List<Pair<EventType, String>> events) {
    // 实现事件处理逻辑
}

5. 临时数据组合

在某些情况下,需要临时组合一些数据进行处理,但又不想为此创建专门的类。元组提供了一种简便的方式来实现这一需求。

6. 函数式编程

在使用 Java 8 及以上版本的函数式编程特性时,元组可以与 Stream API 结合使用,简化数据处理流程。

Apache Commons 中的元组实现

Apache Commons Lang 库提供了元组(Tuple)的实现,位于org.apache.commons.lang3.tuple包下。这个包中的类提供了一种便捷的方式来处理成对或三元组的数据。

Maven 依赖配置

要在 Java 项目中使用 Apache Commons Lang 提供的元组实现,需要添加以下 Maven 依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

如果使用 Gradle,可以添加以下依赖:

groovy 复制代码
implementation 'org.apache.commons:commons-lang3:3.12.0'

添加依赖后,就可以导入org.apache.commons.lang3.tuple包中的类,开始使用元组了。

元组的类层次结构

Apache Commons Lang 中的元组从两个维度进行分类:

  1. 是否可变

    • 可变元组:元素值可以修改
    • 不可变元组:元素值不能修改(通过 final 修饰属性实现)
  2. 元素数量

    • 二元组(Pair):包含两个元素,左值和右值
    • 三元组(Triple):包含三个元素,左值、中间值和右值

这两个维度组合起来,形成了四种具体的元组类型:

  • 可变二元组(MutablePair)
  • 不可变二元组(ImmutablePair)
  • 可变三元组(MutableTriple)
  • 不可变三元组(ImmutableTriple)

Pair 类

Pair是一个抽象类,它实现了Map.Entry接口,这意味着它可以被视为一个键值对。

类签名

java 复制代码
public abstract class Pair<L, R> implements Map.Entry<L, R>, Comparable<Pair<L, R>>, Serializable

主要方法

  1. 获取元素的方法

    java 复制代码
    public abstract L getLeft();
    public abstract R getRight();
  2. Map.Entry 接口方法

    java 复制代码
    @Override
    public final L getKey() {
        return getLeft();
    }
    
    @Override
    public R getValue() {
        return getRight();
    }
  3. 静态工厂方法

    java 复制代码
    public static <L, R> Pair<L, R> of(final L left, final R right) {
        return ImmutablePair.of(left, right);
    }

ImmutablePair 类

ImmutablePairPair的一个具体实现,它的特点是一旦创建,其元素值就不能被修改。

类签名

java 复制代码
public class ImmutablePair<L, R> extends Pair<L, R>

主要特性

  1. 不可变的元素

    java 复制代码
    public final L left;
    public final R right;
  2. 不支持 setValue

    java 复制代码
    @Override
    public R setValue(final R value) {
        throw new UnsupportedOperationException();
    }
  3. 静态工厂方法

    java 复制代码
    public static <L, R> ImmutablePair<L, R> of(final L left, final R right) {
        return new ImmutablePair<>(left, right);
    }

MutablePair 类

MutablePairPair的另一个具体实现,它允许修改元素值。

类签名

java 复制代码
public class MutablePair<L, R> extends Pair<L, R>

主要特性

  1. 可变的元素

    java 复制代码
    public L left;
    public R right;
  2. 支持 setValue

    java 复制代码
    @Override
    public R setValue(final R value) {
        final R result = getRight();
        setRight(value);
        return result;
    }
  3. 提供 setter 方法

    java 复制代码
    public void setLeft(final L left) {
        this.left = left;
    }
    
    public void setRight(final R right) {
        this.right = right;
    }
  4. 静态工厂方法

    java 复制代码
    public static <L, R> MutablePair<L, R> of(final L left, final R right) {
        return new MutablePair<>(left, right);
    }

Triple 类

Triple是一个抽象类,它表示一个包含三个元素的元组。

类签名

java 复制代码
public abstract class Triple<L, M, R> implements Comparable<Triple<L, M, R>>, Serializable

主要方法

  1. 获取元素的方法

    java 复制代码
    public abstract L getLeft();
    public abstract M getMiddle();
    public abstract R getRight();
  2. 静态工厂方法

    java 复制代码
    public static <L, M, R> Triple<L, M, R> of(final L left, final M middle, final R right) {
        return ImmutableTriple.of(left, middle, right);
    }

ImmutableTriple 类和 MutableTriple 类

这两个类是Triple的具体实现,分别提供了不可变和可变的三元组。它们的实现方式与ImmutablePairMutablePair类似,只是增加了对中间值(middle)的处理。

元组在 Java 中的实际应用示例

下面通过几个实际的代码示例,展示元组在 Java 中的应用。

示例 1:多值返回

在这个示例中,我们使用元组来从方法中返回多个值,避免了创建专门的 DTO 类或使用集合等其他方式的复杂性。

java 复制代码
package com.example.tuple;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

/**
 * 示例1:使用元组进行多值返回
 */
public class MultiValueReturnExample {

    public static void main(String[] args) {
        // 调用返回最大值和最小值的方法
        Pair<Integer, Integer> minMax = findMinAndMax(new int[]{5, 2, 9, 1, 7, 3, 8});

        System.out.println("最小值: " + minMax.getLeft());
        System.out.println("最大值: " + minMax.getRight());

        // 调用返回用户名和年龄的方法
        Pair<String, Integer> userInfo = getUserInfo(1001);

        System.out.println("用户名: " + userInfo.getLeft());
        System.out.println("年龄: " + userInfo.getRight());
    }

    /**
     * 查找数组中的最小值和最大值
     */
    public static Pair<Integer, Integer> findMinAndMax(int[] numbers) {
        if (numbers == null || numbers.length == 0) {
            throw new IllegalArgumentException("数组不能为空");
        }

        int min = numbers[0];
        int max = numbers[0];

        for (int i = 1; i < numbers.length; i++) {
            if (numbers[i] < min) {
                min = numbers[i];
            }
            if (numbers[i] > max) {
                max = numbers[i];
            }
        }

        // 使用ImmutablePair创建不可变的元组
        return ImmutablePair.of(min, max);
    }

    /**
     * 模拟从数据库获取用户信息
     */
    public static Pair<String, Integer> getUserInfo(int userId) {
        // 在实际应用中,这里会查询数据库
        // 这里仅作为示例,返回模拟数据
        if (userId == 1001) {
            return ImmutablePair.of("张三", 28);
        } else if (userId == 1002) {
            return ImmutablePair.of("李四", 32);
        } else {
            return ImmutablePair.of("未知用户", 0);
        }
    }
}

在这个示例中,我们使用ImmutablePair来返回数组的最小值和最大值,以及用户的姓名和年龄。这种方式比创建专门的类更加简洁,同时又保持了类型安全性。

示例 2:分页数据处理

在这个示例中,我们使用三元组来处理分页数据,同时返回当前页数据、总记录数和总页数。

java 复制代码
package com.example.tuple;

import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;

import java.util.ArrayList;
import java.util.List;

/**
 * 示例2:使用元组进行分页数据处理
 */
public class PaginationExample {

    public static void main(String[] args) {
        // 模拟从数据库获取分页数据
        Triple<List<User>, Integer, Integer> pageResult = getUserPage(2, 3);

        List<User> users = pageResult.getLeft();
        int totalCount = pageResult.getMiddle();
        int totalPages = pageResult.getRight();

        System.out.println("当前页数据:");
        for (User user : users) {
            System.out.println("  - " + user.getName() + " (ID: " + user.getId() + ", 年龄: " + user.getAge() + ")");
        }

        System.out.println("总记录数: " + totalCount);
        System.out.println("总页数: " + totalPages);
    }

    /**
     * 模拟从数据库获取分页用户数据
     */
    public static Triple<List<User>, Integer, Integer> getUserPage(int pageNum, int pageSize) {
        // 模拟数据库中的所有用户
        List<User> allUsers = getAllUsers();

        // 计算总记录数
        int totalCount = allUsers.size();

        // 计算总页数
        int totalPages = (int) Math.ceil((double) totalCount / pageSize);

        // 确保页码有效
        if (pageNum < 1) {
            pageNum = 1;
        }
        if (pageNum > totalPages) {
            pageNum = totalPages;
        }

        // 计算当前页的起始和结束索引
        int startIndex = (pageNum - 1) * pageSize;
        int endIndex = Math.min(startIndex + pageSize, totalCount);

        // 获取当前页的数据
        List<User> pageUsers = new ArrayList<>();
        for (int i = startIndex; i < endIndex; i++) {
            pageUsers.add(allUsers.get(i));
        }

        // 返回包含当前页数据、总记录数和总页数的三元组
        return ImmutableTriple.of(pageUsers, totalCount, totalPages);
    }

    // 省略User类和getAllUsers方法的实现...
}

在这个示例中,我们使用ImmutableTriple来返回分页查询的结果,包括当前页的用户列表、总记录数和总页数。这种方式比创建专门的分页结果类更加简洁,特别是在只需要临时使用分页结果的场景下。

示例 3:数据库查询结果处理

在这个示例中,我们使用元组来处理数据库查询结果,特别是在需要同时处理多个相关数据时的应用。

java 复制代码
package com.example.tuple;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 示例3:使用元组处理数据库查询结果
 */
public class DatabaseQueryExample {

    public static void main(String[] args) {
        // 模拟获取订单及其关联客户信息
        List<Pair<Order, Customer>> orderWithCustomers = getOrdersWithCustomers();

        System.out.println("订单及客户信息:");
        for (Pair<Order, Customer> pair : orderWithCustomers) {
            Order order = pair.getLeft();
            Customer customer = pair.getRight();

            System.out.println("订单ID: " + order.getId() +
                               ", 金额: " + order.getAmount() +
                               ", 客户: " + customer.getName() +
                               ", 联系方式: " + customer.getContact());
        }

        // 模拟统计每个客户的订单总金额
        Map<Customer, Double> customerTotals = calculateCustomerTotals(orderWithCustomers);

        System.out.println("\n客户订单总金额:");
        for (Map.Entry<Customer, Double> entry : customerTotals.entrySet()) {
            Customer customer = entry.getKey();
            Double total = entry.getValue();

            System.out.println(customer.getName() + ": " + total);
        }

        // 使用可变元组跟踪处理状态
        MutablePair<Integer, String> processStatus = processOrders(orderWithCustomers);
        System.out.println("\n处理状态: " + processStatus.getRight() +
                           ", 成功处理: " + processStatus.getLeft() + "/" + orderWithCustomers.size());
    }

    /**
     * 模拟从数据库获取订单及其关联的客户信息
     */
    public static List<Pair<Order, Customer>> getOrdersWithCustomers() {
        // 在实际应用中,这里会查询数据库
        // 这里仅作为示例,返回模拟数据
        List<Pair<Order, Customer>> result = new ArrayList<>();

        Customer customer1 = new Customer(101, "张三", "13800138001");
        Customer customer2 = new Customer(102, "李四", "13900139002");

        result.add(ImmutablePair.of(new Order(1001, 299.99, "已付款"), customer1));
        result.add(ImmutablePair.of(new Order(1002, 599.50, "已发货"), customer1));
        result.add(ImmutablePair.of(new Order(1003, 1299.00, "已付款"), customer2));
        result.add(ImmutablePair.of(new Order(1004, 499.99, "待付款"), customer2));

        return result;
    }

    /**
     * 计算每个客户的订单总金额
     */
    public static Map<Customer, Double> calculateCustomerTotals(List<Pair<Order, Customer>> orderWithCustomers) {
        Map<Customer, Double> result = new HashMap<>();

        for (Pair<Order, Customer> pair : orderWithCustomers) {
            Order order = pair.getLeft();
            Customer customer = pair.getRight();

            // 只计算已付款和已发货的订单
            if ("已付款".equals(order.getStatus()) || "已发货".equals(order.getStatus())) {
                Double currentTotal = result.getOrDefault(customer, 0.0);
                result.put(customer, currentTotal + order.getAmount());
            }
        }

        return result;
    }

    /**
     * 模拟处理订单
     */
    public static MutablePair<Integer, String> processOrders(List<Pair<Order, Customer>> orderWithCustomers) {
        // 创建可变元组来跟踪处理状态
        MutablePair<Integer, String> status = MutablePair.of(0, "处理中");

        // 模拟处理订单
        for (Pair<Order, Customer> pair : orderWithCustomers) {
            Order order = pair.getLeft();

            // 模拟只处理已付款的订单
            if ("已付款".equals(order.getStatus())) {
                // 模拟处理逻辑
                System.out.println("处理订单: " + order.getId());

                // 更新处理计数
                status.setLeft(status.getLeft() + 1);
            }
        }

        // 更新处理状态
        if (status.getLeft() == 0) {
            status.setRight("无订单处理");
        } else if (status.getLeft() == orderWithCustomers.size()) {
            status.setRight("全部处理完成");
        } else {
            status.setRight("部分处理完成");
        }

        return status;
    }

    // 省略Order类和Customer类的实现...
}

在这个示例中,我们使用元组来处理订单和客户的关联数据,以及使用可变元组来跟踪订单处理的状态。这种方式比创建专门的关联类更加灵活,特别是在处理临时的数据关联时。

示例 4:事件处理和数据转换

在这个示例中,我们展示了元组在事件处理和数据转换场景中的应用。

java 复制代码
package com.example.tuple;

import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 示例4:使用元组进行事件处理和数据转换
 */
public class EventProcessingExample {

    public static void main(String[] args) {
        // 1. 事件处理示例
        System.out.println("===== 事件处理示例 =====");
        List<Pair<EventType, String>> events = generateEvents();
        processEvents(events);

        // 2. 数据转换示例
        System.out.println("\n===== 数据转换示例 =====");
        String[] rawData = {"10", "20", "30", "40", "50"};
        Triple<String[], String[], Double[]> convertedData = convertData(rawData);

        System.out.println("原始数据:");
        printArray(convertedData.getLeft());

        System.out.println("转换后的字符串数据:");
        printArray(convertedData.getMiddle());

        System.out.println("转换后的数值数据:");
        printArray(convertedData.getRight());

        // 3. 测试数据生成示例
        System.out.println("\n===== 测试数据生成示例 =====");
        List<Pair<User, List<Order>>> testData = createTestData();

        for (Pair<User, List<Order>> userData : testData) {
            User user = userData.getLeft();
            List<Order> orders = userData.getRight();

            System.out.println("用户: " + user.getName() + " (ID: " + user.getId() + ")");
            System.out.println("订单数量: " + orders.size());

            for (Order order : orders) {
                System.out.println("  - 订单ID: " + order.getId() + ", 金额: " + order.getAmount());
            }
            System.out.println();
        }
    }

    // 省略其他方法和类的实现...
}

在这个示例中,我们使用元组来处理事件数据、进行数据格式转换,以及生成测试数据。这些场景展示了元组在处理临时数据组合时的灵活性和便利性。

元组使用的最佳实践

在 Java 中使用元组时,以下是一些最佳实践,可以帮助你更有效地使用这一工具:

1. 选择合适的元组类型

  • 对于不需要修改的数据,优先使用不可变元组(ImmutablePair、ImmutableTriple)
  • 对于需要在处理过程中修改的数据,使用可变元组(MutablePair、MutableTriple)
  • 对于超过三个元素的数据,考虑创建专门的类,而不是使用元组

2. 明确元素的含义

  • 在方法签名中使用有意义的泛型参数名称,如Pair<UserName, Age>而不是Pair<String, Integer>
  • 在方法文档中明确说明元组各个位置元素的含义
  • 在使用元组的代码中,通过变量名或注释说明元素的含义

3. 适度使用元组

  • 元组适合临时的数据组合和简单的多值返回
  • 对于复杂的数据结构或业务实体,应该创建专门的类
  • 如果元组在代码中被广泛传递或者具有明确的业务含义,考虑创建专门的类

4. 利用 Map.Entry 接口

  • Pair 类实现了 Map.Entry 接口,可以直接用于 Map 的操作
  • 可以利用这一特性简化处理 Map 的代码

5. 与 Java 8 Stream API 结合使用

  • 元组可以与 Stream API 结合使用,简化数据处理流程
  • 例如,可以使用元组来存储中间计算结果,或者进行分组操作

6. 注意性能影响

  • 元组会创建额外的对象,在性能敏感的场景下需要谨慎使用
  • 在循环或高频调用的代码中,可能需要权衡使用元组和直接使用基本类型数组的性能差异

元组与其他替代方案的比较

在 Java 中,除了使用元组,还有其他几种方式可以实现类似的功能。下面是元组与这些替代方案的比较:

1. 元组 vs 自定义类

元组优势

  • 无需编写额外的类定义
  • 适合临时使用和快速开发
  • Apache Commons 提供了现成的实现

自定义类优势

  • 字段名更有意义
  • 可以添加业务逻辑和验证
  • 更好的代码可读性和可维护性
  • 可以实现更多自定义功能

适用场景

  • 对于临时的数据组合或简单的多值返回,使用元组
  • 对于具有明确业务含义或需要在多处使用的数据结构,创建自定义类

2. 元组 vs 数组

元组优势

  • 类型安全,不同位置可以有不同类型
  • 有意义的访问方法(getLeft(), getRight())
  • 可以是不可变的

数组优势

  • 性能更好,特别是基本类型数组
  • 可以存储任意数量的元素
  • 语言内置支持

适用场景

  • 对于不同类型的少量元素组合,使用元组
  • 对于同类型的多个元素,或性能敏感的场景,使用数组

3. 元组 vs 集合

元组优势

  • 固定长度,类型安全
  • 不同位置可以有不同类型
  • 可以是不可变的

集合优势

  • 可以存储任意数量的元素
  • 提供丰富的操作方法
  • 更灵活的数据结构

适用场景

  • 对于固定数量、不同类型的元素组合,使用元组
  • 对于可变数量、同类型的元素集合,使用集合类

结论

元组作为一种轻量级的数据结构,为 Java 开发者提供了一种简洁、类型安全的方式来处理多个相关联的数据项。通过 Apache Commons Lang 库提供的实现,Java 开发者可以在多值返回、数据分页、数据库查询结果处理等场景中使用元组,简化代码并提高可读性。

在使用元组时,需要根据具体场景选择合适的元组类型,明确元素的含义,并适度使用元组。对于复杂的数据结构或具有明确业务含义的数据,应该考虑创建专门的类,而不是过度依赖元组。

总的来说,元组是 Java 开发者工具箱中的一个有用工具,合理使用可以提高开发效率和代码质量。

参考资料

  1. Apache Commons Lang 官方文档:commons.apache.org/proper/comm...
  2. Apache Commons Lang 源码:github.com/apache/comm...
相关推荐
大刀爱敲代码1 小时前
基础算法01——二分查找(Binary Search)
java·算法
声声codeGrandMaster2 小时前
Django项目入门
后端·mysql·django
千里码aicood3 小时前
【2025】基于springboot+vue的医院在线问诊系统设计与实现(源码、万字文档、图文修改、调试答疑)
vue.js·spring boot·后端
追风少年1553 小时前
常见中间件漏洞之一 ----【Tomcat】
java·中间件·tomcat
yang_love10114 小时前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
Pandaconda4 小时前
【后端开发面试题】每日 3 题(二十)
开发语言·分布式·后端·面试·消息队列·熔断·服务限流
郑州吴彦祖7724 小时前
【Java】UDP网络编程:无连接通信到Socket实战
java·网络·udp
spencer_tseng5 小时前
eclipse [jvm memory monitor] SHOW_MEMORY_MONITOR=true
java·jvm·eclipse
鱼樱前端5 小时前
mysql事务、行锁、jdbc事务、数据库连接池
java·后端
Hanson Huang5 小时前
23种设计模式-外观(Facade)设计模式
java·设计模式·外观模式·结构型设计模式