We schrijven flexibele code, gebruikmakend van SOLID

We schrijven flexibele code, gebruikmakend van SOLID

Van de vertaler: we hebben dit gepubliceerd voor jou het artikel van Severin Perez over het gebruik van SOLID-principes in programmeren. De informatie in het artikel zal nuttig zijn voor zowel beginners als ervaren programmeurs.

Als je met ontwikkeling bezig bent, heb je waarschijnlijk gehoord van de SOLID-principes. Ze stellen programmeurs in staat om schone, goed gestructureerde en gemakkelijk te onderhouden code te schrijven. Het is belangrijk op te merken dat er verschillende benaderingen in programmeren zijn voor hoe je elk soort werk correct uitvoert. Verschillende specialisten hebben verschillende ideeën en opvattingen over wat de 'juiste weg' is; dit hangt af van de ervaring van elk individu. Desondanks worden de ideeën die in SOLID worden gepromoot door vrijwel alle leden van de IT-gemeenschap geaccepteerd. Ze zijn een vertrekpunt geworden voor de ontwikkeling en opkomst van vele goede beheermethoden in ontwikkeling.

Laten we onderzoeken wat de SOLID-principes zijn en hoe ze ons helpen.

Skillbox raadt aan: Praktische cursus ‘Mobiele Ontwikkelaar PRO’.

Ter herinnering: voor alle lezers van «Habr» — een korting van 10.000 roebel bij inschrijving voor elke cursus van Skillbox met de promocode «Habr».

Wat zijn SOLID?

Deze term is een acroniem; elke letter van de term is de beginletter van de naam van een specifiek principe:

  • SSingle Responsibility Principle (principes van enkele verantwoordelijkheden). Een module mag slechts één reden hebben om te veranderen.
  • De OOpen/Closed Principle (open-/gesloten principe). Klassen en andere elementen moeten open zijn voor uitbreiding, maar gesloten voor wijzigingen.
  • De LLiskov Substitution Principle (principe van de substitutie van Liskov). Functies die gebruik maken van het basistype moeten in staat zijn om subtypes van het basistype te gebruiken, zonder daarvan op de hoogte te zijn.
  • De IInterface Segregation Principle(principe van scheiding van interfaces). Software-eenheden mogen niet afhankelijk zijn van methoden die ze niet gebruiken.
  • De DDependency Inversion Principle (principe van omkering van afhankelijkheid). Modules op hogere niveaus mogen niet afhankelijk zijn van modules op lagere niveaus.

Principes van enkele verantwoordelijkheden

 
Het principe van de enkele verantwoordelijkheid (SRP) stelt dat elke klasse of module in een programma verantwoordelijk moet zijn voor slechts één deel van de functionaliteit van dat programma. Bovendien moeten de elementen van deze verantwoordelijkheid aan hun klasse worden toegewezen en niet worden verspreid over niet-verwante klassen. De ontwikkelaar en belangrijkste pleitbezorger van SRP, Robert C. Martin, beschrijft verantwoordelijkheid als de oorzaak van verandering. Hij introduceerde deze term oorspronkelijk als één van de elementen in zijn werk "Principes van objectgeoriënteerd ontwerpen". Veel van wat in dit concept is opgenomen, komt voort uit de eerder door Tom DeMarco gedefinieerde samenhangprincipes.

Verder zijn er verschillende concepten opgenomen die door David Parnas zijn geformuleerd. De twee belangrijkste zijn encapsulatie en informatieverberging. Parnas stelde dat de verdeling van een systeem in afzonderlijke modules niet gebaseerd moet zijn op de analyse van stroomdiagrammen of uitvoeringsstromen. Elk van de modules moet een bepaalde oplossing bevatten die een minimaal aantal informatie aan de klanten biedt.

Overigens gaf Martin een interessant voorbeeld met de hogere managers van een bedrijf (COO, CTO, CFO), elk van hen gebruikt specifieke software voor zakelijke doeleinden, maar met verschillende doelen. Uiteindelijk kan een van hen wijzigingen aanbrengen in de software zonder de belangen van de andere managers te raken.

Goddelijke Object

Zoals gebruikelijk is de beste manier om SRP te begrijpen, alles in actie te zien. Laten we kijken naar een gedeelte van het programma dat NIET voldoet aan het principe van de enkele verantwoordelijkheid. Dit is Ruby-code die het gedrag en de attributen van een ruimtestation beschrijft.

