Menulis kod fleksibel menggunakan SOLID

Menulis kod fleksibel menggunakan SOLID

Daripada penterjemah: diterbitkan untuk anda artikel oleh Severin Perez tentang menggunakan prinsip SOLID dalam pengaturcaraan. Maklumat daripada artikel akan berguna kepada kedua-dua pemula dan pengaturcara berpengalaman.

Jika anda berminat dengan pembangunan, kemungkinan besar anda pernah mendengar tentang prinsip SOLID. Mereka membolehkan pengaturcara menulis kod yang bersih, tersusun dengan baik dan mudah diselenggara. Perlu diingat bahawa dalam pengaturcaraan terdapat beberapa pendekatan bagaimana untuk melaksanakan tugas tertentu dengan betul. Pakar yang berbeza mempunyai idea dan pemahaman yang berbeza tentang "jalan yang betul"; semuanya bergantung pada pengalaman setiap orang. Walau bagaimanapun, idea yang diisytiharkan dalam SOLID diterima oleh hampir semua wakil komuniti IT. Mereka menjadi titik permulaan untuk kemunculan dan perkembangan banyak amalan pengurusan pembangunan yang baik.

Mari kita fahami apakah prinsip SOLID dan cara ia membantu kita.

Skillbox mengesyorkan: Kursus praktikal "Pro Pembangun Mudah Alih".

Kami mengingatkan: untuk semua pembaca "Habr" - diskaun sebanyak 10 rubel apabila mendaftar dalam mana-mana kursus Skillbox menggunakan kod promosi "Habr".

Apa itu SOLID?

Istilah ini adalah singkatan, setiap huruf istilah adalah permulaan nama prinsip tertentu:

  • SPrinsip Tanggungjawab. Modul boleh mempunyai satu dan hanya satu sebab untuk perubahan.
  • . Open/Prinsip Tertutup (prinsip terbuka/tertutup). Kelas dan elemen lain harus dibuka untuk sambungan, tetapi ditutup untuk pengubahsuaian.
  • β€Š. LPrinsip Penggantian iskov (prinsip penggantian Liskov). Fungsi yang menggunakan jenis asas sepatutnya boleh menggunakan subjenis jenis asas tanpa mengetahuinya.
  • . IPrinsip Pengasingan Antara Mukaβ€Š (prinsip pemisahan antara muka). Entiti perisian tidak boleh bergantung pada kaedah yang mereka tidak gunakan.
  • . DPrinsip Inversi ependency (prinsip penyongsangan kebergantungan). Modul pada tahap yang lebih tinggi seharusnya tidak bergantung pada modul pada tahap yang lebih rendah.

Prinsip Tanggungjawab Tunggal

β€Š
Prinsip Tanggungjawab Tunggal (SRP) menyatakan bahawa setiap kelas atau modul dalam program harus bertanggungjawab untuk hanya satu bahagian kefungsian program tersebut. Selain itu, elemen tanggungjawab ini harus diberikan kepada kelas mereka sendiri, bukannya tersebar di seluruh kelas yang tidak berkaitan. Pemaju dan ketua penginjil SRP, Robert S. Martin, menggambarkan akauntabiliti sebagai sebab perubahan. Beliau pada asalnya mencadangkan istilah ini sebagai salah satu elemen karyanya "Principles of Object-Oriented Design". Konsep ini menggabungkan banyak corak ketersambungan yang sebelum ini ditakrifkan oleh Tom DeMarco.

Konsep tersebut turut merangkumi beberapa konsep yang dirumuskan oleh David Parnas. Dua yang utama ialah enkapsulasi dan penyembunyian maklumat. Parnas berhujah bahawa membahagikan sistem kepada modul berasingan tidak seharusnya berdasarkan analisis gambar rajah blok atau aliran pelaksanaan. Mana-mana modul mesti mengandungi penyelesaian khusus yang menyediakan maklumat minimum kepada pelanggan.

By the way, Martin memberikan contoh yang menarik dengan pengurus kanan sebuah syarikat (COO, CTO, CFO), yang masing-masing menggunakan perisian perniagaan khusus untuk tujuan yang berbeza. Akibatnya, mana-mana daripada mereka boleh melaksanakan perubahan dalam perisian tanpa menjejaskan kepentingan pengurus lain.

Objek ketuhanan

Seperti biasa, cara terbaik untuk mempelajari SRP ialah melihatnya dalam tindakan. Mari kita lihat bahagian program yang TIDAK mengikut Prinsip Tanggungjawab Tunggal. Ini ialah kod Ruby yang menerangkan tingkah laku dan sifat stesen angkasa.

Semak contoh dan cuba tentukan perkara berikut:
Tanggungjawab objek tersebut yang diisytiharkan dalam kelas SpaceStation.
Mereka yang mungkin berminat dengan operasi stesen angkasa lepas.

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

Sebenarnya, stesen angkasa kami tidak berfungsi (saya rasa saya tidak akan menerima panggilan daripada NASA dalam masa terdekat), tetapi ada sesuatu untuk dianalisis di sini.

Oleh itu, kelas SpaceStation mempunyai beberapa tanggungjawab (atau tugasan) yang berbeza. Kesemuanya boleh dibahagikan kepada jenis:

  • penderia;
  • bekalan (bahan habis pakai);
  • bahan api;
  • pemecut.

