编写可维护软件的不朽代码随想-4

本文探讨了在软件开发中避免复制代码的重要性,指出重复代码可能导致维护困难和错误传播。通过实例展示了如何将重复代码抽取到可重用的方法或父类中,以实现代码复用和模块化。同时,文章提到了复制代码的潜在问题,如失去版本控制的好处和增加不必要的维护工作。最后,建议使用版本控制系统和单元测试来辅助管理和发现代码变更。
摘要由CSDN通过智能技术生成

不写重复代码

不要复制代码

编写可重用的、通用的代码,调用已有的代码

如果复制代码,就需要在多个地方修复BUG。

举例

一个管理银行账户的系统,通过Transfer类对象来表示钱在不同账户之间的流转过程。

CheckingAccount表示银行提供的支票账户 ,如下

public class CheckingAccount
    {
        private int transferLimit = 100;
        public Transfer MakeTransfer(string counterAccount, Money amount)
        {
            //1 检查提款限额
            if (amoiunt.GreaterThan(this.transferLimit))
            {
                throw new BusinessException("Limit exceeded!");
            }
            //2 假设结果是一个9位数的银行账户号码,使用11-test进行验证
            int sum = 0;
            for (int i = 0; i < counterAccount.Length; i++)
            {
                sum += (9 - i) * (int)char.GetNumericValue(counterAccount[i]);
            }
            if (sum % 11 == 0)
            {
                //3 查找对方账户并创建transfer对象
                CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
                Transfer result = new Transfer(this, acct, amount);
                return result;
            }
            else
            {
                throw new BusinessException("Invalid account number!");
            }
        }
    }

根据对方的账户号码,MakeTransfer方法会创建一个Transfer对象,首先检查转账金额有没有超过指定限额,然后检查对方账户号码是否有效,然后就获取到表示该账户的对象,然后创建一个Transfer对象。

如果银行新增了一种账户类型,比如储蓄账户,它没有转账限额,但是有另一个限制:钱只能转给一个指定的支票账户。为此需要创建一个新类,这里是复制CheckingAccount并重命名和一些调整

public class SavingAccount
    {
        CheckingAccount registeredCounterAccount;
        public Transfer MakeTransfer(string counterAccount, Money amount)
        {
            //1 假设结果是一个9位数的银行账户号码,使用11-test进行验证
            int sum = 0;
            for (int i = 0; i < counterAccount.Length; i++)
            {
                sum += (9 - i) * (int)char.GetNumericValue(counterAccount[i]);
            }
            if (sum % 11 == 0)
            {
                //2 查找对方账户并创建transfer对象
                CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
                Transfer result = new Transfer(this, acct, amount);
                //3 检查提款方是否是已注册的对方账户
                if (result.getCounterAccount().Equals(this.registeredCounterAccount))
                {
                    return result;
                }
                else
                {
                    throw new BusinessException("Counter-account not registered!");
                }
            }
            else
            {
                throw new BusinessException("Invalid account number!");
            }
        }
    }

假设我们在验证银行账户号码的地方有BUG,那么需要对以上2个类都进行修改,这样不仅增加了额外的工作,也让维护变得低效。

 

重复代码定义是,一段至少6行都相同的代码(不包括空行和注释),也称为1类克隆,与出现的位置无关。

在语法上相同的两段代码称为2类克隆,只是在空格,注释,标识符名称和字面值方面不同。原则适用于1类克隆。

这个6行代码的限制是根据SIG的经验,正好达到一个平衡点,避免识别的重复代码过多或过少,举例,一个ToString()方法可能有3或4行代码,这些代码可能出现在许多的领域对象中,这样的重复代码可以忽略。

 

重复代码更加难以分析:遇到问题肯定是希望去解决它,解决的一个要点是对问题的定位,当你调用一个已有方法时,你可以很容易找到源代码,而当你复制代码时,问题的根源可能还存在与其他地方。

代码重复更加难以修改:如果重复的代码中含有一个BUG,那么相同的BUG就会出现多次,常规的修改也存在同样的问题,需要修改多处。

 

如何使用原则