Bekijk het voorbeeld en probeer het volgende te bepalen:
De verantwoordelijkheden van de objecten die zijn verklaard in de klasse SpaceStation.
Degenen die geïnteresseerd kunnen zijn in het functioneren van het ruimtestation.

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} beschikbaar: #{quantity} eenheden"
      end
    else
      puts "Voorraad is leeg."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} eenheden brandstof in de tank."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} eenheden brandstof beschikbaar."
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @fuel >= 10
      puts "Duwactie succesvol."
      @fuel -= 10
    else
      puts "Thruster Error: Onvoldoende brandstof beschikbaar."
    end
  end
end

Onze ruimtebasis functioneert eigenlijk niet (ik denk niet dat ik binnenkort een telefoontje van NASA ontvang), maar er is genoeg om te analyseren.

Dus, de klasse SpaceStation heeft verschillende verantwoordelijkheden (of taken). Deze kunnen allemaal worden ingedeeld in types:

  • sensors;
  • levering (verbruiksmaterialen);
  • brandstof;
  • stuwraketten.

Hoewel geen van de medewerkers van het station is gedefinieerd in de klasse, kunnen we gemakkelijk voorstellen wie waarvoor verantwoordelijk is. Waarschijnlijk controleert een wetenschapper de sensoren, is er een logistiek medewerker verantwoordelijk voor de bevoorrading, is een ingenieur bezig met de brandstofvoorraden, en de piloot houdt de stuwraketten in de gaten.

Kunnen we zeggen dat dit programma niet aan de SRP voldoet? Ja, natuurlijk. Maar de klasse SpaceStation is een typisch ‘godobject’ dat alles weet en alles doet. Dit is het belangrijkste anti-patroon in objectgeoriënteerd programmeren. Voor een beginner zijn dergelijke objecten extreem moeilijk te onderhouden. Tot nu toe is het programma heel eenvoudig, ja, maar stel je voor wat er gebeurt als we nieuwe functies toevoegen. Misschien heeft onze ruimtebasis wel een medisch centrum of een vergaderruimte nodig. En hoe meer functies, hoe sterker de SpaceStation zal groeien. Aangezien dit object met andere zal zijn verbonden, zal het onderhoud van het hele complex nog ingewikkelder worden. Uiteindelijk kunnen we de werking bijvoorbeeld van de versnellers verstoren. Als een wetenschapper om wijzigingen in de bediening van de sensoren vraagt, kan dit de communicatiesystemen van het station beïnvloeden.

Het schenden van het SRP-principe kan een kortetermijn tactische overwinning opleveren, maar uiteindelijk ‘verliezen we de oorlog’, het zal behoorlijk moeilijk zijn om zo'n monster in de toekomst te onderhouden. Het is het beste om het programma op te splitsen in afzonderlijke codefragmenten, die elk verantwoordelijk zijn voor het uitvoeren van een specifieke operatie. Met deze gedachte in ons achterhoofd, laten we de klasse SpaceStation aanpassen.

Verdeel de verantwoordelijkheden

Bovenstaand hebben we vier types operaties gedefinieerd die door de klasse SpaceStation worden gecontroleerd. Bij het refactoren houden we deze in gedachten. De bijgewerkte code voldoet beter aan de 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 Actie -----"
    puts "Sensors worden uitgevoerd!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Leveringsactie -----"
    puts "Laden van #{quantity} eenheden van #{type} in de voorraadruimte."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Leveringsactie -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Gebruik van #{quantity} van #{type} uit de voorraadruimte."
      @supplies[type] -= quantity
    else
      puts "Leveringsfout: Onvoldoende #{type} in de voorraadruimte."
    end
  end
 
  def report_supplies
    puts "----- Voorraadrapport -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} beschikbaar: #{quantity} eenheden"
      end
    else
      puts "De voorraadruimte is leeg."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Brandstofactie -----"
    puts "Laden van #{quantity} eenheden brandstof in de tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Brandstofactie -----"
    puts "Gebruik van #{quantity} eenheden brandstof uit de tank."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Brandstofrapport -----"
    puts "#{@fuel} eenheden brandstof beschikbaar."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Actie Thruster -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Duwactie succesvol."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Thruster Fout: Onvoldoende brandstof beschikbaar."
    end
  end
