Try   HackMD
文章目錄

高內聚,鬆耦合

常說在物件導向設計(和一般的程式設計)中,最重要的目標之一就是要有高內聚和鬆耦合。這是什麼意思?為什麼這麼重要,如何實現?

高內聚是指一個模塊(類或方法)內部的相關操作應該緊密相連,並專注於執行一項特定的任務。鬆耦合是指模塊之間的依賴性應該盡可能地降低,這使得模塊更容易修改和擴展。這兩者都非常重要,因為他們有助於提高程式的可讀性,可維護性和可重用性。

Python範例:

# 高內聚示例:一個專門處理日期的類 class DateHandler: def __init__(self, date): self.date = date def get_year(self): return self.date.year def get_month(self): return self.date.month def get_day(self): return self.date.day # 鬆耦合示例:使用DateHandler類,但不直接依賴其實現 class Event: def __init__(self, date_handler): self.date_handler = date_handler def get_event_date(self): return self.date_handler.get_day(), self.date_handler.get_month(), self.date_handler.get_year()

索引0

為什麼大多數語言中,陣列索引從 0 開始?

這是由於從0開始索引可以使某些計算變得更簡單,例如計算元素在記憶體中的位置。另外,從歷史上看,許多早期的程式設計語言(如C)都從0開始索引,許多現代語言也沿用了這種習慣。

Python範例:

numbers = [10, 20, 30, 40] print(numbers[0]) # 會輸出10,因為索引從0開始

TDD

測試和 TDD 如何影響程式設計?

TDD(測試驅動開發)是一種開發實踐,其中開發者首先寫出失敗的自動化測試案例,然後寫出能使該測試通過的程式。此過程重複進行,逐漸建立和改進程式的功能。TDD 可以幫助我們設計出低耦合,高內聚的程式,因為它鼓勵我們使用較小的、獨立的單元來實現程式的功能。另外,TDD 還可以促使我們常常重構程式,以改進其結構並消除重複的程式碼。

Python範例:

import unittest # 待測試的函數 def add(a, b): return a + b # 測試類 class TestAdd(unittest.TestCase): def test_add(self): self.assertEqual(add(1, 2), 3) self.assertEqual(add(-1, 1), 0) self.assertEqual(add(0, 0), 0) # 運行測試 if __name__ == '__main__': unittest.main()

DRY Violation

寫一段違反Don't Repeat Yourself (DRY)原則的程式碼。然後解釋為什麼這是一個壞的設計,並修正它。

DRY原則強調程式碼不應該有重複。如果兩段程式碼都做同樣的事情,那麼如果需要改變這個行為,我們將需要在兩個地方都做修改,這增加了維護的難度並提高了錯誤發生的風險。

以下是一段違反DRY原則的JavaScript程式碼,重複定義了計算平方和立方的功能:

function squareAndCube(x) { let square = x * x; let cube = x * x * x; return [square, cube]; } function squareAndCubeAgain(y) { let square = y * y; let cube = y * y * y; return [square, cube]; }

我們可以通過將共享的功能提取到一個單獨的函數中來修正這個問題,從而消除重複的程式碼,如下所示:

function calculateSquare(x) { return x * x; } function calculateCube(x) { return x * x * x; } function squareAndCube(x) { let square = calculateSquare(x); let cube = calculateCube(x); return [square, cube]; } function squareAndCubeAgain(y) { let square = calculateSquare(y); let cube = calculateCube(y); return [square, cube]; }

Cohesion vs Coupling

什麼是內聚力和耦合性的區別?

內聚力和耦合性是衡量軟件設計品質的兩個重要參數。

內聚力(Cohesion)是指一個模組、類或函數內部元素的相關程度。高內聚力意味著模組、類或函數內部的元素密切相關,每個模組或類有一個清晰的,單一的功能。這可以使代碼易於理解和維護。

耦合性(Coupling)是指兩個模組、類或函數之間的相依程度。低耦合性表示每個模組或類可以獨立於其他模組或類運作。這可以使得模組或類更容易重用和修改,並提高代碼的健壯性。

舉例來說,以下是一個遵循高內聚低耦合原則的 Python 程式碼:

class Circle: def __init__(self, radius): self.radius = radius def area(self): return 3.14 * self.radius * self.radius class Square: def __init__(self, side): self.side = side def area(self): return self.side * self.side shapes = [Circle(2), Square(3)] for shape in shapes: print(shape.area())

