
Van de vertaler: we hebben dit gepubliceerd voor jou 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 .
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.
- (open-/gesloten principe). Klassen en andere elementen moeten open zijn voor uitbreiding, maar gesloten voor wijzigingen.
- (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.
- (principe van scheiding van interfaces). Software-eenheden mogen niet afhankelijk zijn van methoden die ze niet gebruiken.
- (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
endOnze 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
endEr 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:
- Praktische tweejarenopleiding .
- Online cursus .
- Praktische jaaropleiding .
Bron: habr.com
