淺談程式設計正規化

沃趣科技發表於2019-03-04

| 前言

相信絕大部分開發人員、DBA都聽過正規化這個詞,在MySQL中有第一正規化、第二正規化、第三正規化、BCNF正規化等,在開發中也有相應的正規化,專業詞彙叫程式設計正規化(ProgrammingParadigm)。由於筆者能力、精力都有限,本篇主要通過針對同一業務場景,基於程式設計正規化的概念,核心原理以及用例實現來對比不同正規化及其實現業務功能的差異。

正規化分類

如圖1所示,正規化可以簡單分為三類:

圖1: 正規化的簡單分類

正規化和語言的關係

圖2: 正規化和語言的關係

與成百種程式語言相比,程式設計正規化要少得多,如圖2所示,共有27種正規化。多數正規化之間僅相差一個或幾個概念,比如圖中的函式程式設計正規化,在加入了狀態(state)之後就變成了物件導向程式設計正規化。


| 程式設計正規化

圖3: 華山派劍氣之爭

程式式程式設計的核心在於模組化,在實現過程中使用了狀態,依賴了外部變數,導致很容易影響附近的程式碼,可讀性較少,後期的維護成本也較高。

函數語言程式設計的核心在於“避免副作用”,不改變也不依賴當前函式外的資料。結合不可變資料、函式是第一等公民等特性,使函式帶有自描述性,可讀性較高。

物件導向程式設計的核心在於抽象,提供清晰的物件邊界。結合封裝、整合、多型特性,降低了程式碼的耦合度,提升了系統的可維護性。

不同的正規化的出現,目的就是為了應對不同的場景,但最終的目標都是提高生產力。就如華山派的劍宗、氣宗之別,劍宗認為“劍為主,氣為輔”,而氣宗則反之。每個正規化都會有自己的”心法”,但最終殊途同歸,達到至高境界後則是劍氣雙修。


| 小結

閱讀完之前內容後,相信各位讀者對程式設計正規化有了初步的理解,那麼接下來就和筆者一起來實現業務的真實需求。


| 需求

  •   1.解析並收集shannon, fio 兩種 flash卡的溫度、壽命等資訊。

  •  2.對實現程式碼進行單元測試

在用過程式實現之前,筆者先給大家介紹下什麼叫程式式程式設計。


程式式程式設計(Procedural)

程式式程式設計和麵向物件程式設計的區別並不在於是否使用函式或者類,也就是說用到類或物件的可能是程式式程式設計,只用函式而沒有類的也可能是物件導向程式設計。那麼他們的區別又在哪兒呢?

程式導向其實是最為實際的一種思考方式,可以說程式導向是一種基礎的方法,它考慮的是實際地實現。一般的程式導向是從上往下步步求精,所以程式導向最重要的是模組化的思想方法。當程式規模不是很大時,程式導向的方法還會體現出一種優勢。因為程式的流程很清楚,按著模組與函式的方法可以很好的組織。

關鍵部分實現程式碼

def get_shannon_info(output):
    """獲取shannon型別flash卡資訊
    """
    def check_health():
        time_left = float(sub_info["life_left"])
        if time_left < DISK_ALARM_LIFETIME:
            message = "time left is less than {}%".format(DISK_ALARM_LIFETIME)
            return message
        temperature = float(sub_info["temperature"].split()[0])
        if temperature > DISK_ALARM_TEMPERATURE:
            message = "temperature is over than {} C".format(DISK_ALARM_TEMPERATURE)
            return message
        return "healthy"
    result = {}
    all_info = _get_shannon_info(output)
    for info in all_info:
        sub_info = {}
        sub_info["available_capacity"] = info.get("disk_capacity", "")
        sub_info["device_name"] = info.get("block_device_node", "")
        sub_info["firmware_version"] = info.get("firmware_version", "")
        sub_info["interface"] = "PCIe"
        sub_info["life_left"] = str(info.get("estimated_life_left", "").replace("%", ""))
        sub_info["pcie_id"] = info.get("pci_deviceid", "")
        sub_info["pcie_length"] = ""
        sub_info["pcie_type"] = ""
        sub_info["physical_read"] = info.get("host_read_data", "")
        sub_info["physical_write"] = info.get("total_write_data", "")
        sub_info["serial_number"] = info.get("serial_number")
        sub_info["temperature"] = info.get("controller_temperature")
        sub_info["type"] = info["type"]
        sub_info["error_msg"] = check_health()
        sub_info["status"] = "ok" if sub_info["error_msg"] == "healthy" else "error"
        if sub_info["serial_number"]:
            result[sub_info["serial_number"]] = sub_info
        else:
            result[sub_info["device_name"]] = sub_info
    return result