在這個例子中,CircleSquare 類別各自有一個清晰的、單一的功能(計算面積),顯示高內聚性。它們可以獨立運作而不依賴於彼此或其他類,顯示低耦合性。

重構

什麼是重構有什麼用?

重構是一種程式設計的練習,在不改變軟體外部行為的情況下,改進其內部結構。重構的主要目的是使程式碼更易於理解和維護。它可以幫助我們清除冗餘的代碼、優化演算法、減少程式碼的複雜性、改善程式碼的讀性等。

當我們發現程式碼中存在的壞味道(如重複的程式碼、過長的函數、過大的類別、許多魔術數字等)時,我們就應該進行重構。重構讓我們的程式碼更乾淨,更容易閱讀,並減少未來出錯的可能性。

以下是一個 Python 程式碼重構的例子:

# 重構前 def calculate_area(type, dimension): if type == "circle": return 3.14 * dimension * dimension elif type == "square": return dimension * dimension # 重構後 class Circle: def __init__(self, radius): self.radius = radius def area(self): return 3.14 * self.radius * self.radius class Square: def __init__(self, side): self.side = side def area(self): return self.side * self.side

重構後的程式碼透過定義兩個類別(Circle和Square)來替代之前的單一函數,使得程式碼的結構更加清晰且容易維護。

程式碼註解

程式碼中的註解是否有用?有些人認為應該儘可能避免使用註解,甚至讓註解變得不必要。你同意嗎?

程式碼註解在某些情況下是很有用的。它們可以幫助我們理解一段程式碼的目的、原理或者我們需要注意的一些事情。它們對於初次閱讀或者在較長時間後回來閱讀程式碼的人特別有幫助。

然而,我們也應該避免過度依賴註解。理想的程式碼應該自解釋——透過明確的變數名稱、函數名稱、模組結構以及清晰的邏輯,讓人可以直觀地理解其功能和行為。在這種情況下,註解就變得不必要了。

如果我們發現某個地方需要註解來解釋,那麼可能就表示那裡的程式碼需要重構。例如,如果一個函數過於複雜,需要註解來說明其邏輯,那麼我們可能需要將該函數分解為幾個更簡單的函數。

以下是一個註解的 Python 例子:

def calculate_bmi(height, weight): # 計算BMI # 參數height為身高(單位:米) # 參數weight為體重(單位:公斤) # 返回BMI值 return weight / (height * height)

而我們可以透過改進函數名稱和參數名稱來消除註解:

def calculate_bmi_in_kg_and_meter(weight_in_kg, height_in_meter): return weight_in_kg / (height_in_meter * height_in_meter)

TDD

測試和TDD如何影響程式設計?

TDD(測試驅動開發)是一種開發實踐,其中開發者首先寫出失敗的自動化測試案例,然後寫出能使該測試通過的程式。此過程重複進行,逐漸建立和改進程式的功能。TDD 可以幫助我們設計出低耦合,高內聚的程式,因為它鼓勵我們使用較小的、獨立的單元來實現程式的功能。另外, TDD 還可以促使我們常常重構程式,以改進其結構並消除重複的程式碼。

Python範例:

import unittest # 待測試的函數 def add(a, b): return a + b # 測試類 class TestAdd(unittest.TestCase): def test_add(self): self.assertEqual(add(1, 2), 3) self.assertEqual(add(-1, 1), 0) self.assertEqual(add(0, 0), 0) # 運行測試 if __name__ == '__main__': unittest.main()

DRY Violation

寫一段違反 Don't Repeat Yourself (DRY) 原則的程式碼。然後解釋為什麼這是一個壞的設計,並修正它。

DRY 原則強調程式碼不應該有重複。如果兩段程式碼都做同樣的事情,那麼如果需要改變這個行為,我們將需要在兩個地方都做修改,這增加了維護的難度並提高了錯誤發生的風險。

以下是一段違反 DRY 原則的 JavaScript 程式碼,重複定義了計算平方和立方的功能:

function squareAndCube(x) { let square = x * x; let cube = x * x * x; return [square, cube]; } function squareAndCubeAgain(y) { let square = y * y; let cube = y * y * y; return [square, cube]; }

我們可以通過將共享的功能提取到一個單獨的函數中來修正這個問題,從而消除重複的程式碼,如下所示:

function calculateSquare(x) { return x * x; } function calculateCube(x) { return x * x * x; } function squareAndCube(x) { let square = calculateSquare(x); let cube = calculateCube(x); return [square, cube]; } function squareAndCubeAgain(y) { let square = calculateSquare(y); let cube = calculateCube(y); return [square, cube]; }

