SOLID kullanarak esnek kod yazma

SOLID kullanarak esnek kod yazma

Tercümandan: sizin için yayınlandı Severin Perez'in makalesi SOLID prensiplerinin programlamada kullanılması hakkında. Makaledeki bilgiler hem yeni başlayanlar hem de deneyimli programcılar için faydalı olacaktır.

Geliştirmeyle ilgileniyorsanız, büyük olasılıkla SOLID ilkelerini duymuşsunuzdur. Programcının temiz, iyi yapılandırılmış ve bakımı kolay kod yazmasını sağlar. Programlamada belirli bir işin doğru şekilde nasıl gerçekleştirileceğine dair çeşitli yaklaşımlar olduğunu belirtmekte fayda var. Farklı uzmanların "doğru yol" konusunda farklı fikirleri ve anlayışları vardır; bu tamamen her kişinin deneyimine bağlıdır. Ancak SOLID'de öne sürülen fikirler, BT camiasının neredeyse tüm temsilcileri tarafından kabul ediliyor. Pek çok iyi geliştirme yönetimi uygulamasının ortaya çıkması ve gelişmesinin başlangıç ​​noktası oldular.

SOLID ilkelerinin ne olduğunu ve bize nasıl yardımcı olduklarını anlayalım.

Skillbox şunları önerir: pratik kurs "Mobil Geliştirici PRO".

Hatırlatıyoruz: tüm "Habr" okuyucuları için - "Habr" promosyon kodunu kullanarak herhangi bir Skillbox kursuna kayıt olurken 10 ruble indirim.

KATI nedir?

Bu terim bir kısaltmadır, terimin her harfi belirli bir ilkenin adının başlangıcıdır:

  • Stek Sorumluluk İlkesi. Bir modülün değişim için tek bir nedeni olabilir.
  • The Okalem/Kapalı Prensip (açık/kapalı prensibi). Sınıflar ve diğer öğeler genişletmeye açık, değişiklik yapmaya kapalı olmalıdır.
  •  The Liskov Değiştirme Prensibi (Liskov ikame ilkesi). Temel tür kullanan işlevler, temel türün alt türlerini bilmeden kullanabilmelidir.
  • The IArayüz Ayırma Prensibi  (arayüz ayırma ilkesi). Yazılım varlıkları kullanmadıkları yöntemlere bağımlı olmamalıdır.
  • The Dbağımlılık Ters Çevirme Prensibi (bağımlılığın tersine çevrilmesi ilkesi). Daha yüksek seviyelerdeki modüller, daha düşük seviyelerdeki modüllere bağlı olmamalıdır.

Tek Sorumluluk Prensibi


Tek Sorumluluk İlkesi (SRP), bir programdaki her sınıf veya modülün, o programın işlevselliğinin yalnızca bir kısmından sorumlu olması gerektiğini belirtir. Ek olarak, bu sorumluluğun unsurları ilgisiz sınıflara dağılmak yerine kendi sınıflarına atanmalıdır. SRP'nin geliştiricisi ve baş müjdecisi Robert S. Martin, değişimin nedeni olarak hesap verebilirliği tanımlıyor. Başlangıçta bu terimi "Nesneye Yönelik Tasarımın İlkeleri" adlı çalışmasının unsurlarından biri olarak önerdi. Konsept, daha önce Tom DeMarco tarafından tanımlanan bağlantı modelinin çoğunu içeriyor.

Konsept aynı zamanda David Parnas tarafından formüle edilen çeşitli konseptleri de içeriyordu. Bunlardan iki tanesi kapsülleme ve bilgi gizlemedir. Parnas, bir sistemi ayrı modüllere bölmenin blok diyagramların veya yürütme akışlarının analizine dayanmaması gerektiğini savundu. Modüllerden herhangi birinin, istemcilere minimum düzeyde bilgi sağlayan özel bir çözüm içermesi gerekir.

Bu arada Martin, her biri farklı amaçlar için belirli iş yazılımlarını kullanan bir şirketin üst düzey yöneticilerinden (COO, CTO, CFO) ilginç bir örnek verdi. Sonuç olarak, herhangi biri diğer yöneticilerin çıkarlarını etkilemeden yazılımda değişiklik yapabilir.

İlahi nesne

Her zaman olduğu gibi SRP'yi öğrenmenin en iyi yolu onu çalışırken görmektir. Programın Tek Sorumluluk İlkesine uymayan bir bölümüne bakalım. Bu, uzay istasyonunun davranışını ve niteliklerini açıklayan Ruby kodudur.

Örneği inceleyin ve aşağıdakileri belirlemeye çalışın:
SpaceStation sınıfında bildirilen nesnelerin sorumlulukları.
Uzay istasyonunun işleyişiyle ilgilenebilecekler.

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

