改变范式:从过早重构和虚假“可重用性”到适应性、可扩展性和可靠性

在软件世界中,普遍存在对**过早重构**和**虚假可重用性**的痴迷。开发人员(尤其是刚入行的开发人员)经常被教导“可重用性”是圣杯。但是,不惜一切代价追求可重用性往往会导致过度设计的解决方案,这些解决方案过于通用、过于死板,并且与手头项目的特定需求相差甚远。事实上,它可能导致我们通常所说的**“抽象地狱”**——除非您完全理解系统的每个部分是如何以及为什么被抽象为适合通用接口的,否则任何事情都无法真正发挥作用。

我们建议进行范式转变:**不要过分关注可重用性,而要关注适应性、可扩展性和可覆盖性。**

在这种背景下,我们不再试图预测代码库的未来需求(就像算命先生预测未来一样),而是专注于为今天创建一个坚实、灵活的基础,并且随着未来的发展,它仍然有成长和发展的空间。

过早重构的困境:虚假的可重用性

**过早重构**的问题在于,它源于这样一种信念:你编写的所有内容都应该**可重用**。这似乎是一个崇高的目标。然而,可重用性往往会导致不必要的复杂性和不必要的抽象。例如,创建一个适用于所有模型的**通用 API 适配器**。理想情况下,这个适配器可以处理任何 API 端点、任何数据格式和任何网络条件。但实际上,这意味着你正在为不确定的未来构建一个**框架**,而不是有效地解决今天的问题。

**例子:**

让我们来看看之前的“BaseAdapter”和“APIAdapter”类:

export class BaseAdapter {
    constructor(modelClass) {
        this.modelClass = modelClass;
    }

    async get(id) {
        throw new Error("Method 'get' must be implemented.");
    }

    async *all() {
        throw new Error("Method 'all' must be implemented.");
    }

    async query(params = {}) {
        throw new Error("Method 'query' must be implemented.");
    }

    async create(payload) {
        throw new Error("Method 'create' must be implemented.");
    }

    async update(payload) {
        throw new Error("Method 'update' must be implemented.");
    }

    async delete(id) {
        throw new Error("Method 'delete' must be implemented.");
    }
}

在上面的代码中,`BaseAdapter` 定义了所有可能的方法,让我们在特定的子类中实现它们(例如 `APIAdapter`、`LocalStorageAdapter` 等)。这是各种适配器的 **模板**。理论上听起来不错,对吧?有一天,如果我们需要连接到新服务或与新的存储解决方案集成,我们只需创建另一个子类即可。

但让我们面对现实:**它真的可以重复使用吗?**还是它只会变得非常复杂,使您的系统更难维护、理解和扩展?您真的在构建可以在世界上重复使用的东西吗?还是您只是在猜测未来?

转变:从可重用性到适应性、可扩展性和可覆盖性

我们建议不要追求过早的可重用性,而是专注于**适应性**和**可扩展性**。这是什么意思?

  • 适应性:创建一个可以轻松改变或扩展的基础,而无需重写大量代码。
  • 可扩展性:为新功能留出空间,而无需重构整个架构。
  • 可覆盖性:允许您的代码被其他人(或您自己将来)轻松扩展或覆盖,而不会冒破坏一切的风险。
  • 这并不是要创建适用于当今每种极端情况的**完全可重用的代码**。相反,我们专注于构建**坚实的基础**,您可以在此基础上进行构建、添加和修改。关键是**灵活性**,而不是过早优化。

    旧“界面”范式:预测未来

    在 Java(以及许多其他静态类型语言)的早期,人们通常关注的是创建**接口**并使代码“面向未来”。其理念是提前预测每种情况并围绕它进行设计。

    然而,这种方法往往会导致**过度设计**:为可能永远不会发生的事情进行设计,或者围绕尚未出现的问题构建抽象框架。您实际上是在编写本应“通用”的代码,而没有理解您正在开发的系统的具体需求。

    在 Java 中,接口用于定义契约。但如果我们将这种思维从“定义契约”转变为简单地**设定当前的期望**,结果会怎样?对于当前情况而言,承诺**清晰**且**可靠**,而无需假设未来会发生什么。

    一种新的承诺:对未来自己的承诺

    在我们的新方法中,我们不会像神秘的算命师一样对应用程序的未来做出承诺。相反,我们**为今天设定明确、可靠的承诺**,并确保这些承诺可以在需要时轻松扩展和调整。

    可以这样想:我们不是预测 5 年后的世界会是什么样子;而是确保我们今天编写的代码能够随着世界的变化而发展和适应。这就像为建筑物打下坚实的基础,确保它足够坚固以承受任何变化。

    我们做出的“承诺”是**对适应性和可扩展性的承诺**。我们的目标不是预测未来,而是**创建工具**,让未来的开发人员(或未来的您自己)能够根据需要轻松添加、修改或扩展功能。

    真实示例:扩展和覆盖适配器

    让我们重新回顾一下使用“BaseAdapter”和“APIAdapter”的示例。我们不会创建试图处理所有情况的**超级通用方法**,而是专注于使代码**适应性**和**易于扩展**。

    以下是 `APIAdapter` 的快速重新架构:

    export class APIAdapter extends BaseAdapter {
        static baseURL;
        static headers;
        static endpoint;
    
        async *all(params = {}) {
            // Custom logic, but easily extensible if needed
            const url = `${this.baseURL}/${this.endpoint}`;
            const response = await API.get(url, { params, headers: this.headers });
            return response.data;
        }
    
        async query(params = {}) {
            // Simplified for illustration
            const url = `${this.baseURL}/${this.endpoint}/search`;
            const response = await API.get(url, { params });
            return response.data;
        }
    
        // Easily extendable for specific cases
        async customRequest(method, endpoint, params = {}) {
            const url = `${this.baseURL}/${endpoint}`;
            const response = await API[method](url, { params });
            return response.data;
        }
    }

    现在,我们不再需要为每种新类型的适配器创建全新的 BaseAdapter,而是创建了一个可以轻松扩展和适应未来需求的基础。

    **扩展新 API 端点的示例:**

    class OrderAdapter extends APIAdapter {
        static baseURL = 'https://api.example.com';
        static endpoint = 'orders';
    }
    
    class UserAdapter extends APIAdapter {
        static baseURL = 'https://api.example.com';
        static endpoint = 'users';
    }

    在这种情况下,如果您需要为一个 API 端点添加特定行为(例如,对“订单”进行自定义错误处理),您可以**覆盖**或**扩展**“APIAdapter”以满足您的需求,而无需重构整个系统。

    结论:对未来自己的承诺

    在这个新模式中,我们不会试图预测未来的每一个需求或问题。相反,我们专注于**构建一个强大而灵活的基础**,以**适应**需求的变化和新挑战的出现。我们**不会过早地抽象**或过度设计基于假设问题的解决方案。相反,我们创建**工具**,这些工具可以随着新需求的出现而发展并轻松适应。

    关键不在于像算命先生那样做好未来准备,而是要创建一个即使世界发生变化也能经受住时间考验的基础。这是你可以向未来的自己做出的承诺:代码是可靠的、适应性强的,并且可以随着新需求的出现而扩展。