Viết mã linh hoạt bằng SOLID

Viết mã linh hoạt bằng SOLID

Từ người dịch: đã xuất bản cho bạn bài viết của Severin Perez về việc sử dụng các nguyên tắc SOLID trong lập trình. Thông tin từ bài viết sẽ hữu ích cho cả người mới bắt đầu và lập trình viên có kinh nghiệm.

Nếu bạn đang trong quá trình phát triển, rất có thể bạn đã nghe nói về các nguyên tắc RẮN. Chúng cho phép lập trình viên viết mã rõ ràng, có cấu trúc tốt và dễ bảo trì. Điều đáng chú ý là trong lập trình có một số cách tiếp cận để thực hiện chính xác một công việc cụ thể. Các chuyên gia khác nhau có quan điểm và cách hiểu khác nhau về “con đường đúng đắn”, tất cả phụ thuộc vào kinh nghiệm của mỗi người. Tuy nhiên, những ý tưởng được nêu trong SOLID được hầu hết các đại diện của cộng đồng CNTT chấp nhận. Chúng trở thành điểm khởi đầu cho sự xuất hiện và phát triển của nhiều phương pháp quản lý phát triển tốt.

Hãy hiểu các nguyên tắc RẮN là gì và chúng giúp ích cho chúng ta như thế nào.

Hộp kỹ năng khuyến nghị: khóa học thực hành "Nhà phát triển di động PRO".

Chúng tôi nhắc nhở: cho tất cả độc giả của "Habr" - giảm giá 10 rúp khi đăng ký bất kỳ khóa học Skillbox nào bằng mã khuyến mại "Habr".

RẮN là gì?

Thuật ngữ này là viết tắt, mỗi chữ cái trong thuật ngữ này là phần đầu tên của một nguyên tắc cụ thể:

Nguyên tắc trách nhiệm duy nhất


Nguyên tắc Trách nhiệm duy nhất (SRP) nêu rõ rằng mỗi lớp hoặc mô-đun trong một chương trình chỉ nên chịu trách nhiệm về một phần chức năng của chương trình đó. Ngoài ra, các thành phần của trách nhiệm này phải được gán cho lớp riêng của chúng, thay vì rải rác trên các lớp không liên quan. Nhà phát triển và nhà truyền giáo chính của SRP, Robert S. Martin, mô tả trách nhiệm giải trình là lý do cho sự thay đổi. Ban đầu ông đề xuất thuật ngữ này như một trong những yếu tố trong tác phẩm "Các nguyên tắc thiết kế hướng đối tượng" của mình. Khái niệm này kết hợp phần lớn mô hình kết nối đã được Tom DeMarco xác định trước đây.

Khái niệm này cũng bao gồm một số khái niệm do David Parnas hình thành. Hai cái chính là đóng gói và ẩn thông tin. Parnas lập luận rằng việc chia một hệ thống thành các mô-đun riêng biệt không nên dựa trên việc phân tích sơ đồ khối hoặc các luồng thực thi. Bất kỳ mô-đun nào cũng phải chứa một giải pháp cụ thể cung cấp tối thiểu thông tin cho khách hàng.

Nhân tiện, Martin đã đưa ra một ví dụ thú vị với các nhà quản lý cấp cao của một công ty (COO, CTO, CFO), mỗi người trong số họ sử dụng phần mềm kinh doanh cụ thể cho các mục đích khác nhau. Kết quả là, bất kỳ ai trong số họ đều có thể thực hiện các thay đổi trong phần mềm mà không ảnh hưởng đến lợi ích của những người quản lý khác.

Vật thần thánh

Như mọi khi, cách tốt nhất để tìm hiểu SRP là xem nó hoạt động như thế nào. Hãy xem xét một phần của chương trình KHÔNG tuân theo Nguyên tắc Trách nhiệm Duy nhất. Đây là mã Ruby mô tả hành vi và thuộc tính của trạm vũ trụ.

Xem lại ví dụ và cố gắng xác định những điều sau:
Trách nhiệm của những đối tượng được khai báo trong lớp SpaceStation.
Những người có thể quan tâm đến hoạt động của trạm vũ trụ.

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

Trên thực tế, trạm vũ trụ của chúng ta đang hoạt động không ổn định (tôi không nghĩ mình sẽ sớm nhận được cuộc gọi từ NASA), nhưng có điều gì đó cần phân tích ở đây.

Vì vậy, lớp SpaceStation có một số trách nhiệm (hoặc nhiệm vụ) khác nhau. Tất cả chúng có thể được chia thành các loại:

  • cảm biến;
  • vật tư (vật tư tiêu hao);
  • nhiên liệu;
  • máy gia tốc.

Mặc dù không có nhân viên nào của nhà ga được phân lớp, nhưng chúng ta có thể dễ dàng tưởng tượng ai chịu trách nhiệm về việc gì. Rất có thể, nhà khoa học điều khiển các cảm biến, nhà hậu cần chịu trách nhiệm cung cấp tài nguyên, kỹ sư chịu trách nhiệm cung cấp nhiên liệu và phi công điều khiển tên lửa đẩy.