将重复的代码提取到一个方法中,对于不同类之间的重复代码,可以将提取方法放到一个工具类中,使用静态方法。

public class Accounts
    {
        public static bool IsValid(string number)
        {
            int sum = 0;
            for (int i = 0; i < number.Length; i++)
            {
                sum += (9 - i) * (int)char.GetNumericValue(number[i]);
            }
            return sum % 11 == 0;
        }
    }

那么在CheckingAccount类变为:

public class CheckingAccount
    {
        private int transferLimit = 100;
        public Transfer MakeTransfer(string counterAccount, Money amount)
        {
            //1 检查提款限额
            if (amoiunt.GreaterThan(this.transferLimit))
            {
                throw new BusinessException("Limit exceeded!");
            }
            //2 假设结果是一个9位数的银行账户号码,使用11-test进行验证
            if (Accounts.IsValid(counterAccount))
            {
                //3 查找对方账户并创建transfer对象
                CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
                Transfer result = new Transfer(this, acct, amount);
                return result;
            }
            else
            {
                throw new BusinessException("Invalid account number!");
            }
        }
    }

同样,在SavingAccount类变为:

public class SavingAccount
    {
        CheckingAccount registeredCounterAccount;
        public Transfer MakeTransfer(string counterAccount, Money amount)
        {
            //1 假设结果是一个9位数的银行账户号码,使用11-test进行验证
            if (Accounts.IsValid(counterAccount))
            {
                //2 查找对方账户并创建transfer对象
                CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
                Transfer result = new Transfer(this, acct, amount);
                //3 检查提款方是否是已注册的对方账户
                if (result.getCounterAccount().Equals(this.registeredCounterAccount))
                {
                    return result;
                }
                else
                {
                    throw new BusinessException("Counter-account not registered!");
                }
            }
            else
            {
                throw new BusinessException("Invalid account number!");
            }
        }
    }

此时重复代码已消失,但存在以下问题:

1. 重复代码已经不存在,但是两个类中仍然存在相同的逻辑。

2. 由于C#的方法必须在类中,不得不将提取出来的代码放在另一个类中,这个类很快会变成一堆无关方法的大杂烩,从而导致体积巨大并且耦合过紧,类中有太多无关的提供各种功能的方法,这样导致其他方法为了使用这个巨大的类,必须知道其实现细节,造成互相之间的紧耦合。

解决以上问题,提取父类,这个重构技巧,不只是将代码行片段提取成方法,而且提取到原类的一个新的父类中。

public class Account
    {
        public virtual Transfer MakeTransfer(string counterAccount, Money amount)
        {
            //1 假设结果是一个9位数的银行账户号码,使用11-test进行验证
            int sum = 0;
            for (int i = 0; i < counterAccount.Length; i++)
            {
                sum += (9 - i) * (int)char.GetNumericValue(counterAccount[i]);
            }
            if (sum % 11 == 0)
            {
                //2 查找对方账户并创建transfer对象
                CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
                Transfer result = new Transfer(this, acct, amount);
                return result;
            }
            else
            {
                throw new BusinessException("Invalid account number!");
            }
        }
    }

这个父类包括两种特殊账户所共享的逻辑,下面是2个子类

public class CheckingAccount : Account
    {
        private int transferLimit = 100;
        public override Transfer MakeTransfer(string counterAccount, Money amount)
        {
            //1 检查提款限额
            if (amoiunt.GreaterThan(this.transferLimit))
            {
                throw new BusinessException("Limit exceeded!");
            }
            return base.MakeTransfer(counterAccount, amount);
        }
    }
public class SavingAccount : Account
    {
        CheckingAccount RegisteredCounterAccount { get; set; }
        public override Transfer MakeTransfer(string counterAccount, Money amount)
        {
            Transfer result = base.MakeTransfer(counterAccount, amount);
            //3 检查提款方是否是已注册的对方账户
            if (result.getCounterAccount().Equals(this.RegisteredCounterAccount))
            {
                return result;
            }
            else
            {
                throw new BusinessException("Counter-account not registered!");
            }
        }
    }

对于上面的父类,还可以把校验账号的部分提取方法,如下:

public class Account
    {
        public virtual Transfer MakeTransfer(string counterAccount, Money amount)
        {
            //1 假设结果是一个9位数的银行账户号码,使用11-test进行验证
            if (IsValid(counterAccount))
            {
                //2 查找对方账户并创建transfer对象
                CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
                Transfer result = new Transfer(this, acct, amount);
                return result;
            }
            else
            {
                throw new BusinessException("Invalid account number!");
            }
        }
        public static bool IsValid(string number)
        {
            int sum = 0;
            for (int i = 0; i < number.Length; i++)
            {
                sum += (9 - i) * (int)char.GetNumericValue(number[i]);
            }
            return sum % 11 == 0;
        }
    }

常见反对意见:

应该允许直接从其他代码库复制代码从其他代码库里复制代码不是问题,因为没有给当前系统带来重复代码。

如果这段代码解决了相同环境下的相同问题,复制代码看上去是能带来好处的,但是如果存在一下任何一种情况,都会遇到麻烦:

1. 另一个代码库仍在维护中,只是将代码复制过来,将无法获得后续对原有代码的改进。

2. 如果另一个代码库不再维护,那么你就是在重写这个代码库。通常只有在遇到可维护性问题或者技术更新时,才会考虑重写已有代码。如果是可维护性差,那么重写计划可能会受到复制代码的影响,因为你引入了确定难以维护的代码。如果是技术更新,可能将旧技术的局限性带到新的代码库中来,例如无法使用所需的抽象概念而导致无法有效地重用方法。

由于细微修改而导致的代码重复是不可避免的

系统经常会包含一些略微不同的通用方法,例如,有些方法只是针对不同的操作系统,不同版本或者不同用户组略有不同,但是这并不意味着重复代码是不可避免,你需要找到所有这些代码中相同的部分,然后将它移到一个通用的父类,应该努力去区分代码的不同之处,保证他们是清晰的,独立的并且是可测试的。 

这些代码永远不会发生变化

现实情况大部分系统会由于多种原因发生变化,系统的功能性需求可能因为用户群体的改变、使用行为的改变、组织业务的改变而发生改变。组织可能会在所有权、职责范围、开发方式、开发流程或者法律要求方面发生改变。系统所处环境中的技术也可能改变,如操作系统、第三方库、框架或者与其他应用程序之间的接口。代码自身也可能有BUG、重构甚至界面优化而发生改变。

拷贝整个文件相当于多了一个备份,建立保留备份,Git这样的版本控制系统提供了很好的而备份机制,不要把备份文件放在代码库里面,以后会分不清哪个文件才是真正使用的。

单元测试会帮助我们发现问题,这只有当重复代码处于相同的方法中,并且单元测试能够覆盖这两段代码时才成立。如果重复代码位于其他方法中,就只能依赖代码分析程序来检查是否发生了变化。否则,只是重复代码发生了变化,单元测试不一定会提示失败。因此,不能只依赖单元测试的结果而不去寻找问题的根源。不能认为最后所有的问题都会在后续开发过程中得以解决,于是现在就可以对它们视而不见。

字符串文本的重复不可避免并且是无害的,我们经常在C#代码中看到,以字符串形式出现的长SQL查询、XML或者HTML文档,有些时候他们是完全相同的,但更多时候是其中某些部分不断重复地出现,例如,长达几百行的SQL查询,只是在排序顺序上有所不同,虽然这里重复代码都不算C#本身的逻辑,但它们依然有害,事实上解决这类重复代码的方法很简单:将字符串提取成一个方法,使用字符串追加和参数方式来处理变量。使用模板引擎,通过体积更小的、没有重复代码的多个文件来生成HTML内容。

 

SIG评估重复代码

评估重复代码,除了完全是import语句的重复代码以外,将所有6行以上的1类代码计算在内,然后将这些重复代码分为两个风险分类:多余重复代码非多余重复代码。例如,代码库中出现了3次10行代码的重复,其中2个是可以去掉的,就都被认为是多余的,因此i行代码是多余重复代码,另一段重复代码被认为是非多余的,要想被评为4星,多余代码占比最多是4.6%

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值