重構學習筆記(9)-大話重構第九章-第五步:降低程式依賴度-筆記

重構

遺留系統重構目的:容易閱讀、容易維護、容易變更。

做到這樣目的:降低系統的耦合度,就是系統的各個功能之間的相互依賴程度,這樣就會跟電腦主機的設備一樣可插拔,哪邊變動就改哪,哪邊壞就換新。

介面與實作與工廠模式

步驟:

  1. 程式依功能分解到一個一個子函數
  2. 分拆成一個一個物件
  3. 一步一步統一相關物件格式
  4. 提取介面

目的:各個功能能夠像元件存在於系統中,介面就是插槽,實作類別就是功能模組。

以PC電腦主機板為例,除了插槽與功能模組還要有主機板,來組織這些插槽,在軟體系統扮演主機板角色的物件就是運用【工廠模式】建立出來的【工廠類別】。

關於工廠模式和依賴反轉原則

放在工廠中的物件,叫做產品,每一個都有自己一個產品名稱,這麼名稱時唯一,例如:BeanFactory中的Id,透過產品名稱,可以輕易找到該產品,每個工廠都有一個方法為它的客戶程式提供工廠中的產品。

工廠模式分成:簡單工廠、工廠方法、抽象工廠。

在物件導向來說引入介面目的:寫程式能夠足夠內聚,它業務邏輯不要被更底層實體實作給耦合,這就是【依賴反轉】。

依賴反轉

  • 高層次模組不應該依賴低層次模組,兩者應該依賴抽象介面
  • 抽象介面不應該依賴具體實作,具體實做應該依賴抽象介面

靠近人機端互動是高層次,硬體設備端就是低層次,人機互動端是我們理解的業務操作,靠近硬體端,則是硬體操作。

例如:開發發票業務類別,上層呼叫是使用者請求端,下層是DB存取類別。

現在我們設計一個保存發票的業務方法,對發票資料檢驗,業務處理後,保存資料庫,對於這個類別,我們本來期望它保存任何類型資料庫中的低層次資料存取類別,這樣結果是我們開發票業務類別,只能保存在ORACLE中,使用該類別執行保存,因此開發票業務類別依賴於這個低層次資料存取類別。

在DIP原則以前,高層次依賴於低層次很普及,帶來的問題:高層次模組無法使用到其他地方。

如何解決?

開發票業務類別在保存資料庫時不是呼叫那個具體的資料庫存取類別,而是一個抽象介面,系統運行再決定呼叫那個具體類別,那個問題就解決,而是一個抽象介面,系統運行再決定那個具體類別,這問題就解決,這就是【兩者都應該依賴抽象介面】。

上圖開發票業務類別直接呼叫資料存取介面,呼叫save(),開發票業務類別只依賴資料存取介面,具體時做類別必須實作資料存取介面規定方法,例如save(),也就是【所有實作類別都依賴介面】。

範例一

在ERP軟體中有各式各樣單據需要產生財務憑證、包含應付憑單、付款憑單、核銷單、調匯單、薪資單…等等,不同單據別的財務憑證規則不一樣,但它們都有一些共同特徵,只是各自產生分錄規則不一樣,因此抽取一個分錄產生介面各自實作去,不同合併策略的程式設計也是不一樣的,但它們是合併功能,因此都是合併分錄介面實作。

具體實作必須將實作類別配置在工廠上,在CreateVoucherBus設計createEntrites與mergeEntries屬性,並且設計出Map放入createEntries與mergeEntries介面中的實作類別。

範例二

客戶原有程式提出三種資料匯出方式:全部匯出、選擇匯出、匯出本頁,之後提出新需求,按頁匯出,使程式符合OCP原則,圖上設計Exporter介面和它三個實作類別ExportAll、ExportChoose、ExportOnePage,後來新增ExportPageRange完成四個設計之後,應該把ExportBus做成工廠,將四個實作類別透過配置動態載入進去。

Adapter-轉接器模式與介面-外部系統解耦

在遺留系統當中會常遇到一個問題就是跟外部系統耦合,既然存在耦合,就是解耦,當遇到這種耦合的時候,最常使用的就是Adapter模式。

 

    class Program
    {
        private static void Main(string[] args)
        {
            Target target = new Adapter();
            target.Request();
        }
    }

    class Target
    {
        public virtual void Request()
        {
            Console.WriteLine("Called Target Request()");
        }
    }

    class Adapter : Target
    {
        private Adaptee adaptee = new Adaptee();

        public override void Request()
        {
            adaptee.SpecificRequest();
        }
    }

    class Adaptee
    {
        public void SpecificRequest()
        {
            Console.WriteLine("Called SpecificRequest()");
        }
    }

