SOLID Example

Example 1 (SRP)

Single Responsibility Principle (SRP):單一職責原則

The Single Responsibility Principle (SRP) suggests that each class or module should have only one responsibility. Here’s an example:

SRP 建議每個類別或模組應該只負責單一的職責。以下是一個範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 不符合 SRP 的範例
class Customer
  def initialize(name, email, phone)
    @name = name
    @email = email
    @phone = phone
  end
  
  def save_to_db
    # 保存顧客信息到數據庫
  end
  
  def send_email
    # 發送電子郵件給顧客
  end
end

The above code violates SRP because the Customer class has two responsibilities - saving customer information to the database and sending an email to the customer. Here’s an example of how we can modify the code to adhere to SRP:

上面的代碼違反了 SRP,因為 Customer 類別負責了兩個職責,即保存顧客信息到數據庫和發送電子郵件給顧客。以下是符合 SRP 的修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Customer
  def initialize(name, email, phone)
    @name = name
    @email = email
    @phone = phone
  end
  
  def send_email(customer)
    # 發送電子郵件給顧客
  end
end

class AccountManagment
  def save_to_db
    # 保存顧客信息到數據庫
  end
end

Example 2 (OCP)

Suppose we have a Payment class that handles payment operations. The class currently supports two payment methods:

假設我們有一個 Payment 類別,它用於處理付款操作。該類別目前支持兩種付款方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Payment
  def initialize(amount, payment_method)
    @amount = amount
    @payment_method = payment_method
  end

  def process_payment
    if @payment_method == "credit_card"
      # Process credit card payment
    elsif @payment_method == "paypal"
      # Process PayPal payment
    end
  end
end

There is an obvious flaw in the above code: if we want to add a new payment method, such as bank transfer, we need to modify the process_payment method. This violates the Open-Closed Principle (OCP), which states that we should be closed to modification but open to extension.

To comply with the OCP, we can use the Strategy pattern to redesign the code. First, we create an abstract PaymentStrategy class that includes a process_payment method:

上述代碼有一個明顯的缺點:如果我們想要添加一種新的付款方式,例如銀行轉賬,我們需要修改 process_payment 方法。這違反了 OCP 的原則,因為我們應該對修改關閉,對擴展開放。

為了符合 OCP 的原則,我們可以使用策略模式來重新設計上述代碼。首先,我們創建一個抽象的 PaymentStrategy 類別,該類別包含一個 process_payment 方法:

1
2
3
4
5
class PaymentStrategy
  def process_payment(amount)
    raise NotImplementedError, "Subclass must implement this method"
  end
end

Then, we create concrete payment strategies, such as CreditCardPayment and PayPalPayment classes, which inherit from PaymentStrategy and implement the process_payment method:

然後,我們創建具體的付款策略,例如 CreditCardPayment 和 PayPalPayment 類別,這些類別繼承自 PaymentStrategy 並實現 process_payment 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class CreditCardPayment < PaymentStrategy
  def process_payment(amount)
    # Process credit card payment
  end
end

class PayPalPayment < PaymentStrategy
  def process_payment(amount)
    # Process PayPal payment
  end
end

Finally, we modify the Payment class to accept a PaymentStrategy object as the payment method:

最後,我們修改 Payment 類別,使其接受一個 PaymentStrategy 對象作為付款方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Payment
  def initialize(amount, payment_strategy)
    @amount = amount
    @payment_strategy = payment_strategy
  end

  def process_payment
    @payment_strategy.process_payment(@amount)
  end
end

Now, if we want to add a new payment method, such as bank transfer, we only need to create a BankTransferPayment class, inherit from PaymentStrategy, and implement the process_payment method. Then, we can use the new payment method like this:

現在,如果我們想要添加一種新的付款方式,例如銀行轉賬,我們只需要創建一個 BankTransferPayment 類別,繼承自 PaymentStrategy 並實現 process_payment 方法。然後,我們可以像這樣使用新的付款方式:

1
2
payment = Payment.new(amount, BankTransferPayment.new)
payment.process_payment

This implementation complies with the OCP, because we can add new payment methods without modifying the existing code.

這種實現方式符合 OCP 的原則,因為我們可以添加新的付款方式而不需要修改現有的代碼。

Example 3 (LSP)

The Liskov Substitution Principle (LSP) is a fundamental principle in object-oriented design. It states that a subclass must be able to replace its parent class without affecting the correctness of the program.

里氏替換原則 (Liskov Substitution Principle, LSP) 是面向對象設計中的一個基本原則,它指出子類別必須能夠替換其父類別,而不會對程式的正確性造成任何影響。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 不符合 LSP 的範例
class Animal
  def speak
    puts "Animal is speaking."
  end
end

class Dog < Animal
  def speak
    puts "Dog is barking."
  end
end

def do_something(animal)
  animal.speak
end

dog = Dog.new
do_something(dog) # 輸出:Dog is barking.

In this example, we have defined an Animal parent class and a Dog subclass. The Animal class defines a speak method, while the Dog subclass overrides the speak method of the parent class and writes its own specific behavior.

