java与模式实战方法:提示词、素材与细节优化

java与模式实战方法:提示词、素材与细节优化

java与模式实战方法:提示词、素材与细节优化

在 Java 项目里使用设计模式,难点通常不是记住“工厂”“策略”“装饰器”这些名字,而是判断问题是否真的需要模式,如何把模式放进现有代码,以及怎样验证改动没有引入新的复杂度。使用 AI 辅助开发后,难点又多了一层:怎样提供足够准确的上下文,让生成的代码符合项目约定,而不是只在孤立示例里看起来合理。

本文把这几件事放在同一个工作流程里:先识别变化点,再选择模式;用结构化提示词约束 AI;准备可复用的项目素材;通过一个可运行的 Java 示例落地策略模式;最后检查边界、参数和安全问题。示例面向 Java 8 及以上版本,不依赖第三方库。

一、先明确“模式”解决什么问题

设计模式是常见问题的组织方式,不是必须完成的清单。一个类里出现多个 if,不意味着一定要套模式;反过来,如果业务规则会持续增加,所有分支都挤在一个方法里,简单的条件判断也可能很快变成维护负担。

开始改造前,可以先用下面几个问题描述现状:

  • 什么在变化?例如折扣计算规则、支付渠道、通知方式或文件导出格式。
  • 谁需要知道变化?如果调用方必须了解每种规则的内部细节,说明边界可能划分得不合适。
  • 变化的频率和规模如何?只有一个长期稳定的分支,未必值得抽象;规则不断增加、经常调整,抽象的价值就更高。
  • 新方案会减少什么成本?它是否降低了修改影响范围,还是仅仅增加接口、工厂和类的数量?

模式的实用目标是把容易变化的部分隔离开,并让依赖关系更清晰。若一个模式不能让修改更集中、测试更容易或调用方更简单,就应考虑更朴素的实现。

二、用提示词把需求变成可检查的任务

向 AI 提问时,单说“帮我用设计模式重构 Java 代码”通常不够。模型不知道项目使用的 Java 版本、现有接口、异常约定和兼容要求,容易自行补设定。更可靠的提示词要交代背景、目标、约束、输出形式和验收条件。

一个可复用的提示词模板

你是一名熟悉 Java 项目维护的工程师。
请基于下面的代码和需求给出重构建议。

项目环境:
- Java 版本:[例如 Java 8]
- 是否允许新增依赖:[是/否]
- 现有异常和日志约定:[填写实际约定]
- 必须保持兼容的公开接口:[填写接口]

当前问题:
[描述现状,以及正在发生的修改或维护问题]

目标:
[描述期望行为,不要只写模式名称]

约束:
- 不改变:[列出不能改变的行为]
- 参数边界:[列出 null、负数、空字符串等处理规则]
- 线程安全要求:[填写已知要求;未知时请说明未知]
- 不要引入:[列出不允许的依赖、静态状态或其他实现]

请按以下结构回答:
1. 判断是否需要设计模式,并说明理由。
2. 给出适用的模式及不适用的情况。
3. 提供完整、可编译的 Java 示例,并写明文件名和运行方式。
4. 说明输入校验、异常、并发和安全边界。
5. 列出需要人工确认的假设,不要把未知业务规则当成事实。

这个模板有两个重要作用。第一,它要求模型解释“为什么选这个模式”,减少为了套模式而套模式的情况。第二,它要求模型把未知信息标出来,而不是擅自决定。例如“优惠金额能否小于零”是业务规则,不能单凭 Java 语法推断。

把大任务拆成几轮

对真实代码库,不建议第一轮就要求 AI 生成完整重构。可以按风险递增的顺序推进:

  1. 请模型只识别变化点、依赖关系和可能的模式,不改代码。
  2. 确认业务规则后,让模型设计最小接口和类关系。
  3. 让模型只改指定文件,并列出新增、删除和行为变化。
  4. 要求补充边界测试,再由开发者本地编译、运行和审查差异。

分步工作的价值不在于模型一定更准确,而在于更容易发现错误假设,也便于把影响限制在明确范围内。提示词不会自动赋予模型对整个仓库的了解;如果工具没有读取某些文件,就应把必要内容明确提供。

三、准备好可复用的“素材”

这里的素材不是图片,而是能帮助人和 AI 快速理解项目的技术资料。素材过少,回答容易脱离项目;素材过多,则会引入无关内容并增加审查成本。应优先提供与本次改动直接相关的信息。