設計與架構的區別

設計和架構之間的區別是什麼?

雖然設計架構這兩個詞語在許多情況下可以互換使用,但在軟體工程中,它們通常有不同的含義。

設計通常指的是系統的細節部分。它涉及到如何實現特定的功能或者行為。設計的範疇包括選擇適當的設計模式、編寫清晰可讀的程式碼、使用有效的資料結構和算法等等。設計的重點在於如何做

架構則在更高的層次上看待系統。它涉及到系統各部分的組織方式,以及這些部分之間的互動。架構的範疇包括定義模組與模組之間的關係、決定系統的擴展性、安全性、性能等方面的策略等等。架構的重點在於做什麼為什麼這樣做

為了幫助理解,我們可以想像正在建造一棟房子。在這個過程中,架構就像是建築師設計的藍圖,它描述了房子的整體結構和風格。而設計則更像是工程師和工匠的工作,他們根據藍圖實現具體的細節,例如建造牆壁、安裝窗戶、鋪設地板等等。

提前測試

TDD 中,為什麼要先寫測試再寫代碼?

測試驅動開發(Test-Driven Development, TDD)是一種編程實踐,其中開發者在編寫新的代碼之前先編寫用於檢查該代碼行為的測試。這樣做有幾個重要的原因:

  1. 確定需求:編寫測試可以讓開發者思考他們的代碼需要做什麼。測試基本上就是需求的另一種形式,它們說明了代碼的期望行為。
  2. 設計輔助:在編寫測試的過程中,開發者可以思考如何設計他們的代碼,以便其容易被測試。這通常導致更好的設計,因為容易測試的代碼往往具有良好的解耦合和高內聚。
  3. 即時驗證:當開發者編寫代碼以使測試通過時,他們可以即時確認他們的代碼是否正常工作。如果代碼導致測試失敗,開發者可以立即修改,而不是等到後期才發現問題。
  4. 回歸保護:所有的測試都構成了一個回歸套件,可以在未來的開發過程中運行,以確保代碼的改變沒有破壞現有的功能。

下面是一個 Python 的例子,首先我們會編寫一個測試函數來測試一個未來要實現的功能,例如一個用於添加兩個數的函數:

def test_add(): assert add(1, 2) == 3 此時,如果我們運行這個測試,它會失敗,因為我們還沒有實現add函數。然後我們可以實現add函數來使測試通過: python Copy code def add(a, b): return a + b

再次運行測試,它應該會通過,證明我們的 add 函數正常工作。

多重繼承

C++支持多重繼承,而 Java 允許一個類實現多個接口。使用這些功能對正交性有什麼影響?使用多重繼承和多個接口之間的影響是否有所不同?使用委託和使用繼承之間有何區別?(此問題來自Andrew Hunt和David Thomas的《實用程序員》)

正交性在設計中意味著系統的一部分可以獨立於其他部分進行更改。繼承(特別是多重繼承)可以降低正交性,因為它創建了一種強烈的依賴關係:子類依賴於其父類的實現。這意味著,如果父類改變,所有的子類都可能受到影響,這使得系統更難以修改和維護。

與此相比,使用接口或委託可以提高正交性。接口定義了一個合約,但不包含實現,因此類可以實現一個或多個接口,而不需要依賴特定的實現。這使得系統的不同部分可以獨立進行更改。

委託是一種設計模式,允許一個對象將某些行為委託給其他對象。這可以實現一種類似於繼承的效果,但是不會創建與繼承相關的緊密耦合。因此,使用委託可以提高正交性,因為它減少了對象之間的依賴關係。

Python 中,我們可以使用繼承或者使用委託來實現相似的功能:

# 使用繼承 class Base: def foo(self): return "foo" class Derived(Base): pass d = Derived() print(d.foo()) # "foo" # 使用委託 class Delegator: def __init__(self): self.base = Base() def foo(self): return self.base.foo() d = Delegator() print(d.foo()) # "foo"

這兩種方法都可以使得 Derived 或者 Delegator 擁有 foo 方法,但是使用委託的方式可以在不破壞正交性的情況下更靈活地重構和修改代碼。

儲存過程中的域邏輯

將域邏輯放在儲存過程中有哪些優點和缺點?