In the do_something method, we pass in a Dog object and then invoke its speak method. Since Dog overrides the speak method in Animal, when the speak method is called, the output is the specific behavior of Dog, not Animal. This violates the requirement of LSP: the parent class and the subclass cannot be used interchangeably because their behaviors are different.

Here is a modification that satisfies LSP:

在這個例子中,我們定義了一個 Animal 父類別和一個 Dog 子類別。Animal 父類別定義了一個 speak 方法,而 Dog 子類別覆寫了父類別中的 speak 方法,改寫成了自己特定的行為。

在 do_something 方法中,我們傳入一個 Dog 物件,然後調用它的 speak 方法。由於 Dog 覆寫了 Animal 中的 speak 方法,因此在調用 speak 方法時,輸出的是 Dog 的特定行為,而不是 Animal 的行為。這樣就違反了 LSP 的要求:父類別和子類別不可以互相替換使用,因為它們的行為不同。

以下是符合 LSP 的修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Animal
  def speak
    puts "Animal is speaking."
  end
end

class Dog < Animal
  # Dog 類別繼承自 Animal 類別,並沒有修改或覆寫 Animal 中的 speak 方法
end

def do_something(animal)
  animal.speak
end

dog = Dog.new
do_something(dog) # 輸出:Animal is speaking.

在這個例子中,我們定義了一個 Animal 父類別和一個 Dog 子類別。Animal 父類別定義了一個 speak 方法,而 Dog 子類別沒有修改或覆寫 Animal 中的 speak 方法,直接繼承了父類別中的方法。

在 do_something 方法中,我們傳入一個 Animal 物件(這裡是 Dog 物件),然後調用它的 speak 方法。由於 Dog 繼承自 Animal,因此它也擁有一個 speak 方法,可以替代 Animal 物件的位置。這樣就符合了 LSP 的要求:父類別和子類別可以互相替換使用,而不會對系統的正確性造成影響。

Example 4 (ISP)

The Interface Segregation Principle (ISP) states that a large interface should be split into smaller ones, and each class should only implement the interfaces that it needs. This reduces unnecessary dependencies and improves code readability, maintainability, and extensibility.

In Ruby, we can implement the ISP using modules. Here’s a simple example:

介面隔離原則(Interface Segregation Principle, ISP)是指應該將一個大型的介面拆分成多個小的介面,並且每個類別只實現它需要的介面。這樣可以減少不必要的依賴關係,從而提高代碼的可讀性、可維護性和可擴展性。

在 Ruby 中,我們可以通過模塊(Module)來實現介面隔離原則。以下是一個簡單的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module Swimable
  def swim
    puts "#{self.class} is swimming"
  end
end

module Flyable
  def fly
    puts "#{self.class} is flying"
  end
end

class Bird
  include Flyable
  
  def eat
    puts "#{self.class} is eating"
  end
end

class Duck < Bird
  include Swimable
  
  def quack
    puts "Quack!"
  end
end

duck1 = Duck.new
duck1.quack # Quack!
duck1.eat # Duck is eating
duck1.fly # Duck is flying

In the example above, we defined two modules: Swimable and Flyable, which define the swim and fly methods, respectively. Then, we defined a Bird class and included the Flyable module, because birds can fly. We also defined a Duck class that inherits from Bird and includes the Swimable module, because ducks can swim.

The benefit of this design is that each class only implements the interfaces that it needs, reducing dependencies between classes. For example, if we need a bird that cannot fly, we can simply derive a new class from Bird without modifying the existing code.

在上面的例子中,我們定義了兩個模塊:Swimable 和 Flyable,它們分別定義了 swim 和 fly 方法。然後我們定義了一個 Bird 類別,並包含了 Flyable 模塊,因為鳥類能夠飛行。接著,我們定義了一個 Duck 類別,它繼承自 Bird 類別,並包含了 Swimable 模塊,因為鴨子能夠游泳。

這樣設計的好處是,每個類別只實現了自己需要的介面,從而減少了類別之間的依賴關係。例如,如果我們需要一個不能飛行的鳥類,我們只需要從 Bird 類別派生一個新類別,而不需要修改原有的代碼。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class NonFlyingBird < Bird
  def fly
    puts "#{self.class} cannot fly"
  end
end

class Penguin < NonFlyingBird
  include Swimable
  
  def waddle
    puts "Waddle waddle"
  end
end

penguin1 = Penguin.new
penguin1.waddle # waddle waddle
penguin1.eat # Penguin is eating
penguin1.fly # Penguin cannot fly

In this code, we define a new class called NonFlyingBird that inherits from the Bird class. However, we override the fly method to output a message saying that the bird cannot fly.

We can now use this new class to define non-flying birds without modifying the original Bird class or the Flyable module.