上述有幾個角色:

  1. Target:專門給Clinet使用的特定領域的介面。
  2. Clinet:符合Target的介面對象。
  3. Adaptee:定義一個已經存在的介面,這個介面需要被轉接。
  4. Adapte:轉接器的核心,專門轉接給Adaptee角色已有接口轉換成目標角色的Target匹配介面,對Adaptee的介面與Target的介面轉接。

筆者曾經在實務上外部系統依賴於Dll,這時候可以用Adapter的方式來解耦合

public class MathDll
{
    [DllImport("Math.dll")]
    public static extern int Add(int a,int b);
}

interface IMathDllAdapter
{
    int Add(int a,int b);
}

public class MathDllAdapter:IMathDllAdapter
{
    public int Add(int a, int b)
    {
        return MathDll.Add(a,b);
    }
}

關於Adapter:透過轉換將兩個不相容的介面接在一起。

它首先讓系統程式呼叫轉接器介面,而不是呼叫外部系統介面,然後讓轉接器實作類別呼叫外部介面。

轉接器的實作類別實現一個轉換,它讓原本不相容的介面,透過轉換轉接再一起,這個過程就像生活當中的兩孔插頭無法插在三孔的插座上,有一個轉接器,讓兩孔插座可以插在轉接器上,讓轉接器插在三孔插座上,問題就解決了。

轉接器的核心在於它的介面和實作類別,當內部系統需要外部系統,並不是呼叫外部系統對應介面和方法,而是呼叫轉接器的介面,對於內部系統它並不知道,也不關心外部系統是怎麼樣,它們只知道跟外部系統介面銜接的轉接器介面長怎麼樣,因為轉接器介面是自己設計的,可以掌握它,它不會因為外部系統變化而變化,當外部系統出現變化,只需調整轉接器與外部系統的介面實作。

只需知道我們銜接是哪一個外部系統以及該外部系統銜接是哪一個實作類別。

氾濫的繼承與橋接模式

當熟悉抽象跟繼承的使用來改善程式結構,隨者時間頻繁使用會遇到一個問題就是繼承範例問題。

橋接模式:最核心的思想是兩種變化形成的繼承改寫為組合,將原有的強關聯變成弱關聯,進而實現兩種變化的相互解耦。

When:

        A
     /     \
    Aa      Ab
   / \     /  \
 Aa1 Aa2  Ab1 Ab2

Refactor to:

     A         N
  /     \     / \
Aa(N) Ab(N)  1   2

簡易例子如下:

 

    class Program
    {
        static void Main(string[] args)
        {
            Abstraction ab = new Abstraction();
            ab.Implementor = new ConcreateImplemetorA();
            ab.Operation();

            ab.Implementor = new ConcreateImplemetorB();
            ab.Operation();
            Console.ReadKey();
        }
    }
    class Abstraction
    {
        protected Implementor implementor;

        public Implementor Implementor
        {
            set { implementor = value; }
        }

        public virtual void Operation()
        {
            implementor.Operation();
        }
    }

    abstract class Implementor
    {
        public abstract void Operation();
    }

    class RefinedAbstraction : Abstraction
    {
        public override void Operation()
        {
            implementor.Operation();
        }
    }

    class ConcreateImplemetorA : Implementor
    {
        public override void Operation()
        {
            Console.WriteLine("ConcreateImplementorA.Operation");
        }
    }
    class ConcreateImplemetorB : Implementor
    {
        public override void Operation()
        {
            Console.WriteLine("ConcreateImplementorB.Operation");
        }
    }


橋接模式職責如下:

抽象化(Abstraction)角色:抽象化給出的定義,並保存一個對實體物件的引用。

修正抽象化(Refined Abstraction)角色:擴展抽象化角色,改變和修正老爸對抽象化的定義。

實現化(Implementor)角色:這個角色給出實現化角色的介面,但不給出具體的實現。必須指

出的是,這個介面不一定和抽象化角色的介面定義相同,實際上,這兩個介面可以非常不一樣。

實現化角色應當只給出底層操作,而抽象化角色應當只給出基於底層操作的更高一層的操作。

具體實現化(Concrete Implementor)角色:這個角色給出實現化角色介面的具體實作。

以下有一個實例,如何透過實例來重構解決,發揮橋接模式特性