Chúng tôi có thể nói rằng chương trình này không tuân thủ SRP không? Vâng, chắc chắn rồi. Nhưng lớp SpaceStation là một "đối tượng thần thánh" điển hình, biết mọi thứ và làm mọi thứ. Đây là một mô hình phản đối chính trong lập trình hướng đối tượng. Đối với người mới bắt đầu, những đồ vật như vậy cực kỳ khó bảo trì. Cho đến nay chương trình rất đơn giản, đúng vậy, nhưng hãy tưởng tượng điều gì sẽ xảy ra nếu chúng ta thêm các tính năng mới. Có lẽ trạm vũ trụ của chúng ta sẽ cần một trạm y tế hoặc một phòng họp. Và càng có nhiều chức năng thì SpaceStation sẽ càng phát triển. Chà, vì cơ sở này sẽ được kết nối với những cơ sở khác nên việc bảo trì toàn bộ khu phức hợp sẽ càng trở nên khó khăn hơn. Kết quả là, chúng ta có thể làm gián đoạn hoạt động của máy gia tốc chẳng hạn. Nếu nhà nghiên cứu yêu cầu thay đổi cảm biến, điều này rất có thể ảnh hưởng đến hệ thống thông tin liên lạc của trạm.

Vi phạm nguyên tắc SRP có thể mang lại chiến thắng ngắn hạn về mặt chiến thuật, nhưng cuối cùng chúng ta sẽ “thua trận”, và việc duy trì một con quái vật như vậy trong tương lai sẽ trở nên rất khó khăn. Tốt nhất nên chia chương trình thành các phần mã riêng biệt, mỗi phần mã chịu trách nhiệm thực hiện một thao tác cụ thể. Hiểu được điều này, hãy thay đổi lớp SpaceStation.

Hãy phân chia trách nhiệm

Ở trên, chúng tôi đã xác định bốn loại hoạt động được điều khiển bởi lớp SpaceStation. Chúng tôi sẽ ghi nhớ chúng khi tái cấu trúc. Mã cập nhật phù hợp hơn với 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

Có rất nhiều thay đổi, chương trình bây giờ chắc chắn trông đẹp hơn. Giờ đây, lớp SpaceStation của chúng tôi đã trở thành một thùng chứa trong đó các hoạt động được bắt đầu cho các bộ phận phụ thuộc, bao gồm một bộ cảm biến, hệ thống cung cấp vật tư tiêu hao, bình nhiên liệu và bộ tăng tốc.

Đối với bất kỳ biến nào hiện có một lớp tương ứng: Cảm biến; Cung cấp; Bình xăng; Bộ đẩy.

Có một số thay đổi quan trọng trong phiên bản mã này. Vấn đề là các hàm riêng lẻ không chỉ được gói gọn trong các lớp riêng của chúng mà còn được tổ chức theo cách sao cho có thể dự đoán được và nhất quán. Chúng tôi nhóm các phần tử có chức năng tương tự nhau để tuân theo nguyên tắc mạch lạc. Bây giờ, nếu chúng ta cần thay đổi cách hệ thống hoạt động, chuyển từ cấu trúc băm sang mảng, chỉ cần sử dụng lớp SupplyHold; chúng ta không cần phải động đến các mô-đun khác. Bằng cách này, nếu nhân viên hậu cần thay đổi thứ gì đó trong khu vực của mình thì phần còn lại của nhà ga sẽ vẫn nguyên vẹn. Trong trường hợp này, lớp SpaceStation thậm chí sẽ không nhận thức được những thay đổi.

Các sĩ quan của chúng tôi làm việc trên trạm vũ trụ có lẽ hài lòng về những thay đổi này vì họ có thể yêu cầu những thứ họ cần. Lưu ý rằng mã này có các phương thức như report_supplies và report_fuel chứa trong các lớp SupplyHold và FuelTank. Điều gì sẽ xảy ra nếu Trái đất yêu cầu thay đổi cách báo cáo? Cả hai lớp SupplyHold và FuelTank đều cần được thay đổi. Điều gì sẽ xảy ra nếu bạn cần thay đổi cách cung cấp nhiên liệu và vật tư tiêu hao? Bạn có thể sẽ phải thay đổi tất cả các lớp tương tự một lần nữa. Và điều này đã vi phạm nguyên tắc SRP. Hãy khắc phục điều này.

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.

Trong phiên bản mới nhất của chương trình này, trách nhiệm đã được chia thành hai lớp mới, FuelReporter và SupplyReporter. Cả hai đều là con của lớp Phóng viên. Ngoài ra, chúng tôi đã thêm các biến thể hiện vào lớp SpaceStation để lớp con mong muốn có thể được khởi tạo nếu cần. Bây giờ, nếu Trái đất quyết định thay đổi thứ gì đó khác, thì chúng ta sẽ thực hiện các thay đổi đối với các lớp con chứ không phải lớp chính.

Tất nhiên, một số lớp chúng tôi vẫn phụ thuộc vào nhau. Do đó, đối tượng SupplyReporter phụ thuộc vào SupplyHold và FuelReporter phụ thuộc vào FuelTank. Tất nhiên, bộ tăng tốc phải được kết nối với bình xăng. Nhưng ở đây mọi thứ đều có vẻ hợp lý và việc thực hiện các thay đổi sẽ không đặc biệt khó khăn - việc chỉnh sửa mã của một đối tượng sẽ không ảnh hưởng lớn đến đối tượng khác.

Vì vậy, chúng tôi đã tạo một mã mô-đun trong đó trách nhiệm của từng đối tượng/lớp được xác định chính xác. Làm việc với mã như vậy không phải là vấn đề, việc duy trì nó sẽ là một nhiệm vụ đơn giản. Chúng tôi đã chuyển đổi toàn bộ “vật thể thần thánh” thành SRP.

Hộp kỹ năng khuyến nghị:

Nguồn: www.habr.com

Thêm một lời nhận xét