Menulis kode fleksibel menggunakan SOLID

Menulis kode fleksibel menggunakan SOLID

Dari penerjemah: diterbitkan untuk Anda artikel oleh Severin Perez tentang penggunaan prinsip SOLID dalam pemrograman. Informasi dari artikel ini akan bermanfaat bagi pemula dan programmer berpengalaman.

Jika Anda tertarik pada pengembangan, kemungkinan besar Anda pernah mendengar prinsip SOLID. Mereka memungkinkan pemrogram untuk menulis kode yang bersih, terstruktur dengan baik, dan mudah dipelihara. Perlu dicatat bahwa dalam pemrograman ada beberapa pendekatan tentang cara melakukan pekerjaan tertentu dengan benar. Spesialis yang berbeda mempunyai gagasan dan pemahaman berbeda tentang β€œjalan yang benar”; semuanya bergantung pada pengalaman masing-masing orang. Namun ide yang dicanangkan dalam SOLID diterima oleh hampir seluruh perwakilan komunitas IT. Hal-hal tersebut menjadi titik awal munculnya dan berkembangnya banyak praktik manajemen pembangunan yang baik.

Mari kita pahami apa itu prinsip SOLID dan bagaimana prinsip tersebut membantu kita.

Skillbox merekomendasikan: Tentu saja praktis "Pengembang Seluler PRO".

Kami mengingatkan: untuk semua pembaca "Habr" - diskon 10 rubel saat mendaftar di kursus Skillbox apa pun menggunakan kode promosi "Habr".

Apa itu SOLID?

Istilah ini merupakan singkatan, setiap huruf dari istilah tersebut merupakan awal dari nama suatu prinsip tertentu:

  • SPrinsip Tanggung Jawab tunggal. Sebuah modul dapat memiliki satu dan hanya satu alasan untuk perubahan.
  • Grafik Opena/Prinsip Tertutup (prinsip terbuka/tertutup). Kelas dan elemen lainnya harus terbuka untuk perluasan, namun tertutup untuk modifikasi.
  • β€ŠGrafik LPrinsip Substitusi iskov (Prinsip substitusi Liskov). Fungsi yang menggunakan tipe dasar harus dapat menggunakan subtipe dari tipe dasar tanpa menyadarinya.
  • Grafik IPrinsip Pemisahan Antarmukaβ€Š (prinsip pemisahan antarmuka). Entitas perangkat lunak tidak boleh bergantung pada metode yang tidak mereka gunakan.
  • Grafik DPrinsip Inversi ketergantungan (prinsip inversi ketergantungan). Modul di tingkat yang lebih tinggi tidak boleh bergantung pada modul di tingkat yang lebih rendah.

Prinsip Tanggung Jawab Tunggal

β€Š
Prinsip Tanggung Jawab Tunggal (SRP) menyatakan bahwa setiap kelas atau modul dalam suatu program harus bertanggung jawab hanya pada satu bagian dari fungsionalitas program tersebut. Selain itu, unsur-unsur tanggung jawab ini harus ditugaskan ke kelas mereka sendiri, bukan tersebar di kelas-kelas yang tidak terkait. Pengembang SRP dan kepala penginjil, Robert S. Martin, menggambarkan akuntabilitas sebagai alasan perubahan. Dia awalnya mengusulkan istilah ini sebagai salah satu elemen karyanya "Principles of Object-Oriented Design". Konsep ini menggabungkan banyak pola konektivitas yang sebelumnya didefinisikan oleh Tom DeMarco.

Konsep tersebut juga mencakup beberapa konsep yang dirumuskan oleh David Parnas. Dua yang utama adalah enkapsulasi dan penyembunyian informasi. Parnas berpendapat bahwa membagi sistem menjadi modul-modul terpisah tidak boleh didasarkan pada analisis diagram blok atau alur eksekusi. Setiap modul harus berisi solusi spesifik yang memberikan informasi minimum kepada klien.

Ngomong-ngomong, Martin memberikan contoh menarik dengan manajer senior sebuah perusahaan (COO, CTO, CFO), yang masing-masing menggunakan perangkat lunak bisnis tertentu untuk tujuan berbeda. Akibatnya, salah satu dari mereka dapat menerapkan perubahan pada perangkat lunak tanpa mempengaruhi kepentingan manajer lainnya.

Objek ilahi

Seperti biasa, cara terbaik untuk mempelajari SRP adalah dengan melihatnya secara langsung. Mari kita lihat bagian dari program yang TIDAK mengikuti Prinsip Tanggung Jawab Tunggal. Ini adalah kode Ruby yang menjelaskan perilaku dan atribut stasiun luar angkasa.

Tinjau contohnya dan coba tentukan hal berikut:
Tanggung jawab objek yang dideklarasikan di kelas SpaceStation.
Mereka yang mungkin tertarik dengan pengoperasian stasiun luar angkasa.

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, stasiun luar angkasa kita tidak berfungsi (saya rasa saya tidak akan menerima telepon dari NASA dalam waktu dekat), tetapi ada sesuatu yang perlu dianalisis di sini.

Dengan demikian, kelas SpaceStation memiliki beberapa tanggung jawab (atau tugas) yang berbeda. Semuanya dapat dibagi menjadi beberapa jenis:

  • sensor;
  • persediaan (bahan habis pakai);
  • bahan bakar;
  • akselerator.

