การเขียนโค้ดแบบยืดหยุ่นโดยใช้ SOLID

การเขียนโค้ดแบบยืดหยุ่นโดยใช้ SOLID

จากผู้แปล: เผยแพร่สำหรับคุณ บทความโดย เซเวริน เปเรซ เกี่ยวกับการใช้หลักการ SOLID ในการเขียนโปรแกรม ข้อมูลจากบทความนี้จะเป็นประโยชน์สำหรับทั้งผู้เริ่มต้นและโปรแกรมเมอร์ที่มีประสบการณ์

หากคุณกำลังเข้าสู่การพัฒนา คุณน่าจะเคยได้ยินเกี่ยวกับหลักการ SOLID มาก่อน ช่วยให้โปรแกรมเมอร์สามารถเขียนโค้ดที่สะอาด มีโครงสร้างดี และบำรุงรักษาได้ง่าย เป็นที่น่าสังเกตว่าในการเขียนโปรแกรมมีหลายวิธีในการปฏิบัติงานเฉพาะอย่างถูกต้อง ผู้เชี่ยวชาญแต่ละคนมีแนวคิดและความเข้าใจใน “แนวทางที่ถูกต้อง” ที่แตกต่างกัน ทั้งนี้ขึ้นอยู่กับประสบการณ์ของแต่ละคน อย่างไรก็ตาม แนวคิดที่ประกาศใน SOLID ได้รับการยอมรับจากตัวแทนชุมชนไอทีเกือบทั้งหมด พวกเขากลายเป็นจุดเริ่มต้นของการพัฒนาแนวทางการจัดการการพัฒนาที่ดีหลายประการ

เรามาทำความเข้าใจว่าหลักการ SOLID คืออะไร และหลักการเหล่านั้นช่วยเราได้อย่างไร

Skillbox แนะนำ: หลักสูตรภาคปฏิบัติ "นักพัฒนามือถือ PRO".

เราเตือนคุณ: สำหรับผู้อ่าน "Habr" ทุกคน - ส่วนลด 10 rubles เมื่อลงทะเบียนในหลักสูตร Skillbox ใด ๆ โดยใช้รหัสส่งเสริมการขาย "Habr"

โซลิดคืออะไร?

คำนี้เป็นคำย่อ โดยแต่ละตัวอักษรจะเป็นจุดเริ่มต้นของชื่อของหลักการเฉพาะ:

หลักการความรับผิดชอบเดียว


หลักการความรับผิดชอบเดี่ยว (SRP) ระบุว่าแต่ละคลาสหรือโมดูลในโปรแกรมควรรับผิดชอบเพียงส่วนหนึ่งของฟังก์ชันการทำงานของโปรแกรมนั้น นอกจากนี้ องค์ประกอบของความรับผิดชอบนี้ควรถูกกำหนดให้กับชั้นเรียนของตนเอง แทนที่จะกระจายไปตามชั้นเรียนที่ไม่เกี่ยวข้อง Robert S. Martin ผู้พัฒนาและหัวหน้าผู้เผยแพร่ของ SRP อธิบายว่าความรับผิดชอบเป็นเหตุผลของการเปลี่ยนแปลง เดิมทีเขาเสนอคำนี้ว่าเป็นหนึ่งในองค์ประกอบของงานของเขา "หลักการของการออกแบบเชิงวัตถุ" แนวคิดนี้รวมเอารูปแบบการเชื่อมต่อส่วนใหญ่ที่ Tom DeMarco กำหนดไว้ก่อนหน้านี้

แนวคิดนี้ยังรวมถึงแนวคิดหลายประการที่จัดทำโดย David Parnas สองสิ่งหลักคือการห่อหุ้มและการซ่อนข้อมูล Parnas แย้งว่าการแบ่งระบบออกเป็นโมดูลที่แยกจากกันไม่ควรขึ้นอยู่กับการวิเคราะห์บล็อกไดอะแกรมหรือโฟลว์การดำเนินการ โมดูลใดๆ จะต้องมีโซลูชันเฉพาะที่ให้ข้อมูลขั้นต่ำแก่ลูกค้า

อย่างไรก็ตาม 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 จะไม่รับรู้ถึงการเปลี่ยนแปลงด้วยซ้ำ

เจ้าหน้าที่ของเราที่ทำงานในสถานีอวกาศอาจพอใจกับการเปลี่ยนแปลงนี้ เนื่องจากพวกเขาสามารถขอสิ่งที่ต้องการได้ โปรดสังเกตว่าโค้ดมีวิธีต่างๆ เช่น report_supplies และ report_fuel ที่มีอยู่ในคลาส SupplyHold และ FuelTank จะเกิดอะไรขึ้นหาก Earth ขอให้เปลี่ยนวิธีการรายงาน ทั้งสองคลาส 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 พวกเขาทั้งคู่เป็นลูกของคลาส Reporter นอกจากนี้ เรายังเพิ่มตัวแปรอินสแตนซ์ให้กับคลาส SpaceStation เพื่อให้สามารถเริ่มต้นคลาสย่อยที่ต้องการได้หากจำเป็น ตอนนี้ หากโลกตัดสินใจที่จะเปลี่ยนแปลงอย่างอื่น เราจะทำการเปลี่ยนแปลงคลาสย่อย ไม่ใช่คลาสหลัก

แน่นอนว่าบางชั้นเรียนของเรายังคงต้องพึ่งพาซึ่งกันและกัน ดังนั้น วัตถุ SupplyReporter ขึ้นอยู่กับ SupplyHold และ FuelReporter ขึ้นอยู่กับ FuelTank แน่นอนว่าต้องต่อบูสเตอร์เข้ากับถังน้ำมันเชื้อเพลิง แต่ที่นี่ทุกอย่างดูสมเหตุสมผลอยู่แล้วและการเปลี่ยนแปลงจะไม่ใช่เรื่องยากโดยเฉพาะ - การแก้ไขโค้ดของวัตถุหนึ่งจะไม่ส่งผลกระทบอย่างมากต่ออีกวัตถุหนึ่ง

ดังนั้นเราจึงได้สร้างโค้ดโมดูลาร์ที่มีการกำหนดความรับผิดชอบของแต่ละอ็อบเจ็กต์/คลาสอย่างแม่นยำ การทำงานกับโค้ดดังกล่าวไม่ใช่ปัญหา การรักษามันจะเป็นงานง่ายๆ เราได้แปลง "วัตถุศักดิ์สิทธิ์" ทั้งหมดให้เป็น SRP

Skillbox แนะนำ:

ที่มา: will.com

เพิ่มความคิดเห็น