域邏輯指的是那些與業務規則和業務過程有關的邏輯。在資料庫儲存過程中包含域邏輯有一些優點和缺點。

優點:

  • 效能:將資料處理邏輯放在儲存過程中可能會提高效能,因為避免了在資料庫和應用程式之間傳輸大量資料。
  • 重用:如果多個應用程式使用相同的邏輯,那麼將該邏輯放在儲存過程中可以避免重複編碼。
  • 安全性:可以透過設定權限控制誰可以執行儲存過程,進一步提高資料的安全性。

缺點:

  • 維護:儲存過程通常用特定於資料庫的語言(如PL/SQL或T-SQL)編寫,這可能使得維護和除錯變得更困難,特別是當開發團隊不熟悉這種語言時。
  • 可移植性:儲存過程綁定於特定的資料庫系統,如果需要更換資料庫系統,可能需要重寫儲存過程。
  • 測試:測試儲存過程可能比測試應用程式邏輯更困難,因為儲存過程運行在資料庫環境中,無法像應用程式代碼那樣輕易地進行單元測試。

在考慮是否在儲存過程中包含域邏輯時,應該根據實際的需求和情況進行決定。

面向對象設計主導了多年的市場

你認為為什麼面向對象設計主導了多年的市場?

面向對象的設計(Object-Oriented Design,OOP)已經成為多年來主導市場的一種重要設計範式,原因可能有以下幾點:

  • 模型世界:面向對象設計允許開發者以類來建模現實世界的實體,這種方法對於人類的思維模式來說很自然且直觀。類和物件的概念使我們可以將複雜的問題分解為更易於管理和理解的部分。
  • 重用:面向對象的設計鼓勵重用。通過繼承,子類可以重用父類的程式碼,這有助於減少重複的程式碼,並使得程式碼更為模組化。
  • 封裝:封裝是面向對象設計的一個重要特性,它允許我們隱藏物件的內部狀態並將其實現細節封裝起來。這使得我們能夠將介面(物件可以做什麼)與實現(物件是如何做到的)分開,從而提高了程式碼的模組化。
  • 支持:很多現代的編程語言(如Java,C++,Python等)都支持面向對象的編程,並提供了一系列工具和函式庫來支持面向對象設計。

這些都是面向對象設計能夠主導市場多年的原因。然而,並不是所有的問題都最適合使用面向對象設計來解決,例如,在處理大規模數據處理或並發性問題時,函數式編程或過程式編程可能會是更好的選擇。

壞設計

如何理解你的程式碼設計不良?

一個壞的程式碼設計通常有以下幾種特徵:

  • 重複的程式碼:如果你發現你的程式碼中存在大量的重複程式碼,這可能意味著你的設計存在問題。良好的設計應該遵循DRY(Don't Repeat Yourself)原則,避免重複的程式碼。
  • 高度耦合:如果你的類或函數之間有過多的依賴關係,這可能導致程式碼很難維護和修改。良好的設計應該優先考慮低耦合。
  • 低內聚:如果一個類或函數負責了過多的職責,或者其行為不一致,那麼這可能導致程式碼很難理解和修改。良好的設計應該優先考慮高內聚。
  • 缺乏模組化和封裝:如果你的程式碼中的每個部分都需要了解其他部分的太多細節,這可能意味著你的設計存在問題。良好的設計應該優先考慮模組化和封裝。

這些都是可能指示你的程式碼設計存在問題的跡象。當然,評估設計的好壞往往需要具體問題具體分析,並結合項目的實際需求和限制。

例如,使用 Python

# 壞設計 class User: def __init__(self, name, age): self.name = name self.age = age def get_name(self): return self.name def get_age(self): return self.age # 使用 User 類的程式碼需要直接訪問並修改其內部狀態 user = User('Alice', 20) print(user.name) # Alice user.name = 'Bob' print(user.name) # Bob # 好設計 class User: def __init__(self, name, age): self._name = name self._age = age @property def name(self): return self._name @property def age(self): return self._age # 使用 User 類的程式碼只能透過公開的介面訪問其內部狀態,不能直接修改 user = User('Alice', 20) print(user.name) # Alice user.name = 'Bob' # 會引發錯誤,因為 name 是只讀的

在這個例子中,壞設計允許任何程式碼直接訪問並修改 User 類的內部狀態,這違反了封裝的原則。好設計通過使用 Python@property 裝飾器,將 User 類的內部狀態封裝起來,只提供了讀取內部狀態的公開介面,這符合封裝的原則。