常用素材清单

  • 代码素材:相关接口、调用方、实现类和测试代码。不要只给一个方法,却省略它的调用方式与约束。
  • 业务素材:规则说明、输入样例、边界样例,以及冲突时以哪条规则为准。
  • 项目约定:Java 版本、包结构、命名方式、异常处理、日志规范和依赖限制。
  • 运行素材:编译命令、测试命令、代表性日志和已知失败现象。敏感数据应先脱敏。
  • 验收素材:期望输出、兼容要求、性能目标及不在本次范围内的事项。

例如,不要只写“增加 VIP 折扣”,而应说明折扣适用于哪些商品、是否可与优惠券叠加、金额的币种和精度、优惠能否超过订单金额,以及规则冲突时的优先级。对这些细节没有共识时,正确做法是标记为待确认,而不是让代码或模型自行猜测。

为 AI 准备项目上下文时,还要遵循数据最小化原则。不要提交生产密钥、访问令牌、真实个人信息或不必要的客户数据。把配置替换成占位值,把日志中的账号、地址和标识符脱敏。使用某个 AI 工具时,数据会如何处理,应以该工具的实际服务条款和组织政策为准;不能假设所有工具都默认不会保存或处理输入内容。

四、实战:用策略模式组织折扣规则

假设订单折扣会持续增加。最初只有普通订单,后来加入比例折扣和固定金额减免。如果规则少且稳定,一个简单分支足够;如果不同规则需要独立测试、持续新增或由不同模块维护,就可以把每种算法封装为策略。

下面的示例遵守几个明确的业务假设:金额必须非空且非负;金额以两位小数进行折扣计算;百分比折扣范围为 0 到 1,表示减免比例;固定减免不能超过订单金额。若真实业务采用不同币种精度、税前或税后口径,应先替换这些假设,而不是直接照搬示例。

将以下代码保存为 DiscountStrategyDemo.java,使用 JDK 8 或更高版本编译运行:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class DiscountStrategyDemo {

    interface DiscountStrategy {
        BigDecimal discountFor(BigDecimal amount);
    }

    static final class NoDiscount implements DiscountStrategy {
        @Override
        public BigDecimal discountFor(BigDecimal amount) {
            return checkedAmount(amount).setScale(2, RoundingMode.HALF_UP);
        }
    }

    static final class PercentageDiscount implements DiscountStrategy {
        private final BigDecimal rate;

        PercentageDiscount(BigDecimal rate) {
            if (rate == null
                    || rate.compareTo(BigDecimal.ZERO) < 0
                    || rate.compareTo(BigDecimal.ONE) > 0) {
                throw new IllegalArgumentException(
                        "rate must be between 0 and 1");
            }
            this.rate = rate;
        }

        @Override
        public BigDecimal discountFor(BigDecimal amount) {
            BigDecimal checked = checkedAmount(amount);
            return checked.multiply(rate)
                    .setScale(2, RoundingMode.HALF_UP);
        }
    }

    static final class FixedDiscount implements DiscountStrategy {
        private final BigDecimal deduction;

        FixedDiscount(BigDecimal deduction) {
            if (deduction == null
                    || deduction.compareTo(BigDecimal.ZERO) < 0) {
                throw new IllegalArgumentException(
                        "deduction must be non-negative");
            }
            this.deduction = deduction;
        }

        @Override
        public BigDecimal discountFor(BigDecimal amount) {
            BigDecimal checked = checkedAmount(amount);
            BigDecimal capped = deduction.min(checked);
            return capped.setScale(2, RoundingMode.HALF_UP);
        }
    }

    enum DiscountType {
        NONE, PERCENTAGE, FIXED
    }

    static final class DiscountFactory {
        static DiscountStrategy create(
                DiscountType type, BigDecimal parameter) {
            if (type == null) {
                throw new IllegalArgumentException(
                        "discount type must not be null");
            }

            switch (type) {
                case NONE:
                    return new NoDiscount();
                case PERCENTAGE:
                    return new PercentageDiscount(parameter);
                case FIXED:
                    return new FixedDiscount(parameter);
                default:
                    throw new IllegalArgumentException(
                            "unsupported discount type: " + type);
            }
        }
    }

    static final class CheckoutService {
        BigDecimal payable(
                BigDecimal amount, DiscountStrategy strategy) {
            BigDecimal checked = checkedAmount(amount);
            if (strategy == null) {
                throw new IllegalArgumentException(
                        "strategy must not be null");
            }

            BigDecimal discount = strategy.discountFor(checked);
            if (discount == null
                    || discount.compareTo(BigDecimal.ZERO) < 0
                    || discount.compareTo(checked) > 0) {
                throw new IllegalStateException(
                        "strategy returned an invalid discount");
            }

            return checked.subtract(discount)
                    .setScale(2, RoundingMode.HALF_UP);
        }
    }

    private static BigDecimal checkedAmount(BigDecimal amount) {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException(
                    "amount must be non-null and non-negative");
        }
        return amount;
    }

    public static void main(String[] args) {
        CheckoutService checkout = new CheckoutService();
        BigDecimal amount = new BigDecimal("100.00");

        DiscountStrategy percentage = DiscountFactory.create(
                DiscountType.PERCENTAGE, new BigDecimal("0.15"));
        DiscountStrategy fixed = DiscountFactory.create(
                DiscountType.FIXED, new BigDecimal("120.00"));

        System.out.println(checkout.payable(
                amount, new NoDiscount()));
        System.out.println(checkout.payable(amount, percentage));
        System.out.println(checkout.payable(amount, fixed));
    }
}