我寫了一個閱讀器的APP,支援了IOS版本和Android版本的APP。


    class Program
    {
        static void Main(string[] args)
        {
            ReadingApp readingAndroidApp = new Android() { Text = "Read this text" };
            readingAndroidApp.Display();
            //readingAndroidApp.ReverseDisplay();

            ReadingApp readingIos = new Ios() { Text = "Read this text" };
            readingIos.Display();
            //readingAndroidApp.ReverseDisplay();
        }
    }
    class ReadingApp
    {
        public string Text { get; set; }

        public virtual void Display()
        {
            Console.WriteLine(Text);
        }

        public virtual void ReverseDisplay()
        {
            Console.WriteLine(new string(Text.Reverse().ToArray()));
        }
    }
    class Android : ReadingApp
    {
        public override void Display()
        {
            Console.WriteLine("Display Android");
            base.Display();
        }

        public override void ReverseDisplay()
        {
            Console.WriteLine("Reverse display in  Android".Reverse());
            base.ReverseDisplay();
        }
    }
    class Ios : ReadingApp
    {
        public override void Display()
        {
            Console.WriteLine("Display Ios");
            base.Display();
        }

        public override void ReverseDisplay()
        {
            Console.WriteLine("Reverse display in  Ios".Reverse());
            base.ReverseDisplay();
        }
    }

現在我要多加一個反向顯示,方法為ReverseDisplay(),變成我要每個類別都要調整,追加方法。

為了解決這個問題瓶頸,這時候可以用橋接模式,重構實現更多解除耦合的程式碼。

    class Program
    {
        private static void Main(string[] args)
        {
            ReadingApp readingAndroidAppND = new Android(new NormalDisplay()) { Text = "Read this text" };
            readingAndroidAppND.Display();

            ReadingApp readingIosAppNd = new Ios(new NormalDisplay()) { Text = "Read this text" };
            readingIosAppNd.Display();

            ReadingApp readingAndroidAppRD = new Android(new ReverseDisplay()) { Text = "Read this text" };
            readingAndroidAppRD.Display();

            ReadingApp readingIosAppRD = new Ios(new ReverseDisplay()) { Text = "Read this text" };
            readingIosAppRD.Display();
        }
    }

    public interface IDisplayFormatter
    {
        void Display(string text);
    }

    abstract class ReadingApp
    {
        protected IDisplayFormatter _displayFormatter;

        protected ReadingApp(IDisplayFormatter displayFormatter)
        {
            _displayFormatter = displayFormatter;
        }

        public string Text { get; set; }

        public abstract void Display();
    }

    class Android : ReadingApp
    {
        public Android(IDisplayFormatter displayFormatter) : base(displayFormatter)
        {
        }

        public override void Display()
        {
            _displayFormatter.Display("This is for Android");
        }
    }

    class Ios : ReadingApp
    {
        public Ios(IDisplayFormatter displayFormatter) : base(displayFormatter)
        {
        }

        public override void Display()
        {
            _displayFormatter.Display("This is for Ios");
        }
    }

    class NormalDisplay : IDisplayFormatter
    {
        public void Display(string text)
        {
            Console.WriteLine(text);
        }
    }

    class ReverseDisplay : IDisplayFormatter
    {
        public void Display(string text)
        {
            Console.WriteLine(new string(text.Reverse().ToArray()));
        }
    }

 

上述透過問題來改善繼承的問題,就能回推它的定義:抽象部分與它的實現部分分離,使它們都可以獨立地變化。

當然筆者覺得這是它優點,那筆者認為缺點會是複雜度提高,從問題來學習設計模式,是挺有幫助。

關於方法解耦的策略模式

書上有提到一個我覺得很特別的實際上員工發放薪資案例

  1. 專案經理有專案津貼與抽成
  2. 技術專家與保護津貼與專案獎金
  3. 業務員有業務抽成

系統跟普通員工是發放薪資的策略

如下類別設計

 

 

策略模式是為了某程式中的某方法定義一系列的執行策略,對每一個策略進行封裝,並由執行中的物件自己去選擇採用哪個或那些執行策略,在策略模式的做法,可以使用一個策略甚至多個策略以及全部策略。

多個策略做法,由上述的寫成如下範例



    public class Program
    {
        public static void Main(string[] args)
        {
           Console.WriteLine("主程式");
		   var salaryBus = new SalaryBus();
		   salaryBus.SetSalaryBus(new List<SalaryStrategy>()
    	   {
        		new EmployeeStrategy(22000),
        		new SalesStrategy(2000)
    	   });
           Console.WriteLine(salaryBus.Salary());
        }
    }

