重構學習筆記(8)-大話重構第八章-第四步:發現程式可擴展點-筆記

重構

 

在系統重構過程中,系統改造強度一步一步強大,原有程式差異越來越大,引入新的BUG風險越高一步一步加大。

避免有效系統重構帶來風險:時間計劃與版控加強管理。

時間上將重構每個步驟劃分不同時間點(第一步與第二步合併成一個節點),每個時間點完成該步驟工作,在節點結束時打基線,形成一個獨立版本。

好處:後面重構遇到重大BUG可以回到某個基線點,確保重構工作不至於失敗,同時系統需要發佈,產品經理一步一步逐步發布版本,使重構逐步展開。

在前面步驟解決程式復用率,後續可以考慮系統擴展問題,主要目的可以更輕鬆應付系統需求變更,提升系統易變更性。


程式可擴展性的一般原則

預先進行可擴展性設計不要太多

需要進行設計前,應客觀評估該功能日後的可能性,針對可能性較大需求變更進行預先可擴展性設計。

常見可擴展設計在淤求變更到來才進行

運用兩頂帽子設計模式,先對原程式進行可擴展設計,再加入新的、要實現的功能。


開放—封閉原則與客擴展點設計

OCP設計原則:開發的軟體系統,對於功能是開放的。

當系統需求發生變更時候,可以針對軟體功能進行擴展,滿足新的需求,同時對軟體的修改是封閉。

這邊的意義是修改程式是無可避免,透過修改程式來實現新需求,關鍵新需求修改程式碼時候同時修改原有功能類別,勢必會為原有功能,引入BUG的風險。

上述的解法式,在不修改原有程式碼過的基礎上實現新功能。

常見問題:要實現新的功能必然會改到原有程式碼,但又不能修改原有程式碼,關鍵在於原有程式碼與新程式有效隔離

為了保證原有功能不受影響,我們不能修改原有程式,但為了新的功能,我們可以增加新的類別,這是OCP原則關鍵所在。

以系統匯出功能為例

if(type =="All")
{
	//全部匯出
}
else if(type == "exportChoosen")
{
	//選擇匯出
}
else if(type == "exportOnePage")
{
    //本頁匯出
}

若是增加一個需求、按頁匯出

if(type =="All")
{
	//全部匯出
}
else if(type == "exportChoosen")
{
	//選擇匯出
}
else if(type == "exportOnePage")
{
    //本頁匯出
}
else if(type == "exportPageRange")
{
    //按頁匯出
}

 

在多個不同選擇當中放在一個類別中必會為公能擴展帶來麻煩。

筆者會比較常用消除的選擇結構會有,目前所知的在C#中會有

  1. 字典、集合做法,若遇到物件可以採用Action或Fun方式去做
  2. 責任練

這樣設計會違反OCP原則,造成系統品質下降,使用IF大量的狀況會造成:降低可讀性、降低維護性、降低易變性。

總結:主要讓類別承受過多職責,降低功能內聚、提高功能耦合。

造成增加修改程式難度,測試的成本,主要是每一項修改,要進行所有功能測試。

再來先前寫的if else 重構也可以用這樣方式陳現

https://www.dotblogs.com.tw/bda605/2021/08/25/095349

上述按照這樣幾個思維解法,一旦遇到匯出新功能需要擴展時候,不用在加if相關程式進行,其實可以增加一個新類別,依據Exporter Interface實作新的類別進行,再進行相對應的配置功能就可以實現,這樣就可以滿足OCP原則,在系統就可以實現這種可擴展性設計的功能,稱之可擴展點。

以下展示以下該範例,不過這邊用運算類別為例,展示一下簡化的範例

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, IMathOperator> strategies = new Dictionary<string, IMathOperator>() {
                { "+", new MathAdd() },
                { "-", new MathSub() }
            };

            IMathOperator selectedStrategy = Instantiate<IMathOperator>($"MathAdd");
            int result = selectedStrategy.Oper(1, 2);
            Console.WriteLine(result);
        }
        private static T Instantiate<T>(string className)
        {
            Type typeImplemented = typeof(T);
            Type selectedType = Assembly.GetExecutingAssembly() // i.e. our project/library
                    .GetTypes()
                    .First(t => typeImplemented.IsAssignableFrom(t) && t.Name == className);

            return (T)Activator.CreateInstance(selectedType);
        }
    }
   
    public interface IMathOperator
    {
        int Oper(int num1, int num2);
    }

    public class MathAdd : IMathOperator
    {
        public int Oper(int num1, int num2)
        {
            return num1 + num2;
        }
    }
    public class MathSub : IMathOperator
    {
        public int Oper(int num1, int num2)
        {
            return num1 - num2;
        }
    }
}

上述用加減,定義一個運算類別的Interface,接者再搭配新增類別去進行擴充。

該小節重點:

  1. 要遵守兩頂帽子設計原則,先重構原有程式碼,使具有可擴展性功能,再增加新功能,滿足OCP設計原則。
  2. 可擴展設計會增加程式複雜度,降低系統效能。

 
新需求到來,在不增加新功能條件下實現可擴展功能:

  1. 首先透過抽取方法將原有程式IF的語句內容,抽取到一個方法。
  2. 在運用抽取類別將方法抽離到多個類別中。
  3. 最後再抽離介面歸納不同的的類別,最後再抽離共同方法放到介面中。

流程擴展運用樣板模式增加可擴展點

系統需要新功能,又要符合ocp原則,新功能不影響原有功能設計,做擴展點設計將各個功能封裝一個介面下多個實作中,新功能擴展就是增加一個新類別實作,這樣設計解決許多問題,但也帶來新問題,各實作類別程式碼,複用的問題。

接者登場就是,處理流程中相同,相似程式碼很合適使用樣板模式,將相同程式碼抽取出來形成函數,將這些函數升級抽象類別與介面,鄉不同程式碼統一函數名稱,放在各個實作類別中各自實現,這樣程式碼複用問題就解決。

介面導向的可擴展設計

AOP是很像做手術一樣切出一些橫切面,這些橫切面就是程式可擴點,這些橫切面就是程式可擴展點。

常見問題:業務檢驗、授權檢查、交易處理。

在AOP設計中,最典型就是業務處理前的檢驗程式。

怎麼知道切面橫向思想?以下圖為例

上圖的系統模組分成訂單管理、庫存管理、商品管理,而這三部分是業務邏輯功能,這三個系統模組有共用的功能,授權認證、LOG處理,不可能在每一個模組寫授權驗證和LOG處理,而授權驗證不屬於具體業務,他屬於功能模組,並區分橫跨多個功能,可以看到這些是橫向,AOP就是把這些公用功能提取出來,這些公用功能發生變化,就只要修改公用功能程式碼即可,其他地方不需要更改,關注通用功能,而不用關注業務類別,而且不修改原有類別

AOP特性:

1.通用功能業務邏輯衝離出來,提升程式重複使用率,好維護與擴展。
2.設計通用功能,有利模組化,降低軟體複雜度。

接者如何實現AOP的手段有兩種方式、靜態代理實現、動態代理實現(後續再補充)。

 

軟體的特性是越來越複雜,第一次開發品質最高,開始逐漸下降,每變更一次下降一次,軟體品質下降不是平穩下降,而是往後面,程式設計師越看不懂程式,越無法掌握程式開發高品質程式碼,選擇妥協,不論糟糕設計,只完成此次任務就好,一次一次糟糕設計,軟體品質呈指數下降。

修改軟體必須要思考過。

元哥的筆記