预期输出为:

100.00
85.00
0.00

代码中的模式分工

  • DiscountStrategy 定义统一策略接口,结算服务不必知道每种折扣的计算过程。
  • NoDiscount、PercentageDiscount 和 FixedDiscount 分别封装不同算法。
  • DiscountFactory 根据类型创建策略,将选择逻辑与结算过程分开。
  • CheckoutService 通过接口调用策略,并检查策略返回值,避免把所有算法和业务流程写在一个方法里。

这里使用 BigDecimal 而不是 double,是因为金额涉及十进制精度。通过字符串构造 BigDecimal,可以避免先经过二进制浮点数再转换所带来的表示误差。示例把折扣结果按两位小数、四舍五入处理,但“折扣在哪一步舍入”属于业务约定,不应一概认为适用于所有结算系统。

percentage 的参数限制是显式的:它不能为 null,并且必须在 0 和 1 之间。该范围来自本示例对参数的定义,表示减免比例,并不是 Java 或 BigDecimal 对参数施加的通用限制。若业务采用“百分数 15 表示 15%”的输入格式,就应改为 0 到 100,并在命名和接口文档中写清楚。

五、如何验证这段实现

可运行的 main 只能证明几个正常路径可以执行,不等于覆盖了所有边界。实际项目应使用测试框架或现有测试体系验证至少以下情况:

  • 普通策略不改变金额。
  • 比例为 0、1 时结果符合规则;小于 0 或大于 1 时构造策略失败。
  • 固定减免为 0、等于金额或大于金额时,结果符合封顶规则。
  • 金额为 null 或负数时,按约定抛出参数异常。
  • 策略返回负值、超过订单金额或返回 null 时,结算服务拒绝该结果。
  • 临界金额的舍入结果符合产品和财务确认过的规则。

示例用 IllegalArgumentException 表示调用参数不合法,用 IllegalStateException 表示策略返回违反结算约定的数据。项目如果有统一的领域异常体系,应采用项目约定,并确保调用方知道哪些异常可恢复、哪些需要上报。

在生产代码中,最好不要仅凭字符串配置随意创建策略。若折扣类型来自数据库或外部请求,应先校验允许的类型,再将经过验证的配置转换为策略。对于无法识别的类型,应明确报错或走经过产品确认的默认行为,避免静默地给用户错误价格。

六、策略模式的适用边界与常见错误

适用的情况

  • 同一业务流程中存在多种可替换算法,调用方希望通过相同接口使用它们。
  • 算法会独立变化,需要单独测试或由不同实现承载。
  • 新增一种算法不应迫使调用方重写整段业务流程。

不一定适用的情况

  • 规则只有一两条且长期稳定,普通条件判断更清楚。
  • 差异只是简单常量,没有独立行为,提取出许多类反而难以导航。
  • 真正变化的是整个对象创建过程,或需要在对象外层动态叠加职责。此时工厂、装饰器等其他结构可能更合适,但仍要按实际问题判断。

常见实现错误

  1. 只把分支搬进工厂。如果所有算法仍写在同一个巨大工厂方法里,维护问题只是换了位置。工厂通常负责创建对象,不应承担各策略的业务计算。
  2. 过度使用枚举。示例使用枚举是为了让类型明确、便于展示;若规则由插件动态注册,固定枚举可能不够灵活。应根据真实扩展方式选择注册表或依赖注入,而不是把示例结构机械推广。
  3. 让策略依赖过多隐式状态。如果策略读取可变全局变量、当前登录用户或环境状态,测试会更难,结果也可能随调用时机变化。必要依赖应通过构造器或明确参数传入。
  4. 忽略金额和舍入规则。折扣先舍入还是最后统一舍入,币种精度是多少,退款如何计算,都可能影响结果。setScale(2, HALF_UP) 只是示例选择,并非所有货币计算的通用答案。
  5. 把创建模式当成扩展能力。工厂能集中创建逻辑,却不会自动让新规则无需改代码。示例新增一种策略,仍需要增加实现并调整工厂。是否需要可配置注册机制,要看项目的扩展和部署要求。

七、AI 生成的模式代码要怎样审查

AI 生成的代码可能语法正确,却不符合项目行为。建议按以下顺序检查,而不是只看代码是否“像设计模式”:先核对业务规则,再检查公开接口和异常,再检查数据精度与并发,最后运行编译、测试和静态检查。

  • 核对需求对应关系:每个新增类都应能对应一项明确变化或职责。找不到理由的抽象可能没有必要。
  • 检查空值和非法值:确认校验覆盖了接口边界,而不仅是示例里的常用输入。
  • 检查对象状态:策略是否包含可变字段?是否会被多个线程共享?不可变对象通常更容易安全复用,但不能仅凭“用了模式”就断言线程安全。
  • 执行真实构建:示例能运行,不代表能兼容项目的 JDK、依赖版本、代码风格和构建流程。
  • 审查差异范围:确认没有改动不相关文件、删除必要校验或引入未经允许的依赖。

尤其要分清“输入校验”和“安全防护”。参数范围检查可以防止不合法折扣进入结算逻辑,但它不能替代身份认证、授权、数据库访问控制或交易风控。来自客户端的金额和折扣类型不能因为前端已经校验就被信任;服务器端必须按可信业务数据重新计算或验证。

八、参数限制与安全用途的准确边界

示例中的百分比限制、金额非负和固定减免封顶,都是针对折扣计算的业务完整性检查。它们有助于发现错误输入,不能据此宣称系统具备支付安全、反欺诈或完整的财务合规能力。真实系统还需要根据架构设计身份认证、权限检查、审计、幂等、防重放和数据访问控制等措施。

提示词中也不要放入密钥、密码、签名材料或可直接访问生产系统的凭证。AI 生成的命令、SQL、配置和代码都应视为待审阅内容。特别是涉及删除数据、修改权限、执行脚本或触发付款的操作,应在受控环境中验证,并遵守项目审批和发布流程。

Java 代码中的参数校验同样不能代替数据库约束。若数据需要持久化,应根据数据模型设置恰当的唯一性、非空和范围约束,并在应用层返回可理解的错误。应用校验与数据库约束解决的问题不同:前者改善调用边界和错误反馈,后者防止数据状态被其他写入路径破坏。

九、把方法落到日常开发流程

一套可重复的实践可以浓缩为以下步骤:

  1. 描述问题,而不是先指定模式。记录当前分支、变化原因和具体维护成本。
  2. 补齐素材。提供相关代码、项目约定、样例输入和验收条件,移除敏感信息。
  3. 让 AI 先分析,再生成。要求说明候选方案、取舍和未知假设。
  4. 选择最小有效结构。只抽取确实变化的部分,避免为未来猜想提前搭建复杂框架。
  5. 验证边界。检查空值、范围、精度、异常、并发和外部输入的处理方式。
  6. 本地运行并审查。执行项目构建和测试,检查差异,再决定是否接受建议。

模式提供的是组织代码的语言,提示词提供的是传递上下文的方法,素材则决定建议能否贴近项目。三者都不能替代业务确认和工程验证。先把规则说清楚,再选择合适的结构,最后用测试和代码审查证明实现符合约定,才是 Java 与设计模式实践中更可靠的路线。

© 版权声明

相关文章