• 【重构学习】05 函数的重构


    《重构》这本书的代码都是java,我准备用C#来一遍。

    而今天我的主要任务是写一大段垃圾代码出来,然后重构(仅限于函数的,不涉及到其它方面的重构)。

    程序界面:

    功能介绍:

    侠客名字自己取,然后点击按钮随机角色的属性,

    根骨,经脉,柔韧,悟性等四项属性值都是随机而来。

    其他的都是由这四个属性计算而来:

    根骨:影响气血,基础外攻和基础内攻

    经脉:影响内力和基础内攻

    柔韧:影响身法和基础闪避

    按钮功能的垃圾代码如下:

    /// <summary>
            /// 产生侠客
            /// </summary>
            private void btnCreateSwordsman_Click(object sender, EventArgs e)
            {
                var random = new Random();
                int boneValue, meridianValue, flexibilityValue, savvyValue;
    
                txtSurname.Text = "";
                txtShortName.Text = "大牛";
    
                boneValue = random.Next(10);
                meridianValue = random.Next(10);
                flexibilityValue = random.Next(10);
                savvyValue = random.Next(10);
    
                txtBone.Text = boneValue.ToString();
                txtMeridian.Text = meridianValue.ToString();
                txtFlexibility.Text = flexibilityValue.ToString();
                txtSavvy.Text = savvyValue.ToString();
    
                txtHP.Text = (boneValue * 20 + 20).ToString();
                txtMP.Text = (meridianValue * 10).ToString();
                txtAGI.Text = (flexibilityValue * 5 + 10).ToString();
    
                txtExteriorAttack.Text = (boneValue * 2).ToString();
                txtInsideAttack.Text = (meridianValue * 3 + boneValue*2).ToString();
                txtDodge.Text = (flexibilityValue * 1.5/100).ToString("p");
            }

    为了便于理解,所以代码很少,但是足够垃圾,让我们通过下面的学习一步步重构来吧!

      基本上关于函数的重构都是因为函数过长而引起的,有的说50行,有的说30行,有的说一个屏幕,不管怎样,别太长就好。而明显我上面的函数看起来很短,实际上是因为我偷了懒,比如命名也有随机的。(还有不要在意那些魔法数字和命名,写了一半我觉得应该写dota英雄属性的随机,我可以直接抄,因为取名真的好麻烦)

      以下所有的这些重构的例子因为代码本来就很简单,所以可能看不出明显的效果,有的时候也许让你感到莫名其妙,但是你如果把它当做一个很大的系统里的一部分,再将里面的逻辑复杂化,那么这些重构就显得很有必要了。

    1、提炼函数:将函数里的一段代码提炼出来,放到一个新的函数中,并让函数名称解释该函数的用途

    动机:如果每个函数的粒度很小,那么函数被复用的机会就更大,覆写也更容易,更高层的函数读起来就像注释。

    做法:创造一个新函数(以做什么命名,而不是怎么做),提炼代码到新函数(注意临时变量和参数)

    无局部变量的提炼函数:

     /// <summary>
            /// 产生侠客
            /// </summary>
            private void btnCreateSwordsman_Click(object sender, EventArgs e)
            {
                RandomSwordsmanName();
                RandomSwordsmanAttribute();
            }
            /// <summary>
            ///  产生一个随机的侠客名(你假装是随机好了)
            /// </summary>
            void RandomSwordsmanName() {
                txtSurname.Text = "";
                txtShortName.Text = "大牛";
            }
            /// <summary>
            /// 随机侠客的属性(按照《重构》的做法,其实这里可以不做注释,因为这些函数名已经很清楚了,注释反而是累赘)
            /// </summary>
            void RandomSwordsmanAttribute() {
                var random = new Random();
                int boneValue, meridianValue, flexibilityValue, savvyValue;
    
                boneValue = random.Next(10);
                meridianValue = random.Next(10);
                flexibilityValue = random.Next(10);
                savvyValue = random.Next(10);
    
                txtBone.Text = boneValue.ToString();
                txtMeridian.Text = meridianValue.ToString();
                txtFlexibility.Text = flexibilityValue.ToString();
                txtSavvy.Text = savvyValue.ToString();
    
                txtHP.Text = (boneValue * 20 + 20).ToString();
                txtMP.Text = (meridianValue * 10).ToString();
                txtAGI.Text = (flexibilityValue * 5 + 10).ToString();
    
                txtExteriorAttack.Text = (boneValue * 2).ToString();
                txtInsideAttack.Text = (meridianValue * 3 + boneValue * 2).ToString();
                txtDodge.Text = (flexibilityValue * 1.5 / 100).ToString("p");
            }

    有局部变量的函数提取:

    要将随机产生四个属性和其它属性的计算提取函数会涉及到临时变量的问题,一般是传参,参数很多就传对象

            /// <summary>
            /// 随机侠客的属性
            /// </summary>
            void RandomSwordsmanAttribute() {
                var basicInfo = RandomSwordsmanBasicAttribute();
                GetOtherInfoByBasicInfo(basicInfo);    
            }
            /// <summary>
            /// 随机侠客的基础属性
            /// </summary>
            /// <returns></returns>
            SwordsmanBasicInfo RandomSwordsmanBasicAttribute() {
                var basicInfo = new SwordsmanBasicInfo();
                var random = new Random();
    
                basicInfo.Bone = random.Next(10);
                basicInfo.Meridian = random.Next(10);
                basicInfo.Flexibility = random.Next(10);
                basicInfo.Savvy = random.Next(10);
                return basicInfo; 
            }
            /// <summary>
            /// 通过侠客基础属性得到其它属性,并展示出来
            /// </summary>
            /// <param name="basicInfo"></param>
            void GetOtherInfoByBasicInfo(SwordsmanBasicInfo basicInfo)
            {
                txtBone.Text = basicInfo.Bone.ToString();
                txtMeridian.Text = basicInfo.Meridian.ToString();
                txtFlexibility.Text = basicInfo.Flexibility.ToString();
                txtSavvy.Text = basicInfo.Savvy.ToString();
    
                txtHP.Text = (basicInfo.Bone * 20 + 20).ToString();
                txtMP.Text = (basicInfo.Meridian * 10).ToString();
                txtAGI.Text = (basicInfo.Flexibility * 5 + 10).ToString();
    
                txtExteriorAttack.Text = (basicInfo.Bone * 2).ToString();
                txtInsideAttack.Text = (basicInfo.Meridian * 3 + basicInfo.Bone * 2).ToString();
                txtDodge.Text = (basicInfo.Flexibility * 1.5 / 100).ToString("p");
            }
            /// <summary>
            /// 侠客基础属性
            /// </summary>
            public class SwordsmanBasicInfo
            {
                /// <summary>
                /// 根骨
                /// </summary>
                public int Bone { get; set; }
                /// <summary>
                /// 经脉
                /// </summary>
                public int Meridian { get; set; }
                /// <summary>
                /// 柔韧
                /// </summary>
                public int Flexibility { get; set; }
                /// <summary>
                /// 悟性
                /// </summary>
                public int Savvy { get; set; }
            }    

    然而这还不够,用函数取代一些的表达式:

         /// <summary>
            /// 通过侠客基础属性得到其它属性,并展示出来
            /// </summary>
            /// <param name="basicInfo"></param>
            void GetOtherInfoByBasicInfo(SwordsmanBasicInfo basicInfo)
            {
                txtBone.Text = basicInfo.Bone.ToString();
                txtMeridian.Text = basicInfo.Meridian.ToString();
                txtFlexibility.Text = basicInfo.Flexibility.ToString();
                txtSavvy.Text = basicInfo.Savvy.ToString();
    
                txtHP.Text = GetHP(basicInfo.Bone).ToString();
                txtMP.Text = GetMP(basicInfo.Meridian).ToString();
                txtAGI.Text = GetAGI(basicInfo.Flexibility).ToString();
    
                txtExteriorAttack.Text = GetExteriorAttack(basicInfo.Bone).ToString();
                txtInsideAttack.Text = GetInsideAttack(basicInfo.Meridian ,basicInfo.Bone).ToString();
                txtDodge.Text = GetDodge(basicInfo.Flexibility).ToString("p");
            }
            int GetHP(int bone) {
                return bone * 20 + 20;
            }
            int GetMP(int meridian)
            {
                return meridian * 10;
            }
            int GetAGI(int flexibility)
            {
                return flexibility * 5 + 10;
            }
            int GetExteriorAttack(int bone)
            {
                return bone * 2;
            }
            int GetInsideAttack(int bone, int meridian)
            {
                return meridian * 3 + bone * 2;
            }
            float GetDodge(int flexibility)
            {
                return flexibility * 1.5f / 100;
            }

    .NET还有更好玩的dynamic玩法:

         /// <summary>
            /// 通过侠客基础属性得到其它属性,并展示出来
            /// </summary>
            /// <param name="basicInfo"></param>
            void GetOtherInfoByBasicInfo(SwordsmanBasicInfo basicInfo)
            {
                SetTextBoxValue(txtBone,basicInfo.Bone);
                SetTextBoxValue(txtMeridian, basicInfo.Meridian);
                SetTextBoxValue(txtFlexibility, basicInfo.Flexibility);
                SetTextBoxValue(txtSavvy, basicInfo.Savvy);
    
                SetTextBoxValue(txtHP, GetHP(basicInfo.Bone));
                SetTextBoxValue(txtMP, GetMP(basicInfo.Meridian));
                SetTextBoxValue(txtAGI, GetAGI(basicInfo.Flexibility));
    
                SetTextBoxValue(txtExteriorAttack, GetExteriorAttack(basicInfo.Bone));
                SetTextBoxValue(txtInsideAttack, GetInsideAttack(basicInfo.Meridian, basicInfo.Bone));
    
                SetTextBoxPercentValue(txtDodge, GetDodge(basicInfo.Flexibility));
            }
            void SetTextBoxValue(TextBox textBox, dynamic num)
            {
                textBox.Text = num.ToString();
            }
            void SetTextBoxPercentValue(TextBox textBox, dynamic percent)
            {
                textBox.Text = percent.ToString("p");
            }

    当然这仍然不够,

    GetHP之类的函数可以放到SwordsmanBasicInfo类里,整个代码在功能上实际上是分为计算和显示两个逻辑,有必要将计算属性,和最后的显示属性提取成不同的函数放在类里

    但是这里只是单纯为了举几个例子来说明函数的提取重构而已,所以也就没必要继续弄了,这段垃圾代码就留到后面继续重构吧。

    2、去函数

    动机:一个函数的本体与名称同样清晰了

    做法:用函数本体去取代函数

    例子就不举了,太简单,不过这种做法的意义通常不用于描述所说的那样的动机,实际上是因为你将这些函数去掉,

    说不定可以发现两个这种函数去函数化后,说不定可以进行逻辑更清晰,意义更明确的函数提炼。

     3、去临时变量

    动机:有一个临时变量,只被一个简单表达式赋值了一次,而它妨碍了其他重构手法

    做法:则将所有对该变量的引用动作,替换为对它赋值的那个表达式本身

    对于一大段很艰涩的代码,你可以把这个临时变量加上const,以确定它确实只被赋值一次,当然实际上VS的CTRL+F已经很清晰了  

    4、以函数取代临时变量

    这一步相当于去临时变量的一个扩展,就是把临时变量替换成一个简单表达式后再用函数放起来。

    (参考:在提取函数的代码里面GetHP那一系列函数就是,不过我写的代码只是反映了可读性,

    实际上如果这个函数是放在类里面,那么所有的函数就都可以访问这个东西,而不是仅有临时变量所在的那个函数,.NET的玩法你也可以考虑放在属性里面用get,set这种就酷多了)

    在这里你也许会认为无用,因为我用个临时变量不是挺好的吗,干嘛不改为函数呢,但事实上我在提取函数的代码里的那一部分重构确实代码好看多了。

    说到底我们的代码不仅是要写给机器看的,也是写给人看的,你要考虑到终究会有一个同行来读,为了大家下班快点,搭把手。

    你也会提到这会不会有性能上的问题,因为一个表达式我给临时变量只要计算一次,但是我用表达式可能就要计算N次。(记住这里的表达式是简单表达式)

    说实话这个问题我也想到过,但是Martin大神解释道:这通常不会影响到性能,因为大多也就几次调用而已,

    就像之前说的不要去揣测性能问题,到了性能优化阶段再去考虑,如果那个时候真的有性能问题,一个良好的重构过得程序也许能让你发现更好地优化方案,实在不行,你改成临时变量也会很容易。

    5、引入解释性变量

    动机:当你有一个复杂的表达式的时候,又是&&又是||的什么判断什么的一大串的时候(我相信大家都遇到过),然后存在大量局部变量,用函数不好提取时

    做法:那么你可以去把这个复杂的表达式中的部分值放入一个临时变量,然后用变量名称来解释表达式的用途

    6、临时变量分解

    动机:某个临时变量被赋值一次以上,它既不是循环变量,也不是用于收集计算结果(毕竟高级语言不是C,C的声明要写在开头,我喜欢直接用一个临时变量,但是高级语言可是可以到处声明的)

    做法:好吧,针对每个赋值,创造一个独立、对应的临时变量(就是单一职责,不仅仅是函数,一个变量的意义也要单一职责)

    7、移除对参数的赋值

    动机:代码对一个参数赋值,降低了代码的清晰度

    做法:以一个临时变量取代该参数的位置,当然引用参数不必遵循这一原则,就是那些引用类型的变量和ref这种,但是ref什么的Martin不建议多用。(好吧,仁者见仁,智者见智,自己权衡吧)

    实际上除此之外的写法都会降低可读性,不如新建个临时变量去处理,这点可怜的性能就不需要考虑了吧,话说咱写的不是C啊,可读性和性能什么的,具体情况具体分析,自己权衡吧。

    8、以函数对象取代函数

    好吧,你看到函数对象的时候可能觉得这个东西碉堡了,一个函数作为对象,是js吗?

    实际上并不是,Martin的函数对象是指一个有函数的对象(我这样解释是不是很清楚,然后一下从高大上变得很low?)

    动机:为了解决大型函数的重构时,其中泛滥成灾的局部变量的使用让你无法用提取函数的情况

    做法:将这个函数放进一个单独对象里面,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解成多个小型函数。

    就是说你去新建一个类,然后把这个函数要重构的函数放进去,然后局部变量都转为字段。(好聪明的做法)

    9、替换算法

    动机:你觉得你的哪段代码写的很烂

    做法:那么你就换一种更清晰的写法写出来

    好吧,这个不存在什么问题吧,我已经说的很清晰了,你所欠缺的只是壮士断腕的勇气,

    当然首先你要记得提取函数哦,这样你断腕的时候不至于砍错了地方。

  • 相关阅读:
    (二)Knockout 文本与外观绑定
    Knockout案例: 全选
    (一)Knockout 计算属性
    打造Orm经典,创CRUD新时代,Orm的反攻战
    让我们开启数据库无Linq、零sql时代
    EF总结
    高性能Web系统设计方案(初稿目录),支持者进
    Bootstrap+angularjs+MVC3+分页技术+角色权限验证系统
    .NET 2.0 检测
    C# 用代码创建 DataSet 和 DataTable 的列和记录
  • 原文地址:https://www.cnblogs.com/vvjiang/p/5095749.html
Copyright © 2020-2023  润新知