Aslında uzay istasyonumuz işlevsiz (yakında NASA'dan bir telefon alacağımı sanmıyorum), ama burada analiz edilmesi gereken bir şey var.

Dolayısıyla SpaceStation sınıfının birkaç farklı sorumluluğu (veya görevi) vardır. Hepsi türlere ayrılabilir:

  • sensörler;
  • sarf malzemeleri (sarf malzemeleri);
  • yakıt;
  • hızlandırıcılar.

Her ne kadar istasyon çalışanlarının hiçbirine bir ders verilmemiş olsa da kimin neyden sorumlu olduğunu rahatlıkla tahmin edebiliyoruz. Büyük olasılıkla, bilim adamı sensörleri kontrol ediyor, lojistikçi kaynak sağlamaktan sorumlu, mühendis yakıt tedarikinden sorumlu ve pilot iticileri kontrol ediyor.

Bu programın SRP uyumlu olmadığını söyleyebilir miyiz? Evet elbette. Ancak SpaceStation sınıfı her şeyi bilen ve her şeyi yapan tipik bir "tanrı nesnesidir". Bu, nesne yönelimli programlamada önemli bir anti-örüntüdür. Yeni başlayanlar için bu tür nesnelerin bakımı son derece zordur. Buraya kadar program çok basit evet ama yeni özellikler eklersek ne olacağını bir düşünün. Belki uzay istasyonumuzun bir sağlık istasyonuna veya toplantı odasına ihtiyacı olacaktır. Ve ne kadar çok işlev varsa, SpaceStation da o kadar büyüyecek. Bu tesis diğerlerine bağlanacağı için kompleksin tamamına hizmet vermek daha da karmaşık hale gelecek. Sonuç olarak, örneğin hızlandırıcıların çalışmasını bozabiliriz. Bir araştırmacının sensörlerde değişiklik talep etmesi, istasyonun iletişim sistemlerini pekala etkileyebilir.

SRP prensibini ihlal etmek kısa vadeli bir taktiksel zafer sağlayabilir, ancak sonunda “savaşı kaybedeceğiz” ve gelecekte böyle bir canavarı sürdürmek çok zor hale gelecektir. Programı, her biri belirli bir işlemi gerçekleştirmekten sorumlu olan ayrı kod bölümlerine bölmek en iyisidir. Bunu anlayarak SpaceStation sınıfını değiştirelim.

Sorumluluğu dağıtalım

Yukarıda SpaceStation sınıfı tarafından kontrol edilen dört tür işlemi tanımladık. Yeniden düzenleme yaparken bunları aklımızda tutacağız. Güncellenen kod SRP ile daha iyi eşleşir.

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

Pek çok değişiklik var, program artık kesinlikle daha iyi görünüyor. Artık SpaceStation sınıfımız, bir dizi sensör, sarf malzemesi tedarik sistemi, yakıt deposu ve güçlendiriciler dahil olmak üzere bağımlı parçalar için operasyonların başlatıldığı bir konteynır haline geldi.

Artık değişkenlerden herhangi biri için karşılık gelen bir sınıf var: Sensörler; Tedarik Tutma; Yakıt tankı; İticiler.

Kodun bu sürümünde birkaç önemli değişiklik var. Buradaki önemli nokta, bireysel işlevlerin yalnızca kendi sınıfları içinde kapsanması değil, aynı zamanda öngörülebilir ve tutarlı olacak şekilde organize edilmiş olmasıdır. Tutarlılık ilkesini takip etmek için benzer işlevlere sahip öğeleri gruplandırıyoruz. Şimdi, hash yapısından diziye geçerek sistemin çalışma şeklini değiştirmemiz gerekiyorsa, sadece SupplyHold sınıfını kullanın; diğer modüllere dokunmamıza gerek yok. Böylece lojistik memuru kendi bölümünde bir değişiklik yaparsa istasyonun geri kalanı sağlam kalacaktır. Bu durumda SpaceStation sınıfı değişikliklerin farkında bile olmayacaktır.

Uzay istasyonunda çalışan memurlarımız muhtemelen ihtiyaç duydukları değişiklikleri talep edebildikleri için değişikliklerden memnunlar. Kodun SupplyHold ve FuelTank sınıflarında bulunan report_supplies ve report_fuel gibi yöntemlere sahip olduğuna dikkat edin. Dünya raporlama şeklini değiştirmek isterse ne olurdu? SupplyHold ve FuelTank sınıflarının her ikisinin de değiştirilmesi gerekecektir. Yakıt ve sarf malzemelerinin teslim edilme şeklini değiştirmeniz gerekirse ne olur? Muhtemelen aynı sınıfların hepsini tekrar değiştirmeniz gerekecek. Ve bu zaten SRP ilkesinin ihlalidir. Bunu düzeltelim.

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.

Programın bu son versiyonunda sorumluluklar FuelReporter ve SupplyReporter olmak üzere iki yeni sınıfa ayrılmıştır. Her ikisi de Muhabir sınıfının çocuklarıdır. Ayrıca gerektiğinde istenilen alt sınıfın başlatılabilmesi için SpaceStation sınıfına örnek değişkenler ekledik. Şimdi, eğer Dünya başka bir şeyi değiştirmeye karar verirse, o zaman değişiklikleri ana sınıfta değil, alt sınıflarda yapacağız.

Elbette bazı derslerimiz hala birbirine bağlı. Dolayısıyla SupplyReporter nesnesi SupplyHold'a, FuelReporter ise FuelTank'e bağlıdır. Elbette hidroforların yakıt deposuna bağlanması gerekiyor. Ancak burada her şey zaten mantıklı görünüyor ve değişiklik yapmak özellikle zor olmayacak - bir nesnenin kodunu düzenlemek diğerini büyük ölçüde etkilemeyecek.

Böylece her nesnenin/sınıfın sorumluluklarının kesin olarak tanımlandığı modüler bir kod oluşturduk. Bu tür kodlarla çalışmak sorun değil, bakımı basit bir iş olacaktır. Tüm “ilahi nesneyi” SRP'ye dönüştürdük.

Skillbox şunları önerir:

Kaynak: habr.com

Yorum ekle