Refactor 重複的程式碼 & If巢狀結構 燒毀之術

主要目的教大家重構的技巧

1.重複的程式碼duplicated code

2.If的巢狀結構

寫這篇文章的動機,主要是無聊逛specflow的一篇問題,覺得很有趣所以想來寫一下

http://stackoverflow.com/questions/8417145/best-practice-for-refactoring-methods

簡述一下問題,作者想要重構以下這段程式碼。

    private static bool Foo()
    {
        bool result = false;

        if (DoMehod1())
        {
            if (DoMehod2())
            {
                if (DoMethod3())
                {
                    result = true;
                }
                else
                {
                    Console.WriteLine("DoMethod3 Failed");
                }
            }
            else
            {
                Console.WriteLine("DoMethod2 Failed");
            }
        }
        else
        {
            Console.WriteLine("DoMethod1 Failed");
        }
        return result;
    }

因為原作者字其實有打錯,先做修正

我先把這個Function移到MethodClass 。

為了測試方便,同時不影響我們要重構的目的。我作了以下三點修正

1.我把宣告 Foo()為public

2.同時因為Foo() 有Console.WriteLine()這個靜態方法 也為了測試方便,支援讓Foo()回傳錯誤訊息,如果成功則回傳"Sucess!"

  Foo()最後回傳一個FooData的一個Data Object

3.實作DoMethod1,DoMethod2,DoMethod3並宣告為protected virtual method(為了之後編寫單元測試)

修正後程式碼如下

   class FooData
    {
        public bool Result;
        public string Message;
    }
    class MethodClass
    {
        public FooData Foo()
        {
            bool result = false;
            string message = "Sucess";
            if (DoMethod1())
            {
                if (DoMethod2())
                {
                    if (DoMethod3())
                    {
                        result = true;
                    }
                    else
                    {
                        message = "DoMethod3 Failed";
                    }
                }
                else
                {
                    message = "DoMethod2 Failed";
                }
            }
            else
            {
                message = "DoMethod1 Failed";
            }
            return new FooData() { Result = result, Message = message };
        }

        protected virtual bool DoMethod1()
        {
            return true;
        }
        protected virtual bool DoMethod2()
        {
            return true;
        }
        protected virtual bool DoMethod3()
        {
            return true;
        }
    }

接著我們來建立測試程式碼!測試為重構之本!

1.我建立一個MockObject Class:MockMethodClass,同時建立它的Constructor 來初始化 Method1~3的Result 1~3是True or False

public MockMethodClass(bool result1, bool result2, bool result3)
{
    Method1Result = result1;
    Method2Result = result2;
    Method3Result = result3;
}

2.為了好一點的可讀性,我把 TestCase分成四種

   1.Method1 Fail

   2.Method2 Fail

   3.Method3 Fail

   4.All Method Sucess

以下是完整的測試程式碼,使用Nunit 2.6.4版本

using NUnit.Framework;
using Refactor_Section1;

namespace Refactor_Section1UnitTest
{
    [TestFixture]
    public class MethodClassTest
    {

        [TestCase(false, true, true, "DoMethod1 Failed")]
        [TestCase(false, true, false, "DoMethod1 Failed")]
        [TestCase(false, false, true, "DoMethod1 Failed")]
        [TestCase(false, false, false, "DoMethod1 Failed")]
        public void Foo_Method1Fail_ShouldMatchMessage(bool result1, bool result2, bool result3, string expected)
        {
            MockMethodClass mockMethodClass = new MockMethodClass(result1, result2, result3);
            var fooData = mockMethodClass.Foo();
            Assert.AreEqual(false, fooData.Result);
            Assert.AreEqual(expected, fooData.Message);
        }

        [TestCase(true, false, true, "DoMethod2 Failed")]
        [TestCase(true, false, false, "DoMethod2 Failed")]
        public void Foo_Method2Fail_ShouldMatchMessage(bool result1, bool result2, bool result3, string expected)
        {
            MockMethodClass mockMethodClass = new MockMethodClass(result1, result2, result3);
            var fooData = mockMethodClass.Foo();
            Assert.AreEqual(false, fooData.Result);
            Assert.AreEqual(expected, fooData.Message);
        }

        [TestCase(true, true, false, "DoMethod3 Failed")]
        public void Foo_Method3Fail_ShouldMatchMessage(bool result1, bool result2, bool result3, string expected)
        {
            MockMethodClass mockMethodClass = new MockMethodClass(result1, result2, result3);
            var fooData = mockMethodClass.Foo();
            Assert.AreEqual(false, fooData.Result);
            Assert.AreEqual(expected, fooData.Message);
        }

