Vi skriver fleksibel kode ved at bruge SOLID

Vi skriver fleksibel kode ved at bruge SOLID

Fra oversætteren: har offentliggjort for dig artiklen af Severin Perez om anvendelse af SOLID-principperne i programmering. Oplysninger fra artiklen vil være nyttige for både nybegyndere og erfarne programmører.

Hvis du arbejder med udvikling, har du sandsynligvis hørt om SOLID-principperne. De gør det muligt for programmører at skrive ren, velstruktureret og nemt vedligeholdelig kode. Det skal bemærkes, at der i programmering er flere tilgange til, hvordan man korrekt udfører forskellige opgaver. Forskellige specialister har forskellige idéer og forståelser af den 'rette vej'; det afhænger af den enkeltes erfaring. Ikke desto mindre accepteres idéerne, der udtrykkes i SOLID, af næsten alle medlemmer af IT-samfundet. De er blevet udgangspunktet for mange gode metoder til håndtering af udvikling.

Lad os se nærmere på, hvad SOLID-principperne er, og hvordan de hjælper os.

Skillbox anbefaler: Praktisk kursus «Mobil udvikler PRO».

Vi minder om: til alle læsere af 'Habr' — rabat på 10.000 rubler ved tilmelding til et hvilket som helst Skillbox-kursus med rabatkoden 'Habr'.

Hvad er SOLID?

Denne betegnelse er en forkortelse, hvor hver bogstav i termen repræsenterer begyndelsen af navnet på et bestemt princip:

  • Angreb op til 1,2Single Responsibility Principle (princippet om enkelt ansvar). En modul kan kun have én grund til ændring.
  • Listen DOpen/Closed Principle (princippet om åbne/lukkede. Klasser og andre elementer skal være åbne for udvidelse, men lukkede for modificering.
  •  The LLiskov Substitution Principle (princippet om substitution af Liskov). Funktioner, der bruger den grundlæggende type, skal have mulighed for at bruge under typer af den grundlæggende type uden at vide det.
  • Listen IInterface Segregation Principle (princippet om interface-segregering). Programmeringsenheder skal ikke afhænge af metoder, som de ikke bruger.
  • Listen DDependency Inversion Principle (princippet om inversion af afhængigheder). Moduler på højere niveauer må ikke afhænge af moduler på lavere niveauer.

Princippet om enkelt ansvar

 
Principen om enkel ansvarighet (SRP) siger, at hver klasse eller modul i et program kun skal tage ansvar for én del af programmets funktionalitet. Derudover skal elementerne af dette ansvar være knyttet til deres egen klasse og ikke fordelt over urelaterede klasser. Udvikleren og hovedforkæmperen for SRP, Robert C. Martin, beskriver ansvar som årsagen til forandringer. Han introducerede oprindeligt dette begreb som et af elementerne i sit arbejde "Principper for objektorienteret design". Mange aspekter af konceptet er inspireret af konceptet om kobling, som tidligere blev defineret af Tom DeMarco.

Derudover er der flere koncepter fra David Parnas inkluderet i teorien. De to grundlæggende er indkapsling og informationsskjul. Parnas hævdede, at opdelingen af et system i separate moduler ikke bør baseres på analyse af blokdiagrammer eller udførelsesstrømme. Ethvert modul skal indeholde en specifik løsning, der giver minimumsoplysninger til kunderne.

Forresten nævnte Martin et interessant eksempel med virksomhedens øverste ledere (COO, CTO, CFO), hvor hver enkelt bruger specifik software til forretning med forskellige formål. Derfor kan enhver af dem implementere ændringer i softwaren uden at påvirke de andre lederes interesser.

Den guddommelige klasse

Som sædvanligt er den bedste måde at lære SRP på at se det i aktion. Lad os tage et kig på et stykke kode, der IKKE overholder princippet om enkel ansvarighed. Dette er Ruby-kode, der beskriver opførsel og egenskaber ved en rumstation.

Gennemgå eksemplet og prøv at bestemme følgende:
De forpligtelser, som objekterne i klassen SpaceStation udtaler.
Dem, der kan have interesse i at arbejde med rumstationen.

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} available: #{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

Vores rumstation er faktisk ikke funktionsdygtig (jeg forventer ikke at få et opkald fra NASA i den nærmeste fremtid), men der er noget at analysere her.

Klassen SpaceStation har flere forskellige ansvar (eller opgaver). Alle kan opdeles efter type:

  • sensorer;
  • forsyning (forbrugsvarer);
  • brændstof;
  • dysser.

Selvom ingen af stationens medarbejdere er defineret i klassen, kan vi let forestille os, hvem der har ansvaret for hvad. Sandsynligvis overvåger en videnskabsmand sensorene, en logistikansvarlig står for forsyningerne, en ingeniør håndterer brændstoflagret, og piloten styrer dysserne.

Kan vi sige, at dette program ikke overholder SRP? Ja, selvfølgelig. Men klassen SpaceStation er en typisk "guddommelig objekt", der ved alt og gør alt. Dette er den primære anti-mall i objektorienteret programmering. For nybegyndere er sådanne objekter ekstremt vanskelige at vedligeholde. Indtil videre er programmet meget simpelt, ja, men forestil dig, hvad der sker, hvis vi tilføjer nye funktioner. Måske får vores rumstation brug for en medicinsk station eller et mødelokale. Og jo flere funktioner, jo mere vil SpaceStation vokse. Og eftersom dette objekt vil være knyttet til andre, vil vedligeholdelsen af hele komplekset blive endnu mere kompliceret. I sidste ende kan vi muligvis forstyrre driften af f.eks. acceleratorer. Hvis en forsker anmoder om ændringer i arbejdet med sensorer, kan det meget vel påvirke stationens kommunikationssystemer.

Overtrædelse af SRP-princippet kan give en kortsigtet taktisk sejr, men i sidste ende "taber vi krigen"; det vil blive ret vanskeligt at vedligeholde sådan et monster i fremtiden. Det er bedst at opdele programmet i separate kodeafsnit, som hver især har ansvaret for at udføre en bestemt operation. Forstået dette, lad os ændre klassen SpaceStation.

Fordel ansvar

Ovenfor har vi defineret fire typer operationer, som styres af klassen SpaceStation. Når vi refaktorerer, vil vi have dem i tankerne. Den opdaterede kode overholder bedre 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} available: #{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

Der er sket mange ændringer, programmet ser nu bestemt bedre ud. Vores class SpaceStation fungerer nu mere som en container, hvor operationer for afhængige dele, inklusive sensorpakken, forsyningssystemet, brændstoftanken og thrustere, initieres.

For hver af variablerne er der nu en tilsvarende klasse: Sensors; SupplyHold; FuelTank; Thrusters.

I denne version af koden er der flere vigtige ændringer. Det er sådan, at de enkelte funktioner ikke kun er inkapslet i deres egne klasser, de er organiseret på en måde, så de bliver forudsigelige og konsistente. Vi grupperer lignende funktionaliteter for at følge principperne for sammenhæng. Nu, hvis vi skal ændre systemets arbejdsmåde fra en hash-struktur til et array, kan vi blot bruge klassen SupplyHold, og vi behøver ikke at berøre andre moduler. På denne måde, hvis den ansvarlige officer for logistik ændrer noget i sin sektion, vil de andre elementer på stationen forblive uberørte. Klassen SpaceStation vil endda ikke være opmærksom på ændringerne.

Vore officerer, der arbejder på rumstationen, er sandsynligvis glade for ændringerne, da de kan anmode om de ressourcer, der præcist er nødvendige for dem. Bemærk, at koden indeholder metoder som report_supplies og report_fuel, der findes i klasserne SupplyHold og FuelTank. Hvad sker der, hvis Jorden beder om at ændre måden, rapporter genereres? Det vil være nødvendigt at ændre begge klasser, SupplyHold og FuelTank. Og hvad, hvis måden at levere brændstof og forsyninger skal ændres? Sandsynligvis skal disse klasser ændres igen. Og det er allerede et brud på SRP-princippet. Lad os rette det.

class Rumstation
  attr_reader :sensors, :forsyningshold, :forsyningsrapportør,
              :brændstoftank, :brændstofrapportør, :thrusters
 
  def initialize
    @sensors = Sensors.new
    @forsyningshold = ForsyningsHold.new
    @forsyningsrapportør = ForsyningsRapportør.new(@forsyningshold)
    @brændstoftank = Brændstoftank.new
    @brændstofrapportør = BrændstofRapportør.new(@brændstoftank)
    @thrusters = Thrusters.new(@brændstoftank)
  end
end
 
class Sensors
  def run_sensors
    puts "----- Sensorhandling -----"
    puts "Kører sensorer!"
  end
end
 
class ForsyningsHold
  attr_accessor :forsyninger
  attr_reader :rapportør
 
  def initialize
    @forsyninger = {}
  end
 
  def get_forsyninger
    @forsyninger
  end
 
  def load_forsyninger(type, quantity)
    puts "----- Forsyningshandling -----"
    puts "Laster #{quantity} enheder af #{type} i forsyningsholdet."
    
    if @forsyninger[type]
      @forsyninger[type] += quantity
    else
      @forsyninger[type] = quantity
    end
  end
 
  def use_forsyninger(type, quantity)
    puts "----- Forsyningshandling -----"
    if @forsyninger[type] != nil && @forsyninger[type] > quantity
      puts "Bruger #{quantity} af #{type} fra forsyningsholdet."
      @forsyninger[type] -= quantity
    else
      puts "Forsyningsfejl: Utilstrækkelig #{type} i forsyningsholdet."
    end
  end
end
 
class Brændstoftank
  attr_accessor :brændstof
  attr_reader :rapportør
 
  def initialize
    @brændstof = 0
  end
 
  def get_brændstofniveauer
    @brændstof
  end
 
  def load_brændstof(quantity)
    puts "----- Brændstofhandling -----"
    puts "Laster #{quantity} enheder af brændstof i tanken."
    @brændstof += quantity
  end
 
  def use_brændstof(quantity)
    puts "----- Brændstofhandling -----"
    puts "Bruger #{quantity} enheder af brændstof fra tanken."
    @brændstof -= quantity
  end
end
 
class Thrusters
  FUEL_PER_THRUST = 10
 
  def initialize(brændstoftank)
    @linked_brændstoftank = brændstoftank
  end
 
  def activate_thrusters
    puts "----- Thrusthandling -----"
    
    if @linked_brændstoftank.get_brændstofniveauer >= FUEL_PER_THRUST
      puts "Thrusthandling succesfuld."
      @linked_brændstoftank.use_brændstof(FUEL_PER_THRUST)
    else
      puts "Thrustfejl: Utilstrækkeligt brændstof til rådighed."
    end
  end
end
 
class Rapportør
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
 
  def report
    puts "----- #{@type.capitalize} Rapport -----"
  end
end
 
class BrændstofRapportør < Rapportør
  def initialize(item)
    super(item, "brændstof")
  end
 
  def report
    super
    puts "#{@linked_item.get_brændstofniveauer} enheder af brændstof tilgængeligt."
  end
end
 
class ForsyningsRapportør  0
      @linked_item.get_forsyninger.each do |type, quantity|
        puts "#{type} tilgængelig: #{quantity} enheder"
      end
    else
      puts "Forsyningsholdet er tomt."
    end
  end
end
 
iss = Rumstation.new
 
iss.sensors.run_sensors
  # ----- Sensorhandling -----
  # Kører sensorer!
 
iss.forsyningshold.use_forsyninger("dele", 2)
  # ----- Forsyningshandling -----
  # Forsyningsfejl: Utilstrækkelig dele i forsyningsholdet.
iss.forsyningshold.load_forsyninger("dele", 10)
  # ----- Forsyningshandling -----
  # Laster 10 enheder af dele i forsyningsholdet.
iss.forsyningshold.use_forsyninger("dele", 2)
  # ----- Forsyningshandling -----
  # Bruger 2 af dele fra forsyningsholdet.
iss.forsyningsrapportør.report
  # ----- Forsyningsrapport -----
  # dele tilgængelig: 8 enheder
 
iss.thrusters.activate_thrusters
  # ----- Thrusthandling -----
  # Thrustfejl: Utilstrækkeligt brændstof til rådighed.
iss.brændstoftank.load_brændstof(100)
  # ----- Brændstofhandling -----
  # Laster 100 enheder af brændstof i tanken.
iss.thrusters.activate_thrusters
  # ----- Thrusthandling -----
  # Thrusthandling succesfuld.
  # ----- Brændstofhandling -----
  # Bruger 10 enheder af brændstof fra tanken.
iss.brændstofrapportør.report
  # ----- Brændstofrapport -----
# 90 enheder af brændstof tilgængelig.

I denne, den sidste version af programmet, er funktionerne opdelt i to nye klasser, FuelReporter og SupplyReporter. Begge er underklasser af klassen Reporter. Desuden har vi tilføjet instansvariabler til klassen SpaceStation, så vi kan initialisere den nødvendige underklasse, hvis det er nødvendigt. Nu, hvis Jorden beslutter at ændre noget andet, kan vi foretage ændringer i underklasserne, og ikke i hovedklassen.

Selvfølgelig afhænger nogle klasser stadig af hinanden. For eksempel afhænger objektet SupplyReporter af SupplyHold, mens FuelReporter afhænger af FuelTank. Selvfølgelig skal acceleratoren forbindes med brændstoftanken. Men her ser alt allerede fornuftigt ud, og ændringer vil ikke være særligt vanskelige — at redigere koden for ét objekt vil ikke påvirke et andet for meget.

På denne måde har vi skabt modulær kode, hvor funktionerne for hver af objekterne/klasserne er præcist defineret. At arbejde med sådan kode er ikke et problem, og dens vedligeholdelse vil være en simpel opgave. Vi har omdannet hele "den guddommelige objekt" til SRP.

Skillbox anbefaler:

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster