使用 SOLID 編寫靈活的程式碼

使用 SOLID 編寫靈活的程式碼

來自譯者: 為您發布 塞維林·佩雷斯的文章 關於在程式設計中使用 SOLID 原則。本文中的資訊對初學者和經驗豐富的程式設計師都有用。

如果您從事開發工作,您很可能聽說過 SOLID 原則。它們使程式設​​計師能夠編寫乾淨、結構良好且易於維護的程式碼。值得注意的是,在程式設計中,有多種方法可以幫助您正確執行特定的工作。不同的專家對「正道」有不同的想法和理解,這取決於每個人的經驗。然而,SOLID 中所宣示的想法幾乎被 IT 社群的所有代表所接受。它們成為許多良好的開發管理實踐的出現和發展的起點。

讓我們了解 SOLID 原則是什麼以及它們如何幫助我們。

技能箱推薦: 實踐課程 “移動開發者專業版”.

提醒: 對於“Habr”的所有讀者 - 使用“Habr”促銷代碼註冊任何 Skillbox 課程可享受 10 盧布的折扣。

什麼是固體?

該術語是一個縮寫,該術語的每個字母都是特定原理名稱的開頭:

  • S單一責任原則。一個模組可以有且只有一個更改原因。
  • O筆/封閉原則 (開放/封閉原則)。類別和其他元素應該對擴展開放,但對修改關閉。
  •  这 L伊斯科夫替換原理 (里氏替換原則)。使用基底類型的函數應該能夠在不知情的情況下使用基底類型的子類型。
  • I介面隔離原則  (界面分離原則)。軟體實體不應依賴它們不使用的方法。
  • D依存倒置原理 (依賴倒置原理)。較高層級的模組不應依賴較低層級的模組。

單一責任原則


單一職責原則 (SRP) 規定程式中的每個類別或模組應該只負責該程式功能的一部分。此外,此職責的元素應分配給它們自己的類,而不是分散在不相關的類中。 SRP 的開發者和首席傳播者 Robert S. Martin 將問責制描述為變革的原因。他最初提出這個術語是作為他的著作《物件導向設計原則》的要素之一。該概念融合了 Tom DeMarco 先前定義的大部分連接模式。

該概念還包括大衛·帕納斯提出的幾個概念。主要的兩個是封裝和資訊隱藏。帕納斯認為,將系統劃分為單獨的模組不應基於框圖或執行流程的分析。任何模組都必須包含向客戶提供最少資訊的特定解決方案。

順便說一句,Martin 舉了一個有趣的例子,一家公司的高階主管(COO、CTO、CFO),每個人都出於不同的目的使用特定的業務軟體。因此,他們中的任何人都可以在軟體中實施更改,而不會影響其他管理人員的利益。

神物

像往常一樣,學習 SRP 的最佳方法是看到它的實際應用。讓我們來看看程序中不遵循單一職責原則的部分。這是描述空間站行為和屬性的 Ruby 程式碼。

查看該範例並嘗試確定以下內容:
SpaceStation 類別中宣告的那些物件的職責。
那些可能對空間站的運作感興趣的人。

class SpaceStation
  def initialize
    @supplies = {}
    @fuel = 0
  end
 
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
 
  def report_supplies
    puts "----- Supply Report -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} units of fuel available."
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @fuel >= 10
      puts "Thrusting action successful."
      @fuel -= 10
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

事實上,我們的太空站功能失調(我認為我不會很快接到 NASA 的電話),但這裡有一些東西需要分析。

因此,SpaceStation 類別具有多種不同的職責(或任務)。所有這些都可以分為以下類型:

  • 感應器;
  • 用品(消耗品);
  • 燃料;
  • 加速器。

儘管車站的員工沒有被分配到一個班級,但我們很容易想像誰負責什麼。最有可能的是,科學家控制感測器,後勤人員負責供應資源,工程師負責燃料供應,飛行員控制助推器。

我們可以說這個程式不符合SRP嗎?是的,當然。但SpaceStation類別是典型的“神物件”,什麼都知道,什麼都做。這是物件導向程式設計中的一個主要反模式。對於初學者來說,維護此類物件極為困難。是的,到目前為止,程式非常簡單,但是想像一下如果我們添加新功能會發生什麼。也許我們的太空站需要一個醫療站或會議室。而且功能越多,SpaceStation 就會成長得越多。好吧,由於該設施將與其他設施相連,維修整個綜合體將變得更加困難。因此,我們可能會擾亂加速器等設備的運作。如果研究人員要求更改感測器,這很可能會影響太空站的通訊系統。

違反SRP原則可能會帶來短期的戰術勝利,但最終我們會“輸掉戰爭”,未來維持這樣的怪物將變得非常困難。最好將程式劃分為單獨的程式碼段,每個程式碼段負責執行特定的操作。理解了這一點,讓我們更改 SpaceStation 類別。

讓我們分配責任

上面我們定義了由 SpaceStation 類別控制的四種操作類型。我們在重構時會牢記它們。更新後的程式碼更符合 SRP。