        [TestCase(true, true, true, "Sucess")]
        public void Foo_AllMethodSucess_ShouldMatchMessage(bool result1, bool result2, bool result3, string expected)
        {
            MockMethodClass mockMethodClass = new MockMethodClass(result1, result2, result3);
            var fooData = mockMethodClass.Foo();
            Assert.AreEqual(true, fooData.Result);
            Assert.AreEqual(expected, fooData.Message);
        }

        class MockMethodClass : MethodClass
        {
            public bool Method1Result = true;
            public bool Method2Result = true;
            public bool Method3Result = true;

            public MockMethodClass(bool result1, bool result2, bool result3)
            {
                Method1Result = result1;
                Method2Result = result2;
                Method3Result = result3;
            }
            protected override bool DoMethod1()
            {
                return Method1Result;
            }
            protected override bool DoMethod2()
            {
                return Method2Result;
            }
            protected override bool DoMethod3()
            {
                return Method3Result;
            }
        }
    }
}

當然全部通過測試XD

搞定測試後~我們終於可以開始Refactor了!

首先,這個Foo()方法 主要的邏輯 是由Method1->Method2->Mthod3一層一層下去判斷,只要其中一個Method失敗,便assign Message 並且結果為flase ,之後就不用再走下一個方法判斷。

這個if else的巢狀結構要是日後有新Feature再發展下去實在有點驚人~嚇屬寶寶了

所以為了對付這段程式碼,我們可以使用反向條件敘述,讓結構變單純。作法如下

        public FooData Foo()
        {
            if (!DoMethod1())
            {
                return new FooData() { Result = false, Message = "DoMethod1 Failed" };
            }

            if (!DoMethod2())
            {
                return new FooData() { Result = false, Message = "DoMethod2 Failed" };
            }

            if (!DoMethod3())
            {
                return new FooData() { Result = false, Message = "DoMethod3 Failed" };
            }

            return new FooData() { Result = true, Message = "Sucess" };
        }

喔耶~爽拉~經過這種反向敘述,巢狀結構燒毀囉~!變成3個if+return 的物件的形式。

同時我有再inline variable讓程式碼變得更簡潔。(當然~測試都要通過才算重構無誤喔~哥當然是成功的XDDDD)

基本上重構到這一步已經算是OK了,但身為Coder,為了Coder的魂,沒有最好只有更好~還有一大塊duplicated code可以改善!

首先,我們可以發現,每個if內部都長得很像,只是Message不太一樣而已,我們可以嘗試用Extract Method去擷取相同的程式碼架構試試看會如何

來囉,經過這個想法,程式碼如下:

        public FooData Foo()
        {
            if (!DoMethod1())
            {
                return HandleUnsucessFoo("DoMethod1 Failed");
            }

            if (!DoMethod2())
            {
                return HandleUnsucessFoo("DoMethod2 Failed" );
            }

            if (!DoMethod3())
            {
                return HandleUnsucessFoo("DoMethod3 Failed");
            }

            return new FooData() { Result = true, Message = "Sucess" };
        }

        private FooData HandleUnsucessFoo(string msg)
        {
            return new FooData() { Result = false, Message = msg };
        }

提取一個Method 叫做HandleUnsucessFoo() 專門回傳失敗FooData物件

最後仍然還是有重複的程式碼,我們要用迴圈結構來消除,作法如下

        public FooData Foo()
        {
            foreach (var methodInfo in GetMethodInfos())
            {
                if (!methodInfo.Value)
                {
                    return HandleUnsucessFoo(methodInfo.Key);
                }
            }
            return new FooData() { Result = true, Message = "Sucess" };
        }

        private Dictionary<string, bool> GetMethodInfos()
        {
            return new Dictionary<string, bool>()
            {
                {"DoMethod1 Failed", DoMethod1()},
                {"DoMethod2 Failed", DoMethod2()},
                {"DoMethod3 Failed", DoMethod3()},
            };
        }

1.我把Foo要做的Methods跟該Method的Message先弄成一個Dictionary的資料結構

(我承認偷懶了 key跟value反著用,為了不讓key重複產生,而且程式碼比較難理解,比較好的做法是產生一個MethodInfo的物件來接值但.....我懶得改了),

好讓之後可以付用同樣的程式碼結構

2.同時把這段擷取成一個Method:GetMethodInfos()

最後跑一下測試,PASS~於是重構終於完成!!跟原本的程式碼是不是十萬八千里呢!其實也沒想像中複雜。

重構後程式碼變得更加好理解,而且有單元測試保護,不怕改錯程式碼邏輯,假使以後Foo要加其他Method4,5,6,.....也都只要在MethodInfo加上新的MethodInfo,就可以拉

爽拉~終於打完這篇了~時間一下子就噴了QQ

希望大家有收穫囉~我們下回見囉~(OS:上一篇是一年前...)~