end

Er zijn veel veranderingen, het programma ziet er nu zeker beter uit. Onze SpaceStation-klasse is nu eerder een container waarin operaties voor afhankelijk onderdelen worden gestart, inclusief een set sensoren, een bevoorradingssysteem, een brandstoftank, en thrusters.

Voor elke variabele is er nu een overeenkomstige klasse: Sensors; SupplyHold; FuelTank; Thrusters.

In deze versie van de code zijn er een aantal belangrijke wijzigingen. Het is namelijk zo dat afzonderlijke functies niet alleen zijn ingekapseld in eigen klassen, maar ook zo zijn georganiseerd dat ze voorspelbaar en consistent worden. We groeperen elementen die qua functionaliteit op elkaar lijken om het principe van coherentie te volgen. Nu, als we de werking van het systeem moeten wijzigen door over te schakelen van een hash-structuur naar een array, kunnen we eenvoudig gebruikmaken van de klasse SupplyHold, zonder andere modules aan te raken. Op deze manier blijven de andere elementen van het station intact, zelfs als de officier die verantwoordelijk is voor de logistiek iets in zijn sectie verandert. De klasse SpaceStation zal zelfs niet op de hoogte zijn van de wijzigingen.

Onze officieren die op het ruimtestation werken, zullen vermoedelijk blij zijn met de wijzigingen, aangezien ze de specifieke zaken kunnen aanvragen die zij nodig hebben. Let op dat de code methoden bevat zoals report_supplies en report_fuel, die zich in de klassen SupplyHold en FuelTank bevinden. Wat gebeurt er als de aarde vraagt om de manier van rapporteren te wijzigen? Beide klassen, SupplyHold en FuelTank, moeten dan worden aangepast. En wat als we de manier van brandstoftoevoer en toebehoren moeten wijzigen? Waarschijnlijk moeten we opnieuw al die dezelfde klassen wijzigen. En dat is al een schending van het SRP-principe. Laten we dit oplossen.

class RuimteStation
  attr_reader :sensoren, :voorraad_houd, :voorraad_rapporteur,
              :brandstoftank, :brandstof_rapporteur, :duwers
 
  def initialize
    @sensoren = Sensoren.new
    @voorraad_houd = VoorraadHoud.new
    @voorraad_rapporteur = VoorraadRapporteur.new(@voorraad_houd)
    @brandstoftank = Brandstoftank.new
    @brandstof_rapporteur = BrandstofRapporteur.new(@brandstoftank)
    @duwers = Duwers.new(@brandstoftank)
  end
end
 
class Sensoren
  def run_sensors
    puts "----- Sensor Actie -----"
    puts "Sensoren draaien!"
  end
end
 
class VoorraadHoud
  attr_accessor :voorraad
  attr_reader :rapporteur
 
  def initialize
    @voorraad = {}
  end
 
  def get_voorraad
    @voorraad
  end
 
  def laad_voorraad(type, hoeveelheid)
    puts "----- Voorraad Actie -----"
    puts "Laadt #{hoeveelheid} eenheden van #{type} in de voorraad houd."
    
    if @voorraad[type]
      @voorraad[type] += hoeveelheid
    else
      @voorraad[type] = hoeveelheid
    end
  end
 
  def gebruik_voorraad(type, hoeveelheid)
    puts "----- Voorraad Actie -----"
    if @voorraad[type] != nil && @voorraad[type] > hoeveelheid
      puts "Gebruik #{hoeveelheid} van #{type} uit de voorraad houd."
      @voorraad[type] -= hoeveelheid
    else
      puts "Voorraad Fout: Onvoldoende #{type} in de voorraad houd."
    end
  end
end
 
class Brandstoftank
  attr_accessor :brandstof
  attr_reader :rapporteur
 
  def initialize
    @brandstof = 0
  end
 
  def get_brandstof_niveaus
    @brandstof
  end
 
  def laad_brandstof(hoeveelheid)
    puts "----- Brandstof Actie -----"
    puts "Laadt #{hoeveelheid} eenheden brandstof in de tank."
    @brandstof += hoeveelheid
  end
 
  def gebruik_brandstof(hoeveelheid)
    puts "----- Brandstof Actie -----"
    puts "Gebruik #{hoeveelheid} eenheden brandstof uit de tank."
    @brandstof -= hoeveelheid
  end