class SalaryBus
{
    private List<SalaryStrategy> _salaryStrategies;

    public void SetSalaryBus(List<SalaryStrategy> salaryStrategies)
    {
        _salaryStrategies = salaryStrategies;
    }

    public int Salary()
    {
        int saleary = 0;
        foreach (var salaryStrategieg in _salaryStrategies)
        {
            saleary += salaryStrategieg.salary();
        }
        return saleary;
    }
}

public abstract class SalaryStrategy
{
    public abstract int salary();
}

public class SalesStrategy : SalaryStrategy
{
    private readonly int _commission;     //抽成

    public SalesStrategy(int commission)
    {
        _commission = commission;
    }

    public override int salary()
    {
        return _commission;
    }
}

public class ExpertStrategy : SalaryStrategy
{
    private readonly int _allowance;      //津貼
    private readonly int _projectBounds;  //專案獎金

    public ExpertStrategy(int allowance, int projectBounds)
    {
        _allowance = allowance;
        _projectBounds = projectBounds;
    }

    public override int salary()
    {
        return _allowance + _projectBounds;
    }
}

public class ManagerStrategy : SalaryStrategy
{
    private readonly int _commission;     //抽成
    private readonly int _projectBounds;  //專案獎金

    public ManagerStrategy(int commission, int projectBounds)
    {
        _commission = commission;
        _projectBounds = projectBounds;
    }

    public override int salary()
    {
        return _commission + _projectBounds;
    }
}

public class EmployeeStrategy : SalaryStrategy
{
    private readonly int _pay; //薪資

    public EmployeeStrategy(int pay)
    {
        _pay = pay;
    }

    public override int salary()
    {
        return _pay;
    }
}

補充:以單向設略模式為例

    public class Program
    {
        public static void Main(string[] args)
        {
            Context context;
            // Three contexts following different strategies
            context = new Context(new ConcreteStrategyA());
            context.ContextInterface();
            context = new Context(new ConcreteStrategyB());
            context.ContextInterface();
            context = new Context(new ConcreteStrategyC());
            context.ContextInterface();
            // Wait for user
            Console.ReadKey();
        }
    }

    public abstract class Strategy
    {
        public abstract void AlgorithmInterface();
    }

    public class ConcreteStrategyA : Strategy
    {
        public override void AlgorithmInterface()
        {
            Console.WriteLine(
                "Called ConcreteStrategyA.AlgorithmInterface()");
        }
    }

    public class ConcreteStrategyB : Strategy
    {
        public override void AlgorithmInterface()
        {
            Console.WriteLine(
                "Called ConcreteStrategyB.AlgorithmInterface()");
        }
    }

    public class ConcreteStrategyC : Strategy
    {
        public override void AlgorithmInterface()
        {
            Console.WriteLine(
                "Called ConcreteStrategyC.AlgorithmInterface()");
        }
    }

    public class Context
    {
        Strategy strategy;
        public Context(Strategy strategy)
        {
            this.strategy = strategy;
        }
        public void ContextInterface()
        {
            strategy.AlgorithmInterface();
        }
    }

總結:無論採用單向、多個,策略模式不可能獨立存在,會搭配工廠模式使用,因為所有策略是透過工廠建立後來傳遞程式。

 

將複雜程序抽取多個方法,每個步驟一個方法,將複雜程序變成易於閱讀,還不能達到自由擴展靈活能力,這時候命令模式派上用場。

  1. 將每一個步驟抽取類別抽取出來,形成一個一個命令類別
  2. 每個命令類別都有一個統一方法
  3. 在原有程式中,串列依次執行命令類別,執行程序的縱向擴展能力得以實現

以書上例子為例

初期需求原有設計

JdbSupport是真正執行查詢模組,SqlProxy負責執行SQL與參數處理,變成JDBC識別的格式,SqlProxy是對SQL與參數處理複雜程序,分成預編譯、參數、解析、每個步驟一個方法。

運用抽象類別步驟方法抽取出一個介面,preCompile、parametric()等步驟方法,形成命令類別,並統一方法叫做perForm(),再抽一個介面叫做SqlProcessor,最後用SqlProxy依據選擇來串列執行這些命令類別。

參考來源:

https://en.wikipedia.org/wiki/Bridge_pattern

https://www.dofactory.com/net/strategy-design-pattern

老E隨手寫