程式碼問題

  • 1.邏輯冗長,區域性修改必須閱讀整段程式碼

  • 2.對外部變數有依賴

  • 3.內部存在共享變數

  • 4.函式內部存在臨時變數

測試程式碼

過程式的測試程式碼效果遠不如函式式有效,過程式的實現邏輯過於冗長,導致測試效果並不夠好。


函數語言程式設計(Functional)

當談論函數語言程式設計,會提到非常多的“函式式”特性。提到不可變資料,第一類物件以及尾呼叫優化,這些是幫助函數語言程式設計的語言特徵。提到mapping(對映),reducing(歸納),piplining(管道),recursing(遞迴),currying(科裡化),以及高階函式的使用,這些是用來寫函式式程式碼的程式設計技術。提到並行,惰性計算以及確定性,這些是有利於函數語言程式設計的屬性。

最主要的原則是避免副作用,它不會依賴也不會改變當前函式以外的資料。

宣告式的函式,讓開發者只需要表達 “想要做什麼”,而不需要表達 “怎麼去做”,這樣就極大地簡化了開發者的工作。至於具體 “怎麼去做”,讓專門的任務協調框架去實現,這個框架可以靈活地分配工作給不同的核、不同的計算機,而開發者不必關心框架背後發生了什麼。

關鍵部分實現程式碼

def get_shannon_info(output):
    """查詢shannon型別flash卡資訊
    """
    lines = checks_string_split_by_function(output, is_shannon_flash_device)
    info = map(parser_shannon_info, lines)
    # map(lambda x: x.setdefault("type", "shannon"), info)
    for item in info:
        item["type"] = "shannon"
    data = map(modify_the_properties, info)
    return reduce(combining_data, map(convert_data_format, data))

以上程式碼帶有自描述性,通過函式名就可知在做什麼,這也是函式式的一個特性:  程式碼是在描述要幹什麼,而不是怎麼幹。

測試程式碼

@pytest.mark.parametrize("line, result", [
("Found Shannon PCIE", False),
("Found Shannon PCIE Flash car", False),
("Found Shannon PCIE Flash card a", True),
("Found Shannon PCIE Flash card", True),
("Found Shannon PCIE Flash card.", True),
])
def test_is_shannon_flash_device(line, result):
    assert functional.is_shannon_flash_device(line) == result
@pytest.mark.parametrize("line, result", [
("a=1", True),
("b=2", True),
("c=2333", True),
("d x=abcde", True),
("Found Shannon PCIE=1", True),
("abcdedfew=", False),
("Found Shannon PCIE", False),
(" =Found Shannon PCIE", False),
("=Found Shannon PCIE", False),
("Found Shannon PCIE=", False),
("Found Shannon PCIE= ", False),
])
def test_is_effective_value(line, result):
    assert functional.is_effective_value(line) == result
@pytest.mark.parametrize("line, result", [
("a=1", {"a": "1"}),
("b=2", {"b": "2"}),
("a=a", {"a": "a"}),
("abc=a", {"abc": "a"}),
("abc=abcde", {"abc": "abcde"}),
])
def test_gets_the_index_name_and_value(line, result):
    assert functional.gets_the_index_name_and_value(line) == result
@pytest.mark.parametrize("output, filter_func, result", [
("abcd\nbcd\nabcd\nbcd\naa\naa", lambda x: "a" in x, ["abcd\nbcd", "abcd\nbcd", "aa", "aa"]),
(open(os.path.join(project_path, "fixtures", "shannon-status.txt")).read(), functional.is_shannon_flash_device, [
    open(os.path.join(project_path, "fixtures", "shannon-sctb.txt")).read(),
    open(os.path.join(project_path, "fixtures", "shannon-scta.txt")).read()
])
])
def test_checks_string_split_by_function(output, filter_func, result):
    assert functional.checks_string_split_by_function(output, filter_func) == result

| 物件導向程式設計(Object-Oriented)