class SpaceStation
  attr_reader :sensors, :supply_hold, :fuel_tank, :thrusters
 
  def initialize
    @supply_hold = SupplyHold.new
    @sensors = Sensors.new
    @fuel_tank = FuelTank.new
    @thrusters = Thrusters.new(@fuel_tank)
  end
end
 
class Sensors
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
 
  def report_supplies
    puts "----- Supply Report -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} units of fuel available."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

有很多變化,程式現在看起來肯定更好了。現在,我們的 SpaceStation 類別更像是一個容器,在其中啟動相關零件的操作,包括一組感測器、消耗品供應系統、燃料箱和助推器。

對於任何變量,現在都有一個相應的類別:感測器;供應暫停;油箱;推進器。

此版本的程式碼有幾個重要的變更。關鍵在於,各個函數不僅封裝在它們自己的類別中,而且以可預測和一致的方式組織它們。我們將具有相似功能的元素分組以遵循連貫性原則。現在,如果我們需要改變系統的工作方式,從雜湊結構轉移到數組,只需使用 SupplyHold 類,我們不必接觸其他模組。這樣,如果後勤官員改變了他所在區域的某些內容,車站的其餘部分將保持不變。在這種情況下,SpaceStation 類別甚至不會意識到這些變化。

我們在太空站工作的官員可能會對這些變化感到高興,因為他們可以要求他們需要的東西。請注意,程式碼具有SupplyHold 和FuelTank 類別中包含的report_supplies 和report_fuel 等方法。如果地球要求改變其報告方式,會發生什麼? SupplyHold 和 FuelTank 這兩個類別都需要更改。如果您需要改變燃料和消耗品的交付方式呢?您可能必須再次更改所有相同的類別。而這已經違反SRP原則了。讓我們解決這個問題。

class SpaceStation
  attr_reader :sensors, :supply_hold, :supply_reporter,
              :fuel_tank, :fuel_reporter, :thrusters
 
  def initialize
    @sensors = Sensors.new
    @supply_hold = SupplyHold.new
    @supply_reporter = SupplyReporter.new(@supply_hold)
    @fuel_tank = FuelTank.new
    @fuel_reporter = FuelReporter.new(@fuel_tank)
    @thrusters = Thrusters.new(@fuel_tank)
  end
end
 
class Sensors
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
  attr_reader :reporter
 
  def initialize
    @supplies = {}
  end
 
  def get_supplies
    @supplies
  end
 
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
  attr_reader :reporter
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
end
 
class Thrusters
  FUEL_PER_THRUST = 10
 
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    
    if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end
 
class Reporter
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
 
  def report
    puts "----- #{@type.capitalize} Report -----"
  end
end
 
class FuelReporter < Reporter
  def initialize(item)
    super(item, "fuel")
  end
 
  def report
    super
    puts "#{@linked_item.get_fuel_levels} units of fuel available."
  end
end
 
class SupplyReporter < Reporter
  def initialize(item)
    super(item, "supply")
  end
 
  def report
    super
    if @linked_item.get_supplies.keys.length > 0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end
 
iss = SpaceStation.new
 
iss.sensors.run_sensors
  # ----- Sensor Action -----
  # Running sensors!
 
iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Supply Error: Insufficient parts in the supply hold.
iss.supply_hold.load_supplies("parts", 10)
  # ----- Supply Action -----
  # Loading 10 units of parts in the supply hold.
iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Using 2 of parts from the supply hold.
iss.supply_reporter.report
  # ----- Supply Report -----
  # parts avalilable: 8 units
 
iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thruster Error: Insufficient fuel available.
iss.fuel_tank.load_fuel(100)
  # ----- Fuel Action -----
  # Loading 100 units of fuel in the tank.
iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thrusting action successful.
  # ----- Fuel Action -----
  # Using 10 units of fuel from the tank.
iss.fuel_reporter.report
  # ----- Fuel Report -----
# 90 units of fuel available.

在程式的最新版本中,職責已分為兩個新類別:FuelReporter 和 SupplyReporter。他們都是記者班的孩子。此外,我們也為 SpaceStation 類別新增了實例變量,以便在需要時可以初始化所需的子類別。現在,如果地球決定改變其他東西,那麼我們將對子類別進行更改,而不是對主類別進行更改。

當然,我們的一些班級還是互相依賴的。因此,SupplyReporter 物件依賴 SupplyHold,FuelReporter 依賴 FuelTank。當然,助推器必須連接到油箱。但這裡一切看起來都符合邏輯,並且進行更改不會特別困難 - 編輯一個物件的程式碼不會對另一個物件產生很大影響。

因此,我們創建了一個模組化程式碼,其中精確定義了每個物件/類別的職責。使用這樣的程式碼不是問題,維護它也將是一項簡單的任務。我們已經將整個“神物”轉換為SRP。

技能箱推薦:

來源: www.habr.com

為具有 DDoS 保護、VPS VDS 服務器的站點購買可靠的主機 🔥 購買具備 DDoS 防護的可靠網站寄存服務,包括 VPS 和 VDS 伺服器 | ProHoster