end
 
class Duwers
  BRANDSTOF_PER_DUWT = 10
 
  def initialize(brandstoftank)
    @verbonden_brandstoftank = brandstoftank
  end
 
  def activeer_duwers
    puts "----- Duw Actie -----"
    
    if @verbonden_brandstoftank.get_brandstof_niveaus >= BRANDSTOF_PER_DUWT
      puts "Duwactie succesvol."
      @verbonden_brandstoftank.gebruik_brandstof(BRANDSTOF_PER_DUWT)
    else
      puts "Duw Fout: Onvoldoende brandstof beschikbaar."
    end
  end
end
 
class Rapporteur
  def initialize(item, type)
    @verbonden_item = item
    @type = type
  end
 
  def rapport
    puts "----- #{@type.capitalize} Rapport -----"
  end
end
 
class BrandstofRapporteur < Rapporteur
  def initialize(item)
    super(item, "brandstof")
  end
 
  def rapport
    super
    puts "#{@verbonden_item.get_brandstof_niveaus} eenheden brandstof beschikbaar."
  end
end
 
class VoorraadRapporteur  0
      @verbonden_item.get_voorraad.each do |type, hoeveelheid|
        puts "#{type} beschikbaar: #{hoeveelheid} eenheden"
      end
    else
      puts "Voorraad houd is leeg."
    end
  end
end
 
iss = RuimteStation.new
 
iss.sensoren.run_sensors
  # ----- Sensor Actie -----
  # Sensoren draaien!
 
iss.voorraad_houd.gebruik_voorraad("onderdelen", 2)
  # ----- Voorraad Actie -----
  # Voorraad Fout: Onvoldoende onderdelen in de voorraad houd.
iss.voorraad_houd.laad_voorraad("onderdelen", 10)
  # ----- Voorraad Actie -----
  # Laadt 10 eenheden onderdelen in de voorraad houd.
iss.voorraad_houd.gebruik_voorraad("onderdelen", 2)
  # ----- Voorraad Actie -----
  # Gebruik 2 van onderdelen uit de voorraad houd.
iss.voorraad_rapporteur.rapport
  # ----- Voorraad Rapport -----
  # onderdelen beschikbaar: 8 eenheden
 
iss.duwers.activeer_duwers
  # ----- Duw Actie -----
  # Duw Fout: Onvoldoende brandstof beschikbaar.
iss.brandstoftank.laad_brandstof(100)
  # ----- Brandstof Actie -----
  # Laadt 100 eenheden brandstof in de tank.
iss.duwers.activeer_duwers
  # ----- Duw Actie -----
  # Duwactie succesvol.
  # ----- Brandstof Actie -----
  # Gebruik 10 eenheden brandstof uit de tank.
iss.brandstof_rapporteur.rapport
  # ----- Brandstof Rapport -----
# 90 eenheden brandstof beschikbaar.

In deze laatste versie van het programma zijn de verantwoordelijkheden verdeeld over twee nieuwe klassen, FuelReporter en SupplyReporter. Beide zijn afgeleiden van de klasse Reporter. Daarnaast hebben we instantievariabelen toegevoegd aan de klasse SpaceStation, zodat we de benodigde subclassen kunnen initialiseren indien nodig. Nu, als de aarde besluit iets anders te veranderen, zullen we aanpassingen doen in de subclasses en niet in de hoofklasse.

Natuurlijk zijn sommige klassen nog steeds afhankelijk van elkaar. Zo is het object SupplyReporter afhankelijk van SupplyHold, en FuelReporter is afhankelijk van FuelTank. Uiteraard moeten de boosters gekoppeld zijn aan de brandstoftank. Maar hier ziet alles er logisch uit en zal het aanbrengen van wijzigingen niet bijzonder moeilijk zijn — het bewerken van de code van één object zal niet veel invloed hebben op een ander.

Zo hebben we modulaire code gecreëerd, waarbij de verantwoordelijkheden van elk van de objecten/klassen precies zijn gedefinieerd. Werken met zulke code is geen probleem; het onderhoud zal een eenvoudige taak zijn. We hebben het hele 'goddelijke object' omgevormd naar SRP.

Skillbox raadt aan:

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster