Skriver flexibel kod med SOLID

Skriver flexibel kod med SOLID

Från översättaren: publicerad för dig artikel av Severin Perez om att använda SOLID-principer i programmering. Informationen från artikeln kommer att vara användbar för både nybörjare och erfarna programmerare.

Om du håller på med utveckling har du med största sannolikhet hört talas om SOLID-principerna. De gör det möjligt för programmeraren att skriva ren, välstrukturerad och lätt underhållbar kod. Det är värt att notera att det i programmering finns flera metoder för hur man korrekt utför ett visst jobb. Olika specialister har olika idéer och förståelse för den "rätta vägen"; allt beror på varje persons erfarenhet. De idéer som proklameras i SOLID accepteras dock av nästan alla företrädare för IT-gemenskapen. De blev utgångspunkten för uppkomsten och utvecklingen av många bra praxis för utvecklingsledning.

Låt oss förstå vad SOLID principerna är och hur de hjälper oss.

Skillbox rekommenderar: Praktisk kurs "Mobilutvecklare PRO".

Påminnelse: för alla läsare av "Habr" - en rabatt på 10 000 rubel när du anmäler dig till någon Skillbox-kurs med hjälp av "Habr"-kampanjkoden.

Vad är SOLID?

Denna term är en förkortning, varje bokstav i termen är början på namnet på en specifik princip:

Principen om ett enda ansvar


The Single Responsibility Principle (SRP) säger att varje klass eller modul i ett program endast ska ansvara för en del av programmets funktionalitet. Dessutom bör delar av detta ansvar tilldelas deras egen klass, snarare än spridda över icke-relaterade klasser. SRP:s utvecklare och chefsevangelist, Robert S. Martin, beskriver ansvarighet som orsaken till förändringen. Han föreslog ursprungligen denna term som en av beståndsdelarna i hans arbete "Principles of Object-Oriented Design". Konceptet innehåller mycket av det anslutningsmönster som tidigare definierades av Tom DeMarco.

I konceptet ingick även flera begrepp formulerade av David Parnas. De två huvudsakliga är inkapsling och informationsdöljande. Parnas hävdade att uppdelning av ett system i separata moduler inte borde baseras på analys av blockdiagram eller exekveringsflöden. Någon av modulerna måste innehålla en specifik lösning som ger ett minimum av information till kunderna.

Martin gav förresten ett intressant exempel med högre chefer för ett företag (COO, CTO, CFO), som var och en använder specifik affärsmjukvara för olika ändamål. Som ett resultat kan vem som helst av dem implementera ändringar i programvaran utan att påverka andra chefers intressen.

Gudomligt föremål

Som alltid är det bästa sättet att lära sig SRP att se det i aktion. Låt oss titta på en del av programmet som INTE följer principen om ett enda ansvar. Detta är Ruby-kod som beskriver rymdstationens beteende och egenskaper.

Granska exemplet och försök fastställa följande:
Ansvar för de objekt som är deklarerade i SpaceStation-klassen.
De som kan vara intresserade av driften av rymdstationen.

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

Egentligen är vår rymdstation dysfunktionell (jag tror inte att jag kommer att få ett samtal från NASA när som helst snart), men det finns något att analysera här.

SpaceStation-klassen har alltså flera olika ansvarsområden (eller uppgifter). Alla kan delas in i typer:

  • sensorer;
  • förnödenheter (förbrukningsvaror);
  • bränsle;
  • acceleratorer.

Även om ingen av stationens anställda tilldelas en klass kan vi lätt föreställa oss vem som ansvarar för vad. Troligtvis kontrollerar vetenskapsmannen sensorerna, logistikern ansvarar för att tillhandahålla resurser, ingenjören ansvarar för bränsletillförseln och piloten kontrollerar boosters.

Kan vi säga att det här programmet inte är SRP-kompatibelt? Ja visst. Men SpaceStation-klassen är ett typiskt "gudsobjekt" som vet allt och gör allt. Detta är ett stort antimönster inom objektorienterad programmering. För en nybörjare är sådana föremål extremt svåra att underhålla. Hittills är programmet väldigt enkelt, ja, men tänk vad som kommer att hända om vi lägger till nya funktioner. Kanske kommer vår rymdstation att behöva en läkarstation eller ett mötesrum. Och ju fler funktioner det finns, desto mer kommer SpaceStation att växa. Tja, eftersom den här anläggningen kommer att kopplas till andra kommer det att bli ännu svårare att serva hela komplexet. Det gör att vi kan störa driften av till exempel acceleratorer. Om en forskare begär förändringar av sensorerna kan det mycket väl påverka stationens kommunikationssystem.

Att bryta mot SRP-principen kan ge en kortsiktig taktisk seger, men i slutändan kommer vi att "förlora kriget", och det kommer att bli mycket svårt att upprätthålla ett sådant monster i framtiden. Det är bäst att dela upp programmet i separata kodsektioner, som var och en är ansvarig för att utföra en specifik operation. För att förstå detta, låt oss ändra SpaceStation-klassen.

Låt oss fördela ansvaret

Ovan definierade vi fyra typer av operationer som styrs av SpaceStation-klassen. Vi kommer att ha dem i åtanke vid omfaktorisering. Den uppdaterade koden matchar SRP bättre.

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

Det är många förändringar, programmet ser definitivt bättre ut nu. Nu har vår SpaceStation-klass blivit mer av en container där operationer initieras för beroende delar, inklusive en uppsättning sensorer, ett förbrukningssystem, en bränsletank och boosters.

För någon av variablerna finns det nu en motsvarande klass: Sensorer; SupplyHold; Bränsletank; Thrusters.

Det finns flera viktiga ändringar i den här versionen av koden. Poängen är att individuella funktioner inte bara är inkapslade i sina egna klasser, de är organiserade på ett sådant sätt att de blir förutsägbara och konsekventa. Vi grupperar element med liknande funktionalitet för att följa principen om koherens. Nu, om vi behöver ändra hur systemet fungerar, flytta från en hashstruktur till en array, använd bara SupplyHold-klassen; vi behöver inte röra andra moduler. På så sätt, om logistiktjänstemannen ändrar något i sin sektion, kommer resten av stationen att förbli intakt. I det här fallet kommer SpaceStation-klassen inte ens att vara medveten om förändringarna.

Våra officerare som arbetar på rymdstationen är förmodligen glada över förändringarna eftersom de kan begära de de behöver. Observera att koden har metoder som report_supplies och report_fuel som ingår i klasserna SupplyHold och FuelTank. Vad skulle hända om jorden bad om att ändra sitt sätt att rapportera? Båda klasserna, SupplyHold och FuelTank, kommer att behöva ändras. Vad händer om du behöver ändra hur bränsle och förbrukningsvaror levereras? Du kommer förmodligen att behöva byta alla samma klasser igen. Och detta är redan ett brott mot SRP-principen. Låt oss fixa det här.

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.

I den senaste versionen av programmet har ansvaret delats upp i två nya klasser, FuelReporter och SupplyReporter. De är båda barn i Reporterklassen. Dessutom har vi lagt till instansvariabler till SpaceStation-klassen så att den önskade underklassen kan initieras vid behov. Om jorden nu bestämmer sig för att ändra något annat, kommer vi att göra ändringar i underklasserna och inte i huvudklassen.

Vissa av våra klasser är naturligtvis fortfarande beroende av varandra. Sålunda beror SupplyReporter-objektet på SupplyHold, och FuelReporter beror på FuelTank. Självklart ska boosterna kopplas till bränsletanken. Men här ser allt redan logiskt ut, och att göra ändringar kommer inte att vara särskilt svårt - att redigera koden för ett objekt kommer inte att påverka ett annat.

Således har vi skapat en modulär kod där ansvaret för vart och ett av objekten/klasserna är exakt definierade. Att arbeta med sådan kod är inte ett problem, att underhålla den kommer att vara en enkel uppgift. Vi har omvandlat hela det "gudomliga objektet" till SRP.

Skillbox rekommenderar:

Källa: will.com

Lägg en kommentar