← 返回蜂巢洞察

在迁移之前如何对现有的旧应用程序进行重构

一旦一个团队决定迁移现有的旧应用程序,通常就会面临立即开始迁移代码的压力。 需要迁移数据库、API或用户界面,或者将整个应用程序迁移到新的框架、运行环境、云服务提供商或架构中。 这听起来似乎合情合理,但实际上存在一个问题。 如果当前的系统将业务规则、数据持久化机制、基础设施、外部集成功能以及各种协调逻辑都混合在同一个模块中,那么迁移工作就会变得比实际情况要困难得多。 你并不是在仅仅迁移软件本身,而是在试图迁移那些在多年开发过程中逐渐纠缠在一起的各种组件和功能。 正因如此,我通常更倾向于在迁移之前先对代码进行重构。这样做的目的并不是为了让旧系统看起来更加美观,也不是要彻底重新设计整个系统,更不是

一旦一个团队决定迁移现有的旧应用程序,通常就会面临立即开始迁移代码的压力。需要迁移数据库、API或用户界面,或者将整个应用程序迁移到新的框架、运行环境、云服务提供商或架构中。这听起来似乎合情合理,但实际上存在一个问题。如果当前的系统将业务规则、数据持久化机制、基础设施、外部集成功能以及各种协调逻辑都混合在同一个模块中,那么迁移工作就会变得比实际情况要困难得多。你并不是在仅仅迁移软件本身,而是在试图迁移那些在多年开发过程中逐渐纠缠在一起的各种组件和功能。正因如此,我通常更倾向于在迁移之前先对代码进行重构。这样做的目的并不是为了让旧系统看起来更加美观,也不是要彻底重新设计整个系统,更不是想把准备阶段变成又一次代码重写的过程。我们的目标其实很简单:仅仅是对系统结构进行适当的调整,使得那些重要的功能能够被独立地迁移出来。在这个工作流程的前几个步骤中,我们首先会尝试了解代码的结构,然后通过编写特征测试来保护那些我们即将修改的功能。现在,你将学习如何开始调整系统结构。在本教程中,我会向你展示如何为迁移旧应用程序做准备,具体方法包括:

  • 确定迁移的范围;

  • 将业务规则与基础设施分离开来;

  • 引入适当的接口和适配层;

  • 隔离那些可能产生副作用的代码部分;

  • 为外部系统编写相应的适配器;

    解决依赖关系方向带来的问题;

    提取出应用程序中那些具有独立性的功能模块;

    在整个重构过程中持续使用特征测试来验证代码的正确性;

    合理利用人工智能技术,但绝不能让它盲目地重新设计整个系统;

    以及判断应用程序何时已经准备好开始迁移。

    这些示例都是使用TypeScript语言来进行的,但实际上这个流程适用于大多数编程语言和架构。

    我们的目标并不是让旧系统变成完美的新系统,而是让它具备适合迁移的特性:

    旧应用程序
    ↓
    适合迁移的结构
    ↓
    逐步进行迁移
    这样的处理方式能够避免很多不必要的麻烦和工作。

    先决条件

    你需要具备以下能力:

    • 阅读现有的代码库;

    • 熟悉TypeScript或类似的语言;

    • 掌握单元测试和集成测试的方法;

    • 了解依赖注入的原理;

    • 熟悉接口和适配器的设计;

    • 具备基本的软件架构知识;

    • 会进行逐步重构工作。

    • 此外,对于那些你计划修改的功能模块,你也应该采取一些措施来保护它们的正常运行。

      这些保护措施可能包括……

      • 特性测试

      • 集成测试

      • 契约测试

      或者采用其他可靠的方法来验证现有系统的行为。

      在没有这些保护措施的情况下进行重构也是可行的。但这样一来,就很难区分哪些是结构上的改进,哪些只是偶然出现的功能变化。

      目录

      为什么迁移问题往往在迁移开始之前就出现了

      假设你需要对一个订单处理系统进行迁移。

      当你检查这个系统的核心功能时,会发现一些如下所示的问题:

      async function processOrder(orderId: string) {
        const connection = await mysql.getConnection();
      
        const [rows] = await connection.query(
          "SELECT * FROM orders WHERE id = ?", 
          [orderId]
        );
      
        const order = rows[0];
      
        if (!order) {
          throw new Error("未找到相应的订单");
        }
      
        if (order.customer_type === "PREMIUM") {
          order.total = order.total * 0.9;
        }
      
        if (
          order.country === "AR" && order.payment_method === "TRANSFER"
        ) {
          order.total -= 500;
        }
      
        await connection.query(
          "UPDATE orders SET total = ?, status = ? WHERE id = ?", 
          [order.total, "PROCESSED", order.id]
        );
      
        await paymentProvider.createPayment({
          orderId: order.id,
          amount: order.total,
        });
      
        await eventBus.publish("order.processed", {
          id: order.id,
          total: order.total,
        });
      
        await emailClient.send({
          to: order.customer_email,
          template: "order-processed",
        });
      
        return order;
      }
      

      假设迁移的目标是:

      从MySQL迁移到PostgreSQL;
      从旧运行时环境迁移到新运行时环境;
      将旧的API替换为新的服务。

      很自然地,人们会想要立即开始将这些功能移植到目标系统中去。

      但究竟是什么需要被迁移呢?

      这些功能实际上包含了:

      数据库访问、业务规则处理、状态转换逻辑、支付集成机制、事件发布功能、电子邮件发送服务以及应用程序的协调调度等。

      如果现在更改数据库,可能会影响系统的定价机制;而修改支付相关的组件,则可能会影响数据持久性。另外,将某个功能移至另一个服务中,意味着需要同时迁移它所依赖的所有组件。

      迁移过程中遇到的困难,在一定程度上是由于当前的系统结构造成的。因此,在开始迁移之前,应该先确保这些组件可以独立地进行迁移。

      在重构之前先确定迁移的范围

      不要从以下步骤开始:

      “让我们先整理一下这个应用程序吧。”

      而应该先问自己:

      我们首先想要迁移哪些功能呢?

      假设你决定首先迁移“订单处理”功能,那么这个功能就为你的重构工作提供了一个明确的边界。

      接下来,你可以将这个功能的具体流程梳理出来:

      输入:
      orderId
      
      业务逻辑:
      加载订单信息、计算所需调整金额、标记订单为已处理
      
      后续操作:
      将订单信息持久化存储、生成支付请求、发布相关事件、发送电子邮件
      
      输出:
      已处理的订单

      这样的规划要比直接决定重构某个文件夹要有用得多,因为一个文件夹并不一定代表一个明确的业务功能边界。

      只有当你能够清晰地理解各个功能的具体作用时,迁移工作才能顺利进行。

      例如:

      订单处理、取消订单、生成发票、注册客户、续订服务

      每一个这些功能都有可能成为一个独立的迁移单元。

      不要对整个应用程序进行重构

      一旦开始发现系统中的架构问题,人们就很容易想要把所有这些问题都解决掉。

      你可能会注意到以下这些问题:

      循环依赖关系、重复存在的代码库、全局配置文件、庞大的服务模块、静态辅助函数、直接访问数据库的行为、不一致的错误处理机制、混合使用的领域模型等等

      虽然所有这些问题都值得关注,但迁移过程并不一定要求所有的技术缺陷都被立即消除。

      假设你的迁移目标是“订单处理”功能,那么一个有用的原则就是:只重构那些会妨碍这一功能安全迁移的部分。

      例如:

      问题:
      订单处理模块直接调用MySQL数据库。
      
      相关吗?
      是的。
      
      问题:
      报告模块使用的日期格式不一致。
      
      相关吗?
      可能不相关。
      
      问题:
      订单处理模块直接使用支付SDK。
      
      相关吗?
      是的。
      
      问题:
      管理员界面中存在重复的CSS代码。
      
      相关吗?
      不相关。

      这样就可以避免准备工作演变成一个无止境的清理项目。

      对旧代码进行现代化改造时,必须严格控制改造的范围。

      将业务规则与基础设施分离

      最有益的结构变革往往就是将业务逻辑与特定技术细节分开来处理。

      来看这段代码:
      async function processOrder(orderId: string) {
        const order = await mysqlOrders.find(orderId);
      
        if (order.customerType === "PREMIUM") {
          order.total *= 0.9;
        }
      
        if (
          order.country === "AR" && order.paymentMethod === "TRANSFER"
        ) {
          order.total -= 500;
        }
      
        await mysqlOrders.update(order);
      
        await stripe.createPayment({
          orderId: order.id,
          amount: order.total,
        });
      }
      
      定价逻辑本身并不需要依赖MySQL或Stripe这些技术。 你可以将这部分代码提取出来,单独编写:
      type Order = {
        id: string;
        total: number;
        customerType: "STANDARD" | "PREMIUM";
        country: string;
        paymentMethod: "CARD" | "TRANSFER";
      };
      
      function calculateOrderTotal(order: Order): number {
        let total = order.total;
      
        if (order.customerType === "PREMIUM") {
          total *= 0.9;
        }
      
        if (
          order.country === "AR" && order.paymentMethod === "TRANSFER"
        ) {
          total -= 500;
        }
      
        return Math.max(total, 0);
      }
      
      现在,定价逻辑 不再与MySQL或Stripe这些技术紧密绑定。 这种分离并不会导致需要进行全面的设计重构,只是一种很有用的做法而已。 后续的迁移步骤可以在不改变现有逻辑的情况下替换基础设施组件。

      在难以替代的依赖关系中建立隔离层

      旧代码中往往存在一些在测试或迁移过程中无法轻易替换的依赖关系。 例如:
      class OrderService {
        async process(orderId: string) {
          const client = new LegacyDatabaseClient();
      
          const order = await client.findOrder(orderId);
      
          // ...
        }
      }
      
      在这个例子中,对数据库的依赖是在方法内部创建的。 这就使得替换这些依赖关系变得非常困难。 通过进行简单的重构,就可以在它们之间建立隔离层:
      interface OrderRepository {
        findById(id: string): Promise;
        save(order: Order): Promise;
      }
      
      修改后的代码如下:
      class OrderService {
        constructor(
          private readonly orders: OrderRepository
        ) {}
      
        async process(orderId: string) {
          const order = await this.orders.findById(orderId);
      
          if (!order) {
            throw new Error("Order not found");
          }
      
          // 其余逻辑保持不变
        }
      }
      
      现在,原有的MySQL实现仍然可以满足这个接口的要求。
      class MySqlOrderRepository implements OrderRepository {
        async.findById(id: string) {
          // 这是与MySQL当前的行为一致的实现方式
        }
      
        async save(order: Order) {
          // 这也与MySQL当前的行为一致的实现方式
        }
      }
      

      后来,这种迁移措施可以带来以下变化:

      class PostgresOrderRepository implements OrderRepository {
        // 新的实现方式
      }
      

      请注意我们并没有改变什么:我们没有改变业务逻辑本身,而是改变了依赖项的可替换性

      正是这种重构方式才有助于顺利完成迁移工作。

      将副作用与决策逻辑分离

      另一种有用的分离方式是区分:

      决策过程

      和:

      async function cancelOrder(order: Order) {
        if (order.status === "SHIPPED") {
          throw new Error("已发货的订单无法取消");
        }
      
        order.status = "CANCELLED";
      
        await orders.save(order);
        await inventory.release(order.id);
        await payment/refund(order.id);
        await audit.log("ORDER_CANCELLED", order.id);
      }
      

      这里实际上包含了两种不同的职责。

      首先是业务决策部分:

      这个订单能否被取消?
      它的新状态应该是什么?

      然后是具体的执行操作:

      function cancelOrderState(order: Order): Order {
        if (order.status === "SHIPPED") {
          throw new Error("已发货的订单无法取消");
        }
      
        return {
          ...order,
          status: "CANCELLED",
        };
      }
      

      剩下的执行操作部分可以保持不变:

      async function cancelOrder(order: Order) {
        const cancelled = cancelOrderState(order);
      
        await orders.save(cancelled);
        await inventory.releasecancelled.id);
        await payment/refund.cancelled.id);
        await audit.log("ORDER_CANCELLED", cancelled.id);
      
        return cancelled;
      }
      

      虽然功能没有变化,但现在状态转换的部分可以独立进行测试和迁移了。

      如果目标架构改变了副作用的执行方式,这种分离机制就显得尤为重要。

      例如,未来的版本可能会使用:

      将外部系统通过适配器进行隔离
      

      外部SDK通常会深入地渗透到旧代码中。

      例如:

      const result = await stripe.paymentIntents.create({
        amount: order.total,
        currency: "usd",
        metadata: {
          orderId: order.id,
        },
      });
      

      如果有很多应用程序模块直接依赖于Stripe SDK,那么更换或重新设计支付处理流程就会变得非常困难。

      相反,应该创建一个应用层层面的边界:

      type PaymentRequest = {
        orderId: string;
        amount: number;
      };
      
      type PaymentResult = {
        paymentId: string;
      };
      
      interface PaymentGateway {
        charge(
          request: PaymentRequest
        ): Promise〈PaymentResult〉;;
      }
      

      Stripe适配器包含了特定于该提供商的实现细节:

      class StripePaymentGateway implements PaymentGateway {
        async charge(
          request: PaymentRequest
        ): Promise〈PaymentResult〉> {
          const result =
            await stripe.paymentIntents.create({
              amount: request.amount,
              currency: "usd",
              metadata: {
                orderId: request.orderId,
              },
            });
      
          return {
            paymentId: result.id,
          };
        }
      }
      

      现在,应用程序所依赖的是:

      PaymentGateway

      而不是:

      Stripe SDK

      这种设计在迁移过程中非常有用,因为特定于提供商的代码可以实现本地化处理。

      同样的模式也适用于以下场景:

      电子邮件服务提供商
      消息代理服务器
      云存储服务
      企业资源规划系统集成
      客户关系管理API
      身份认证服务提供商
      搜索引擎

      适配器的价值并不在于接口本身,而在于它能够帮助我们建立可灵活调整的边界。

      无需重建所有代码即可优化依赖关系方向

      传统系统中,依赖关系往往呈现如下结构:

      业务逻辑
          ↓
      数据库访问层
          ↓
      框架工具类

      这种架构会导致基础设施难以被替换或升级。

      你并不一定需要完全遵循“清洁架构”的设计原则,只需要在需要进行迁移的地方优化依赖关系方向即可。

      举个例子:

      改造前:

      OrderService ↓ MySQL

      改造后:

      OrderService ↓ OrderRepository ↑ MySqlOrderRepository

      应用程序依赖的是一个抽象层,而基础设施则负责实现这一抽象层。

      在支付处理方面也可以采用同样的方式:

      OrderService ↓ PaymentGateway ↑ StripePaymentGateway

      对于消息传递机制也是如此:

      OrderService ↓ OrderEvents ↑ KafkaOrderEvents

      这样一来,更换基础设施就不再需要重新编写应用程序代码了——这才是真正的关键所在。

      提取一个紧密统一的application边界

      经过多次小规模的重构后,系统的结构可能会变成这样:

      interface OrderRepository { .findById(id: string): Promise〈Order | null〉;; save(order: Order): Promise〈void〉;; } interface PaymentGateway { charge(request: { orderId: string; amount: number; ): Promise〈void〉;; } interface OrderEvents { processed(order: Order): Promise〈void〉;; } class ProcessOrder { constructor( private readonly orders: OrderRepository, private readonly payments: PaymentGateway, private readonly events: OrderEvents ) {} async execute(orderId: string) { const order = await this.orders.findById(orderId); if (!order) { throw new Error("订单未找到"); } const total = calculateOrderTotal(order); const processed: Order = { ...order, total, status: "已处理", }; await thisorders.save(processed); await thispayments.charge({ orderId: processed.id, amount: processed.total, }); await this.events.processed(processed); return processed; } }

      这并不一定是最终的架构方案,这一点很重要。

      我们并不是在声称:

      应用程序就应该永远保持这样的外观。

      我们只是想说:

      目前的这种功能设计存在一定的局限性,但这些局限性反而使得后续的迁移工作变得更加容易。

      基础设施可以独立地进行调整和变更。

      业务规则是可以被验证的,系统各组件之间的交互关系也是清晰可见的,而且外部接口的定义也非常明确。

      这些条件已经足以让我们开始考虑进行迁移了。

      在重构过程中要确保行为测试能够持续运行

      在这个阶段,之前步骤中编写的行为测试就派上了用场。

      假设原来的功能实现是通过以下代码来保护的:
      it("功能实现应保持原有的高级订单处理流程", async () => {
        const result = await processOrder("order-1");
      
        expect(result.total).toBe(9000);
        expect(result.status).toBe("PROCESSED");
      
        expect(payment.charge).toHaveBeenCalledWith({
          orderId: "order-1",
          amount: 9000,
        });
      
        expect(events.processed).toHaveBeenCalled();
      });
      
      现在你可以将其中直接访问数据库的部分修改为使用
      仓库机制
      ,然后重新运行测试。 接着再将直接使用支付SDK的部分修改为使用
      进行小的结构调整 → 运行测试 → 再进行小的结构调整 → 运行测试 → ……
      这样做很重要,因为当不会同时发生行为上的变化时,结构重构会变得容易理解得多。 如果在进行了一次小的修改后测试失败,那么可能的故障原因就非常有限; 但如果在进行了长达两周的代码重写之后测试仍然失败,那么可能的故障原因几乎可以是任何东西。

      在结构重构过程中如何利用人工智能

      在这个阶段,人工智能能够提供很大的帮助。

      但是,此时需要给人工智能提供的指令应该与以下类型的不同:
      当前这个服务是直接访问MySQL数据库的。
      
      我希望在不改变现有功能行为的前提下,引入一个“订单仓库”机制。
      
      具体任务如下:
      1. 找出这个服务所使用的所有数据库操作;
      2> 确定所需的最低限度的仓库接口设计;
      3> 将现有的数据库调用全部移至适配器之后执行;
      4> 在适当的情况下保留返回值、错误信息以及调用的顺序;
      5> 不要修改任何业务规则;
      6> 不要引入额外的抽象层。
      
      在生成代码之前,必须先详细解释每一项结构上的变更。

      这样一来,人工智能所能承担的任务范围就变得狭窄多了。

      另一个有用的请求是:

      比较这次重构前后代码的实现情况,找出可能发生的变化。具体需要检查以下内容:  
      - 异常处理;  
      - 返回值;  
      - 边界效应;  
      - 边界效应的执行顺序;  
      - 对空值的处理方式;  
      - 事务边界;  
      - 重试机制。  
      不要因为代码看起来相似就认为它们是等价的。

      在这里,人工智能可以作为第二审查者发挥重要作用。它能够比人类更快地发现这些差异,但最终还是需要测试来提供更有力的证据。

      不要过早让人工智能来设计目标架构

      人工智能在识别常见的架构模式方面确实非常擅长,但这也会带来一定的风险。例如,如果你让人工智能去分析一个复杂的旧系统,然后询问它“应该如何进行现代化改造?”,它可能会给出诸如“微服务”、“事件驱动架构”之类的答案。不过,这些技术或模式是否真的适合当前的需求,还需要进一步评估。

      这些技术或模式确实都是可行的,但并没有一种能自动证明其合理性。

      在选择目标架构之前,你必须明确一些约束条件。例如:

      部署频率;  
      团队规模;  
      事务处理要求;  
      延迟时间;  
      容错能力;  
      数据所有权;  
      集成边界;  
      系统的成熟度;  
      流量情况;  
      成本因素;  
      监管要求等等。

      有时候,一个具有明确边界的大型单体系统可能比微服务更适合当前的需求;同步处理流程可能比事件驱动架构更有效率;而数据库迁移也可能并不需要修改业务模型。因此,架构设计应该遵循具体的约束条件,而不是盲目追求某种模式。

      选择架构时,必须考虑各种实际因素,而不能仅仅因为某个模式看起来熟悉就采用它。

      如何判断某项功能是否已经准备好进行迁移

      在某个时候,你必须停止对代码进行重构,而这个决策非常重要。你并不需要完美的代码——当你能清楚地回答以下这些问题时,就说明某项功能已经差不多可以准备进行迁移了。

      我能描述这项功能的输入参数吗?

      例如:

      订单编号;  
      客户信息;  
      请求数据;  
      事件信息等等。

      我能描述这项功能的输出结果吗?

      例如:

      处理后的订单信息;  
      HTTP响应结果;  
      事件通知;  
      数据库中的变更记录等等。

      这些功能的重要业务规则是否清晰可见?

      这些规则不需要做到完美无缺,但你必须知道它们具体存在于哪里。

      这些功能的外部依赖关系是否已经明确列出来?

      例如:

      OrderRepository  
      PaymentGateway  
      OrderEvents  
      EmailSender  
      

      基础设施可以被替换吗?

      如果更换 MySQL 需要修改定价逻辑,那么这样的系统结构很可能还不适合进行替换。

      重要的功能是否得到了保护?

      你应该有足够的测试来检测那些可能会无意中引入的变更。

      你知道这些变更可能带来的副作用吗?

      例如:

      保存订单信息  
      创建支付记录  
      发布事件通知  
      发送电子邮件  
      

      那些重大的未知因素是否已经被记录下来?

      虽然可能仍存在一些不确定性,但这些不确定性不应该被忽视。

      如果你能够回答这些问题,那么你很可能已经具备了开始迁移相关功能的必要条件。

      实用的迁移前重构工作流程

      以下是我会采用的工作流程。

      1. 选择一项功能进行重构

      不要对整个应用程序进行全面重构。

      可以选择以下某一项功能:

      处理订单  
      生成发票  
      续订服务  
      

      2. 确认相关功能是否得到了有效保护

      在进行结构变更之前,要确保那些关键功能有相应的测试用例。

      需要记录的内容包括:

      输出结果  
      状态转换过程  
      副作用  
      错误信息  
      契约规范  
      

      3. 识别迁移过程中可能遇到的障碍

      要留意那些可能导致迁移受阻的耦合关系,例如:

      4>在可能的情况下提取纯粹的业务逻辑
      

      应该将计算和决策逻辑从基础设施层中分离出来。

      例如:

      5>创建适当的边界划分
      

      对于那些与特定技术相关的功能,应该为它们创建明确的边界。

      例如:

      7>使整个迁移过程变得清晰可见
      

      应该确保每个功能模块的执行顺序都是易于理解的。

      例如:

      8>在每一步完成后都运行相应的行为测试

      不要一次性进行十项重构操作。每次所做的修改都应该尽量小。

      9. 对改前后的结果进行比较

      需要检查的内容包括:

      输入数据
      输出结果
      错误信息
      副作用
      数据结构
      排序方式
      事务处理流程

      10. 当迁移成为可能时立即停止重构

      不要继续进行重构,因为代码还有可能变得更加简洁。其实总是有改进的空间。

      我们的目标应该是让系统具备迁移的能力。

      在迁移之前不应进行哪些重构操作

      在这个阶段,通常我会避免修改一些内容,除非这些改动会直接影响到迁移进程。

      为所有元素重新命名

      你可能会不喜欢那些旧的命名方式,但为所有内容都重新命名会导致差异文件变得庞大,而实际上这种重构带来的价值却很小。

      对整个代码库进行格式化

      同样的问题:过多的格式化操作会使得代码变更的内容变得更难被理解。

      替换所有的设计模式

      一个老旧的系统可能会包含以下这些组件:

      重写稳定的算法
      

      如果某个旧的计算逻辑虽然不够简洁,但却被妥善保护且与其他代码隔离在一起,那么先保留它,之后再对其进行优化可能会更加安全。

      修复每一个发现的漏洞

      这一点尤为重要。

      在准备进行迁移的过程中如果发现了漏洞,一定要记录下来。然后再决定是否应该将这个漏洞的修复纳入当前的修改任务中。

      如果将以下几种操作混合在一起进行:

      保留原有的漏洞
      先完成迁移工作
      之后再专门修复这个漏洞

      这种做法听起来可能有些不便,但在迁移过程中意外引发的行为变化可能会带来更加严重的后果。

      重构只是准备工作,并非迁移本身

      在迁移之前进行的重构很容易变成一个永无止境的架构改进项目。

      你可能会开始想到:

      我们需要将数据库系统分离出来。

      然后又会想到:

      我们应该重新设计领域模型。

      接着又可能会认为:

      或许我们应该引入事件驱动机制。

      最后还可能觉得:

      如果这样做了,这个系统也许应该被拆分成微服务。

      几个月后,实际上什么都没有真正完成迁移。原本只是为了重构而开展的工作反而变成了整个项目的主要内容。

      这也是一个失败的模式。我们的目标应该是明确的、具体的。

      在迁移之前的代码结构可能是这样的:

      ProcessOrder
      ├── MySQL
      ├── 定价规则模块
      ├── Stripe支付系统
      ├── Kafka消息队列
      ├── 邮件处理组件
      └── 框架内部实现

      修改后的结构如下:

      ProcessOrder
      ├── OrderRepository
      ├── pricing rules
      ├── PaymentGateway
      ├── OrderEvents
      └── EmailSender
      

      这样可能就已经足够了。

      现在你有了多种选择。

      你可以进行如下操作:

      将 MySQL 迁移到 PostgreSQL

      而无需重新设计定价机制。

      你也可以替换某些组件:

      将 ProcessOrder 模块迁移到另一个运行环境

      同时确保其接口和功能保持不变。

      这种重构为人们提供了更多的选择,而这正是它的价值所在。

      结论

      当多种类型的变更同时发生时,对旧系统的迁移工作就会变得充满风险。

      你会修改系统的:

      了解系统现状 → 分析其行为特性 → 进行重构 → 最后进行迁移

      人工智能可以帮助大幅加快重构工作的进度。

      它能够识别各种依赖关系,提取接口信息,将相关调用放入适配器中处理,对比不同的实现方案,并帮助分析复杂的代码差异。

      不过,即使重构速度更快,也依然离不开架构层面的判断。事实上,这种判断反而变得更加重要了。

      因为我们的目标并不是要打造出旧系统的最完美版本,而是要创建出足以保证其能够安全迁移的结构

      一旦我们可以修改系统的基础设施而不影响其功能,那么迁移工作就不再看起来像是一次彻底的重新开发了。

      它反而会变成一系列有控制、有条理的变更过程。

相关文章

技术实践

如何在重构旧代码之前设计相应的特性测试用例

许多工程师在继承遗留代码后,首先想要做的就是改进这些代码。 你会遇到一些难以理解的函数,或者看到重复的逻辑、深度嵌套的条件语句、混合了业务规则的数据库调用,以及那些使得测试几乎无法进行的依赖关系。 你知道这些代码本可以做得更好,于是开始对其进行优化。 然而,随后就会出现问题。并不是因为新的实现方式存在明显的错误,而是因为旧的实现方式在背后做了某些人们之前根本不知道它在做的事情。 这就是在遗留代码现代化过程中最常出现的风险之一。 在修改代码之前,你需要有一种方法来回答一个简单的问题: 我是否保留了那些原本就重要的功能行为? 这时,特性测试就派上了用场。 特性测试并不是从“软件应该做什么”这个角度

阅读全文
技术实践

在修改现有的代码库之前,如何利用人工智能来理解这些代码的含义

许多工程师在继承现有的代码库时,首先想要做的就是修改这些代码。我完全理解这种冲动。 当你打开一个长达1500行的类文件时,你会看到其中混杂着数据库调用、业务规则、分散在各处的配置值、根本没人愿意去修改的方法,以及那些提到早已被淘汰的系统的注释。 这时,一款人工智能编码助手主动提出可以帮助你理解整个代码库的结构。 于是你就会问: 请重构这个类。 但通常来说,现在还不是进行重构的时候。 从处理遗留系统的经验中,我认识到:代码虽然可能写得很糟糕,但却可能蕴含着重要的信息。 某个奇怪的条件实际上可能代表着某种业务规则;重复出现的计算逻辑可能是由于两个看似相同的流程其实并不完全相同所致;一个命名很糟糕的

阅读全文
技术实践

如何利用人工智能对传统应用程序进行现代化改造,同时又避免对其进行彻底的重写?

我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。 但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。 虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。 人工智能让这个问题变得更加复杂了。 它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。 但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果: 还是那个系统,只不过被更快地重新编写了一遍而已。 这并不一定算

阅读全文
技术实践

文章:在金融支付系统中应用混沌工程:企业级ECS部署实践带来的经验教训

标准的混沌工程假设实验能够顺利结束,且故障的影响范围是可以提前预测的,同时生产环境也会成为这些实验的对象。然而,支付系统却完全违背了这三个前提。Salim Adedeji指出了在企业环境中部署ECS时可能出现的特定故障模式:例如60秒的DNS TTL设置会导致93秒的故障切换时间,重试机制会使数据库负载增加2.4倍,而区域重新平衡机制也会被常规工具忽略。 作者:Salim Adedeji

阅读全文