並不是使用類才是物件導向程式設計。如果你專注於狀態改變和密封抽象,你就是在用物件導向程式設計。類只是幫助簡化物件導向程式設計的工具,並不是物件導向程式設計的要求或指示器。封裝是一個過程,它分隔構成抽象的結構和行為的元素。封裝的作用是分離抽象的概念介面及其實現。類只是幫助簡化物件導向程式設計的工具,並不是物件導向程式設計的要求或指示器。

隨著系統越來越複雜,系統就會變得越來越容易崩潰,分而治之,解決複雜性的技巧。面對物件思想的產生是為了讓你能更方便的理解程式碼。有了那些封裝,多型,繼承,能讓你專注於部分功能,而不需要了解全域性。

關鍵部分實現程式碼

class IFlash(six.with_metaclass(abc.ABCMeta)):
    def __init__(self):
        pass
    @abc.abstractmethod
    def collect(self):
        """收集flash卡物理資訊
        """
        pass
class FlashShannon(IFlash):
    """寶存的Flash卡
    """
    def __init__(self, txt_path, command, printer):
        super(FlashShannon, self).__init__()
        self.txt_path = txt_path
        self.command = command
        self.printer = printer
    def collect(self):
        result = {}
        for info in self._get_shannon_info():
            life_left = str(info.get("estimated_life_left", "")).replace("%", "")
            temperature = info.get("controller_temperature", "")
            error_msg = self._get_health_message(life_left, temperature)
            sub_info = {
                "available_capacity": info.get("disk_capacity", ""),
                "device_name": info.get("block_device_node", ""),
                "firmware_version": info.get("firmware_version", ""),
                "interface": "PCIe",
                "life_left": life_left,
                "pcie_id": info.get("pci_deviceid", ""),
                "pcie_length": "",
                "pcie_type": "",
                "physical_read": info.get("host_read_data", ""),
                "physical_write": info.get("total_write_data", ""),
                "serial_number": info.get("serial_number", ""),
                "temperature": temperature,
                "type": info["type"],
                "error_msg": error_msg,
                "status": "ok" if error_msg == "healthy" else "error"
            }
            if sub_info["serial_number"]:
                result[sub_info["serial_number"]] = sub_info
            else:
                result[sub_info["device_name"]] = sub_info
        return result
class FlashFio(IFlash):
    """fio的Flash卡
    """
    def __init__(self, txt_path):
        super(FlashFio, self).__init__()
        self.txt_path = txt_path
    def collect(self):
        disk_info = {}
        adapter_info = self._get_adapter_info()
        for info in adapter_info:
            serial_number = info["fio_serial_number"]
            for io in info["iomemory"]:
                data = self._combining_io_memory(io)
                data["serial_number"] = serial_number
                disk_info[serial_number] = data
        return disk_info

| 程式設計正規化帶來的好處

正規化就像武功心法,可以更快的練成絕世神功,但還是離不開基礎功。程式碼也一樣,通過遵循相關正規化和良好的設計後,會帶來可讀性、擴充套件性和可維護性更好的程式碼,進而提升軟體的質量。


| 總結


指令式程式設計、物件導向程式設計、函數語言程式設計,雖然受人追捧的時間點各不相同,但是本質上並沒有優劣之分。 物件導向和函式式、程式式程式設計也不是完成獨立和有嚴格的界限,在抽象出各個獨立的物件後,每個物件的具體行為實現還是有函式式和過程式完成。

現代的程式設計師應該很少有門派之見了,應該集百家之所長,學習其它正規化(語言)的優秀設計理念,整合到自己的程式碼(產品、語言)中,提升工作效率。


致謝

  • 簡述程式設計正規化:https://ginqi7.github.io/posts/brief-description-of-programming-paradigm.html

  • 程式設計範型:https://zh.wikipedia.org/wiki/%E7%BC%96%E7%A8%8B%E8%8C%83%E5%9E%8B

  • 學習程式設計之概述—從程式設計正規化開始:http://dataunion.org/23223.html

  • 物件導向程式設計 VS 函數語言程式設計:http://blog.swanspace.org/oo_vs_fp/


| 作者簡介

黃劍冬·沃趣科技高階開發工程師

人生苦短,我用Python。Python開發愛好者,畢業後一直從事Python相關開發工作,對Python生態有一定理解。主要負責QData相關產品的研發工作。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28218939/viewspace-2637465/,如需轉載,請註明出處,否則將追究法律責任。

相關文章