Skip to content

什么时候需要代码抽象?

抽象是一种为了提升代码复用性、可维护性和扩展性而对功能进行归纳和提取的过程,但抽象本身也是一种成本,过早或不必要的抽象可能会让代码变得复杂,增加维护难度。所以,是否需要抽象,以及何时抽象,是一个需要权衡的问题

以下是一些关于何时抽象的建议,结合你的场景(训练模型时流程一致,但细节不同),我们可以通过以下几个方面来判断是否需要抽象。


1. 抽象的时机

1.1 当逻辑重复时

如果你发现代码中存在明显的重复逻辑,并且这些逻辑在多个地方都需要维护,那么是时候考虑将这些重复的部分抽象为通用逻辑了。

示例:

假设你在训练多个模型时,每次都需要加载数据,但每次加载的格式和细节略有不同。以下是重复的逻辑:

# 加载数据的逻辑几乎一样,只是路径不同
def train_model_a():
    dataset = load_dataset(path="dataset_a", batch_size=32, shuffle=True)
    model = ModelA()
    train(model, dataset)

def train_model_b():
    dataset = load_dataset(path="dataset_b", batch_size=64, shuffle=False)
    model = ModelB()
    train(model, dataset)

如果这种重复逻辑越来越多,就可以考虑抽象一个通用的 train_model 函数:

def train_model(model_cls, dataset_path, batch_size, shuffle):
    dataset = load_dataset(path=dataset_path, batch_size=batch_size, shuffle=shuffle)
    model = model_cls()
    train(model, dataset)

# 使用抽象后的逻辑
train_model(ModelA, "dataset_a", 32, True)
train_model(ModelB, "dataset_b", 64, False)

抽象能减少重复代码,避免多处维护。


1.2 当需求稳定时

抽象需要一定的成本。如果你的需求还在频繁变化,抽象可能会导致过度设计,反而增加了代码的复杂性和维护成本。因此,当某些操作或流程已经足够稳定,并且变更不大时,可以考虑抽象。

示例:

  • 如果你已经确定所有模型的训练过程(如加载数据、初始化模型、训练、验证等)是大致一致的,但部分细节(如数据路径、模型类型)需要灵活配置,那么可以抽象出一个训练框架,允许通过参数或配置文件调整细节。

1.3 当需要支持扩展时

如果你需要支持多种模型或配置,并且希望让代码更具扩展性(即不需要改动现有代码,只需增加新模块),那么可以通过抽象来实现。

示例:

你可能需要支持不同的模型(如 ResNet、VGG)、不同的数据集(如 CIFAR10、MNIST)以及不同的优化器。通过抽象注册机制或工厂模式,可以简化扩展逻辑:

# 注册模型和数据集
MODELS = Registry('models')
DATASETS = Registry('datasets')

@MODELS.register_module(name='resnet50')
class ResNet50: ...

@DATASETS.register_module(name='cifar10')
def load_cifar10(): ...

# 通用训练函数
def train_model(model_name, dataset_name):
    model_cls = MODELS.get(model_name)
    dataset_loader = DATASETS.get(dataset_name)
    dataset = dataset_loader()
    model = model_cls()
    train(model, dataset)

# 通过简单的配置扩展
train_model("resnet50", "cifar10")

2. 判断是否需要抽象的关键维度

2.1 重复性

  • 高重复性:如果某段逻辑在多处被复用,抽象的价值较高。
  • 低重复性:如果某个逻辑仅在少数场景中使用,抽象可能得不偿失。

2.2 稳定性

  • 高稳定性:可以抽象,因为需求不太会变,抽象的设计可以长期使用。
  • 低稳定性:不要过早抽象,因为需求变动可能导致抽象被频繁重构或废弃。

2.3 灵活性

  • 需要高灵活性:可以抽象出通用框架,允许通过配置或参数调整细节。
  • 灵活性需求低:直接硬编码实现即可,无需抽象。

2.4 复杂性

  • 抽象后减少复杂性:如果抽象后代码更清晰、更易维护,那值得抽象。
  • 抽象后增加复杂性:如果抽象会引入大量额外的类、接口或逻辑,应该避免。

3. 如何逐步抽象?

抽象是一种迭代的过程,不一定要一次性完成。以下是一个逐步抽象的思路:

3.1 从重复的代码开始

先识别出最明显的重复逻辑,将这些部分提取出来作为独立函数或模块。

示例:

# 提取重复的加载数据逻辑
def load_data(path, batch_size, shuffle):
    return load_dataset(path=path, batch_size=batch_size, shuffle=shuffle)

dataset_a = load_data("dataset_a", 32, True)
dataset_b = load_data("dataset_b", 64, False)

3.2 提取通用流程

当整个流程中只有少部分细节不同时,可以通过参数化或继承的方式来处理。

示例:

# 通用训练流程
def train_pipeline(model_cls, dataset_loader):
    dataset = dataset_loader()
    model = model_cls()
    train(model, dataset)

# 配置差异化
train_pipeline(ModelA, lambda: load_data("dataset_a", 32, True))
train_pipeline(ModelB, lambda: load_data("dataset_b", 64, False))

3.3 引入配置和注册机制

当扩展需求增多,可以通过配置文件或注册表的方式替代硬编码。

示例:

# 配置文件
config = {
    'model': 'resnet50',
    'dataset': 'cifar10',
    'batch_size': 32,
    'shuffle': True
}

# 动态加载
model_cls = MODELS.get(config['model'])
dataset_loader = DATASETS.get(config['dataset'])
dataset = dataset_loader(batch_size=config['batch_size'], shuffle=config['shuffle'])
model = model_cls()
train(model, dataset)

4. 什么时候不应该抽象?

4.1 需求频繁变动

如果业务需求还没有稳定,抽象可能会导致需要频繁修改抽象逻辑,增加工作量。

4.2 抽象后增加复杂性

如果抽象后的代码比原始代码更难理解或更复杂,那抽象就失去了意义。

4.3 低复用性

如果某段逻辑只在少数地方使用,直接实现即可,抽象可能是浪费。


总结

抽象的本质是权衡: - 当重复性高、需求稳定、需要扩展时,抽象是合适的。 - 如果需求不稳定、复用性低、抽象增加复杂性,就不应该过早抽象。

建议: 1. 先写具体实现:在需求不明朗时,先实现功能,不要急于抽象。 2. 识别重复逻辑:当代码中有明显的重复时,再逐步提取通用部分。 3. 逐步抽象:从简单的函数抽象开始,逐步向更复杂的模块抽象过渡。 4. 优先读代码的可读性:抽象后的代码应该更易读、更易维护,而不是更复杂。