首页 > 解决方案 > 如何避免在许多点上存在少量差异的类似方法中的代码重复?

问题描述

我有一个非常数据驱动的程序,其中包含不同类型的实体,这些实体具有非常相似的结构,并且仅在特定位置有所不同。

例如,每个实体都有一个可以更改的名称。这里有两个示例方法来演示这些方法的相似之处:

pub fn rename_blueprint(
    &mut self,
    ctx: &mut Context,
    db_handle: &Transaction,
    blueprint_id: Uuid,
    new_name: &str,
) -> Result<(), DataError> {
    ctx.debug(format!(
        "Renaming blueprint {} to {}",
        blueprint_id, new_name
    ));
    self.assert_blueprint_exists(ctx, db_handle, blueprint_id)?;
    let mut stmt = db_handle
        .prepare("UPDATE `blueprints` SET `name` = ? WHERE `id` == ?")
        .on_err(|_| ctx.err("Unable to prepare update statement"))?;
    let changed_rows = stmt
        .execute(params![new_name.to_string(), blueprint_id])
        .on_err(|_| ctx.err("Unable to update name in database"))?;
    if changed_rows != 1 {
        ctx.err(format!("Invalid amount of rows changed: {}", changed_rows));
        return Err(DataError::InvalidChangeCount {
            changes: changed_rows,
            expected_changes: 1,
        });
    }
    ctx.blueprint_renamed(blueprint_id, new_name);
    Ok(())
}

pub fn rename_attribute(
    &mut self,
    ctx: &mut Context,
    db_handle: &Transaction,
    attribute_id: Uuid,
    new_name: &str,
) -> Result<(), DataError> {
    ctx.debug(format!(
        "Renaming attribute {} to {}",
        attribute_id, new_name
    ));
    self.assert_attribute_exists(ctx, db_handle, attribute_id)?;
    let mut stmt = db_handle
        .prepare("UPDATE `attributes` SET `name` = ? WHERE `id` == ?")
        .on_err(|_| ctx.err("Unable to prepare update statement"))?;
    let changed_rows = stmt
        .execute(params![new_name.to_string(), attribute_id])
        .on_err(|_| ctx.err("Unable to update name in database"))?;
    if changed_rows != 1 {
        ctx.err(format!("Invalid amount of rows changed: {}", changed_rows));
        return Err(DataError::InvalidChangeCount {
            changes: changed_rows,
            expected_changes: 1,
        });
    }
    ctx.attribute_renamed(attribute_id, new_name);
    Ok(())
}

对于 5-11 种实体类型,现在需要存在具有几乎相同代码的相同方法。我通常可以只用Blueprint其他实体类型的名称替换,它会工作的。然而,这似乎是一个非常愚蠢的解决方案。

同样,编写一个接受所有相关字符串、方法等的辅助方法来调用它似乎同样愚蠢。

我不相信我什至可以通过传递一些“策略”或其他间接帮助器(EntityRenamer或类似的东西)来避免这种情况,因为无论如何逻辑都需要在那里编码。它只是将问题向前推进了一步。

应该提到的是,这是较短的方法之一。实体也可以被移动、删除、创建等。所有这些都有相似的代码——有时长达 30 多行。

如何避免具有语义相同的字段/属性的不同结构的代码重复?不能解决我的问题。这个问题基本上是问“当不存在继承时如何进行继承”,而我的代码正在努力将非常相似的逻辑集体化为最低公分母。特征或通用实现不会解决我的问题,因为代码仍然存在 - 它只会被移动到其他地方。

您将如何去重复删除此代码?

与为我编写代码的人相比,我更在寻找指导方针。一些可能的解决方案可能是:

  1. 使用宏,然后使用类似的东西entity_rename_impl!(args)

  2. 对每个可能因函数而异的特定事物使用具有不同参数的辅助方法

  3. 不要试图抽象整个方法,而是专注于为较小的东西编写辅助函数,这样方法可能会重复,但在其他地方抽象的代码很少

MCVE(游乐场):

#![allow(unused)]

pub struct Transaction {}

impl Transaction {
    pub fn execute_sql(&self, sql: &str) -> i32 {
        // .. do something in the database
        0
    }

    pub fn bind_id(&self, id: Uuid) {}
}

#[derive(Clone, Copy)]
pub struct Uuid {}

impl std::fmt::Display for Uuid {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "mockup")
    }
}

pub fn assert_blueprint_exists(blueprint_id: Uuid) {}

pub fn track_blueprint_rename(id: Uuid, new_name: String) {}

pub fn assert_attribute_exists(blueprint_id: Uuid) {}

pub fn track_attribute_rename(id: Uuid, new_name: String) {}

pub fn rename_blueprint(
    db_handle: &Transaction,
    blueprint_id: Uuid,
    new_name: &str,
) -> Result<(), String> {
    println!("Renaming blueprint {} to {}", blueprint_id, new_name);
    assert_blueprint_exists(blueprint_id);

    db_handle.bind_id(blueprint_id);
    let changed_rows = db_handle.execute_sql("UPDATE `blueprints` SET `name` = ? WHERE `id` == ?");

    if changed_rows != 1 {
        println!("Invalid amount of rows changed: {}", changed_rows);
        return Err("Invalid change count in blueprint rename".to_string());
    }

    track_blueprint_rename(blueprint_id, new_name.to_string());

    Ok(())
}

pub fn rename_attribute(
    db_handle: &Transaction,
    attribute_id: Uuid,
    new_name: &str,
) -> Result<(), String> {
    println!("Renaming attribute {} to {}", attribute_id, new_name);
    assert_attribute_exists(attribute_id);

    db_handle.bind_id(attribute_id);
    let changed_rows = db_handle.execute_sql("UPDATE `attributes` SET `name` = ? WHERE `id` == ?");

    if changed_rows != 1 {
        println!("Invalid amount of rows changed: {}", changed_rows);
        return Err("Invalid change count in attribute rename".to_string());
    }

    track_attribute_rename(attribute_id, new_name.to_string());

    Ok(())
}

标签: rustduplicatescode-duplication

解决方案


我通常解决这类问题的方法是使用泛型。让调用者选择合适的类型。

trait Entity {
    fn desc(&self) -> String;
}

impl Entity for Blueprint {
    // ...
}

pub fn rename<T>(/* ... */)
where
    T: Entity,
{
    // ...
}

推荐阅读