在上面的代碼中,我們定義了一個 NonFlyingBird 類別,它繼承自 Bird 類別,但是重寫了 fly 方法,讓它輸出一個不能飛行的信息。然後,我們定義了一個 Penguin 類別,它繼承自 NonFlyingBird 類別,並包含了 Swimable 模塊,因為企鵝可以游泳。Penguin 類別也定義了自己的方法 waddle。

現在,如果我們需要一個不能飛行的鳥類,我們只需要使用 NonFlyingBird 類別即可,不需要修改原有的 Bird 類別或 Flyable 模塊。

Example 5 (DIP)

The Dependency Inversion Principle (DIP) is one of the SOLID design principles. It states that high-level modules should not depend on low-level modules, but both should depend on abstractions. By relying on abstractions, the system’s coupling is reduced, and it becomes easier to perform unit testing and module replacement.

Here’s an example of Ruby code that violates the Dependency Inversion Principle:

依賴反轉原則(Dependency Inversion Principle, DIP)是 SOLID 設計原則中的一個,主要原則是指高層模組不應該依賴於低層模組,而兩者都應該依賴於抽象介面,由抽象介面來統一管理。這樣可以達到降低系統耦合度的目的,並且方便進行單元測試和模組替換。

以下是一個不符合依賴反轉原則的 Ruby 範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Car
  def initialize
    @engine = Engine.new
  end

  def start
    @engine.start
  end
end

class Engine
  def start
    puts "Engine started"
  end
end

In this example, the Car class directly depends on the Engine class, which is a low-level module. This violates the Dependency Inversion Principle.

In Ruby, implementing the Dependency Inversion Principle can be achieved through Duck Typing since Ruby is a dynamically typed language. If a class implements a method, it can be considered as implementing an interface.

Therefore, in Ruby, it is usually unnecessary to create an interface. Instead, we only need to ensure that the lower-level module implements the methods that the higher-level module needs. We can use modules or Ruby’s multiple inheritance feature to make the lower-level module implement the required methods of the higher-level module.

Here’s an example of using a module to implement the Dependency Inversion Principle in Ruby: 在這個例子中,Car 類別直接依賴於 Engine 類別,而 Engine 類別也是低層模組,這樣就違反了依賴反轉原則。

在 Ruby 中,實作依賴反轉原則可以使用 Duck Typing,因為 Ruby 是一個動態類型的語言。如果一個類別實現了一個方法,它就可以被視為實現了一個接口。

因此,在 Ruby 中,通常不需要建立一個介面,只要確定低層模組實現了高層模組需要使用的方法即可。我們可以使用模組或者 Ruby 的多重繼承功能,讓低層模組實現高層模組需要使用的方法。

以下是一個使用模組實現依賴反轉原則的 Ruby 範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module EngineInterface
  def start
    raise NotImplementedError, "#{self.class}##{__method__} must be implemented"
  end
end

class Car
  def initialize(engine)
    @engine = engine
  end

  def start
    @engine.start
  end
end

class Engine
  include EngineInterface

  def start
    puts "Engine started"
  end
end

engine = Engine.new
car = Car.new(engine)
car.start

In this example, we define a module, EngineInterface, which Engine class implements so that it can implement the methods that Car needs to use. The Car class accepts an argument engine, which can be any class that implements the EngineInterface module. This allows the Car class to depend on an abstract interface rather than the lower-level Engine class itself.

In Ruby, as long as a specific method exists in the object being called, even if the method is not explicitly defined as an interface, Ruby can still find and call that method.

However, this approach is not recommended. If you want to modify the name or parameters of the start method in the Engine class, or if you want other classes to implement the EngineInterface module, you may need to modify the Car class. If you create an explicit interface, such modifications can be simpler because you only need to make the Engine class implement the new interface, rather than modifying the Car class.

In summary, even though Ruby does not have explicit interfaces like some other languages, using abstract interfaces to implement the Dependency Inversion Principle is still a good practice because it can make the system more flexible and maintainable.

在這個例子中,我們定義了一個模組 EngineInterface,讓 Engine 類別實現這個模組,這樣 Engine 就能夠實現 Car 需要使用的方法。Car 類別接收一個引數 engine,這個引數可以是任何實現了 EngineInterface 模組的類別。這樣就讓 Car 類別依賴於一個抽象介面,而不是低層模組 Engine 類別本身。

在 Ruby 中,對於某個特定的方法,只要該方法存在於被呼叫的物件中,即使該方法沒有被明確地定義為介面,Ruby 仍然可以找到該方法並呼叫它。

然而,這種做法並不推薦。如果您想要在 Engine 類別中修改 start 方法的名稱或者參數,或者您想要讓其他類別實現 EngineInterface 模組,您可能需要對 Car 類別進行修改。如果您建立了一個明確的介面,這樣的修改就可以更加簡單,因為您只需要讓 Engine 類別實現新的介面即可,而不需要對 Car 類別進行修改。

總之,即使在 Ruby 中沒有像其他語言中一樣明確的介面,使用抽象介面來實現依賴反轉原則仍然是一種好的做法,因為這樣可以讓系統更具彈性和可維護性。

updatedupdated2023-04-262023-04-26