Meskipun tidak ada pegawai stasiun yang diberi kelas, kita dapat dengan mudah membayangkan siapa yang bertanggung jawab atas apa. Kemungkinan besar, ilmuwan mengendalikan sensor, ahli logistik bertanggung jawab menyediakan sumber daya, insinyur bertanggung jawab atas pasokan bahan bakar, dan pilot mengendalikan booster.

Dapatkah kami mengatakan bahwa program ini tidak sesuai dengan SRP? Ya tentu. Tapi kelas SpaceStation adalah tipikal "objek dewa" yang mengetahui segalanya dan melakukan segalanya. Ini adalah anti-pola utama dalam pemrograman berorientasi objek. Bagi seorang pemula, benda seperti itu sangat sulit dirawat. Sejauh ini programnya sangat sederhana ya, tapi bayangkan apa jadinya jika kita menambahkan fitur baru. Mungkin stasiun luar angkasa kita memerlukan stasiun medis atau ruang pertemuan. Dan semakin banyak fungsinya, SpaceStation akan semakin berkembang. Karena fasilitas ini akan terhubung dengan fasilitas lain, pelayanan seluruh kompleks akan menjadi lebih sulit. Akibatnya, kita bisa mengganggu pengoperasian, misalnya akselerator. Jika peneliti meminta perubahan pada sensor, hal ini dapat berdampak besar pada sistem komunikasi stasiun.

Melanggar prinsip SRP mungkin memberikan kemenangan taktis jangka pendek, namun pada akhirnya kita akan β€œkalah perang”, dan akan menjadi sangat sulit untuk mempertahankan monster seperti itu di masa depan. Yang terbaik adalah membagi program menjadi beberapa bagian kode yang terpisah, yang masing-masing bertanggung jawab untuk melakukan operasi tertentu. Memahami hal ini, mari kita ubah kelas SpaceStation.

Mari kita mendistribusikan tanggung jawab

Di atas kami mendefinisikan empat jenis operasi yang dikendalikan oleh kelas SpaceStation. Kami akan mengingatnya saat melakukan refactoring. Kode yang diperbarui lebih cocok 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

Banyak perubahannya, programnya pasti terlihat lebih baik sekarang. Kini kelas Stasiun Luar Angkasa kami telah menjadi lebih dari sebuah wadah tempat pengoperasian bagian-bagian yang bergantung dimulai, termasuk seperangkat sensor, sistem pasokan habis pakai, tangki bahan bakar, dan booster.

Untuk salah satu variabel sekarang ada kelas yang sesuai: Sensor; Penahanan Pasokan; Tangki bahan bakar; Pendorong.

Ada beberapa perubahan penting pada versi kode ini. Intinya adalah bahwa fungsi-fungsi individual tidak hanya dikemas dalam kelasnya sendiri, tetapi juga diatur sedemikian rupa agar dapat diprediksi dan konsisten. Kami mengelompokkan elemen dengan fungsi serupa untuk mengikuti prinsip koherensi. Sekarang, jika kita perlu mengubah cara kerja sistem, berpindah dari struktur hash ke array, cukup gunakan kelas SupplyHold; kita tidak perlu menyentuh modul lain. Dengan cara ini, jika petugas logistik mengubah sesuatu di bagiannya, bagian stasiun lainnya akan tetap utuh. Dalam hal ini, kelas SpaceStation bahkan tidak akan menyadari perubahan tersebut.

Petugas kami yang bekerja di stasiun luar angkasa mungkin senang dengan perubahan ini karena mereka dapat meminta perubahan yang mereka butuhkan. Perhatikan bahwa kode tersebut memiliki metode seperti report_supplies dan report_fuel yang terdapat dalam kelas SupplyHold dan FuelTank. Apa yang akan terjadi jika Bumi meminta perubahan dalam cara pelaporannya? Kedua kelas, SupplyHold dan FuelTank, perlu diubah. Bagaimana jika Anda perlu mengubah cara pengiriman bahan bakar dan bahan habis pakai? Anda mungkin harus mengubah semua kelas yang sama lagi. Dan ini sudah merupakan pelanggaran prinsip SRP. Mari kita perbaiki 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 program versi terbaru ini, tanggung jawab telah dibagi menjadi dua kelas baru, FuelReporter dan SupplyReporter. Mereka berdua adalah anak kelas Reporter. Selain itu, kami menambahkan variabel instan ke kelas SpaceStation sehingga subkelas yang diinginkan dapat diinisialisasi jika diperlukan. Sekarang, jika Bumi memutuskan untuk mengubah sesuatu yang lain, maka kita akan membuat perubahan pada subkelasnya, dan bukan pada kelas utama.

Tentu saja, beberapa kelas kami masih bergantung satu sama lain. Jadi, objek SupplyReporter bergantung pada SupplyHold, dan FuelReporter bergantung pada FuelTank. Tentu saja boosternya harus terhubung dengan tangki bahan bakar. Namun di sini semuanya sudah terlihat logis, dan membuat perubahan tidak akan terlalu sulit - mengedit kode satu objek tidak akan terlalu memengaruhi objek lainnya.

Jadi, kami telah membuat kode modular di mana tanggung jawab masing-masing objek/kelas didefinisikan secara tepat. Bekerja dengan kode seperti itu bukanlah suatu masalah, memeliharanya akan menjadi tugas yang sederhana. Kami telah mengubah seluruh β€œobjek ilahi” menjadi SRP.

Skillbox merekomendasikan:

Sumber: www.habr.com

Tambah komentar