Walaupun tidak ada pekerja stesen yang diberikan kelas, kita boleh membayangkan dengan mudah siapa yang bertanggungjawab untuk apa. Kemungkinan besar, saintis mengawal penderia, logistik bertanggungjawab untuk membekalkan sumber, jurutera bertanggungjawab untuk bekalan bahan api, dan juruterbang mengawal penggalak.

Bolehkah kita mengatakan bahawa program ini tidak mematuhi SRP? Ya pasti. Tetapi kelas SpaceStation ialah "objek tuhan" biasa yang mengetahui segala-galanya dan melakukan segala-galanya. Ini adalah anti-corak utama dalam pengaturcaraan berorientasikan objek. Bagi pemula, objek sedemikian amat sukar untuk dikekalkan. Setakat ini program ini sangat mudah, ya, tetapi bayangkan apa yang akan berlaku jika kita menambah ciri-ciri baru. Mungkin stesen angkasa kita memerlukan stesen perubatan atau bilik mesyuarat. Dan lebih banyak fungsi yang ada, lebih banyak SpaceStation akan berkembang. Oleh kerana kemudahan ini akan disambungkan kepada orang lain, perkhidmatan keseluruhan kompleks akan menjadi lebih sukar. Akibatnya, kita boleh mengganggu operasi, sebagai contoh, pemecut. Jika penyelidik meminta perubahan pada penderia, ini boleh menjejaskan sistem komunikasi stesen.

Melanggar prinsip SRP mungkin memberikan kemenangan taktikal jangka pendek, tetapi pada akhirnya kita akan "kalah dalam perang", dan akan menjadi sangat sukar untuk mengekalkan raksasa seperti itu pada masa akan datang. Adalah lebih baik untuk membahagikan program kepada bahagian kod yang berasingan, setiap satunya bertanggungjawab untuk melaksanakan operasi tertentu. Memahami perkara ini, mari tukar kelas SpaceStation.

Mari agihkan tanggungjawab

Di atas kami menentukan empat jenis operasi yang dikawal oleh kelas SpaceStation. Kami akan mengingati mereka semasa pemfaktoran semula. Kod yang dikemas kini lebih sepadan dengan 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

Terdapat banyak perubahan, program ini pasti kelihatan lebih baik sekarang. Kini kelas SpaceStation kami telah menjadi lebih kepada bekas di mana operasi dimulakan untuk bahagian bergantung, termasuk satu set penderia, sistem bekalan boleh guna, tangki bahan api dan penggalak.

Untuk mana-mana pembolehubah kini terdapat kelas yang sepadan: Sensor; SupplyHold; Tangki bahan api; Pendorong.

Terdapat beberapa perubahan penting dalam versi kod ini. Intinya ialah fungsi individu bukan sahaja terkandung dalam kelas mereka sendiri, ia diatur sedemikian rupa untuk menjadi boleh diramal dan konsisten. Kami mengumpulkan elemen dengan fungsi yang serupa untuk mengikut prinsip keselarasan. Sekarang, jika kita perlu menukar cara sistem berfungsi, beralih daripada struktur cincang kepada tatasusunan, hanya gunakan kelas SupplyHold; kita tidak perlu menyentuh modul lain. Dengan cara ini, jika pegawai logistik menukar sesuatu di bahagiannya, seluruh stesen akan kekal utuh. Dalam kes ini, kelas SpaceStation tidak akan menyedari perubahan itu.

Pegawai kami yang bekerja di stesen angkasa mungkin gembira dengan perubahan kerana mereka boleh meminta yang mereka perlukan. Perhatikan bahawa kod tersebut mempunyai kaedah seperti report_supplies dan report_fuel yang terkandung dalam kelas SupplyHold dan FuelTank. Apakah yang akan berlaku jika Bumi meminta mengubah cara ia melaporkan? Kedua-dua kelas, SupplyHold dan FuelTank, perlu ditukar. Bagaimana jika anda perlu menukar cara penghantaran bahan api dan bahan habis pakai? Anda mungkin perlu menukar semua kelas yang sama sekali lagi. Dan ini sudah pun melanggar prinsip SRP. Mari kita betulkan ini.

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.

Dalam versi terbaru program ini, tanggungjawab telah dibahagikan kepada dua kelas baharu, FuelReporter dan SupplyReporter. Mereka berdua adalah anak kelas Reporter. Di samping itu, kami menambah pembolehubah contoh pada kelas SpaceStation supaya subkelas yang dikehendaki boleh dimulakan jika perlu. Sekarang, jika Bumi memutuskan untuk menukar sesuatu yang lain, maka kita akan membuat perubahan kepada subkelas, dan bukan kepada kelas utama.

Sudah tentu, beberapa kelas kami masih bergantung antara satu sama lain. Oleh itu, objek SupplyReporter bergantung pada SupplyHold, dan FuelReporter bergantung pada FuelTank. Sudah tentu, penggalak mesti disambungkan ke tangki bahan api. Tetapi di sini semuanya sudah kelihatan logik, dan membuat perubahan tidak akan menjadi sangat sukar - mengedit kod satu objek tidak akan menjejaskan yang lain.

Oleh itu, kami telah mencipta kod modular di mana tanggungjawab setiap objek/kelas ditakrifkan dengan tepat. Bekerja dengan kod sedemikian tidak menjadi masalah, mengekalkannya akan menjadi tugas yang mudah. Kami telah menukar keseluruhan "objek ketuhanan" kepada SRP.

Skillbox mengesyorkan:

Sumber: www.habr.com

Tambah komen