Skrivning af fleksibel kode ved hjælp af SOLID

Skrivning af fleksibel kode ved hjælp af SOLID

Fra oversætteren: udgivet for dig artikel af Severin Perez om at bruge SOLID principper i programmering. Oplysningerne fra artiklen vil være nyttige for både begyndere og erfarne programmører.

Hvis du er til udvikling, har du højst sandsynligt hørt om SOLID-principperne. De gør det muligt for programmøren at skrive ren, velstruktureret og let vedligeholdelig kode. Det er værd at bemærke, at der i programmering er flere tilgange til, hvordan man korrekt udfører et bestemt job. Forskellige specialister har forskellige ideer og forståelse af den "rigtige vej"; det hele afhænger af hver persons erfaring. Men de ideer, der proklameres i SOLID, accepteres af næsten alle repræsentanter for IT-samfundet. De blev udgangspunktet for fremkomsten og udviklingen af ​​mange gode udviklingsledelsesmetoder.

Lad os forstå, hvad de SOLIDE principper er, og hvordan de hjælper os.

Skillbox anbefaler: Praktisk kursus "Mobiludvikler PRO".

Påmindelse: for alle læsere af "Habr" - en rabat på 10 rubler ved tilmelding til ethvert Skillbox-kursus ved hjælp af "Habr"-kampagnekoden.

Hvad er SOLID?

Dette udtryk er en forkortelse, hvert bogstav i udtrykket er begyndelsen af ​​navnet på et specifikt princip:

  • SIngle Ansvarsprincip. Et modul kan have én og kun én årsag til ændring.
  • Open/lukket princip (åbent/lukket princip). Klasser og andre elementer bør være åbne for forlængelse, men lukket for ændringer.
  • Liskov Substitutionsprincip (Liskov substitutionsprincippet). Funktioner, der bruger en basistype, bør kunne bruge undertyper af basistypen uden at vide det.
  • IInterfacesegregationsprincip  (grænsefladeadskillelsesprincip). Softwareenheder bør ikke være afhængige af metoder, som de ikke bruger.
  • Dafhængighedsinversionsprincippet (princippet om afhængighedsinversion). Moduler på højere niveauer bør ikke afhænge af moduler på lavere niveauer.

Enkelt ansvarsprincip


Single Responsibility Principle (SRP) siger, at hver klasse eller modul i et program kun skal være ansvarlig for én del af programmets funktionalitet. Derudover bør elementer af dette ansvar tildeles deres egen klasse i stedet for spredt ud over ikke-relaterede klasser. SRP's udvikler og chefevangelist, Robert S. Martin, beskriver ansvarlighed som årsagen til forandring. Han foreslog oprindeligt dette udtryk som et af elementerne i hans arbejde "Principles of Object-Oriented Design". Konceptet inkorporerer meget af det forbindelsesmønster, som tidligere blev defineret af Tom DeMarco.

Konceptet omfattede også flere begreber formuleret af David Parnas. De to vigtigste er indkapsling og informationsskjul. Parnas argumenterede for, at opdeling af et system i separate moduler ikke burde være baseret på analyse af blokdiagrammer eller udførelsesflows. Ethvert af modulerne skal indeholde en specifik løsning, der giver et minimum af information til kunderne.

Martin gav i øvrigt et interessant eksempel med seniorledere i en virksomhed (COO, CTO, CFO), som hver især bruger specifik forretningssoftware til forskellige formål. Som følge heraf kan enhver af dem implementere ændringer i softwaren uden at påvirke andre lederes interesser.

Guddommelig genstand

Som altid er den bedste måde at lære SRP på at se det i aktion. Lad os se på en del af programmet, der IKKE følger princippet om enkelt ansvar. Dette er Ruby-kode, der beskriver rumstationens adfærd og egenskaber.

Gennemgå eksemplet og prøv at bestemme følgende:
Ansvar for de objekter, der er erklæret i SpaceStation-klassen.
Dem, der måtte være interesserede i driften af ​​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} 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

Faktisk er vores rumstation dysfunktionel (jeg tror ikke, jeg får et opkald fra NASA lige nu), men der er noget at analysere her.

SpaceStation-klassen har således flere forskellige ansvarsområder (eller opgaver). Alle kan opdeles i typer:

  • sensorer;
  • forsyninger (forbrugsstoffer);
  • brændstof;
  • acceleratorer.

Selvom ingen af ​​stationens ansatte er tildelt en klasse, kan vi sagtens forestille os, hvem der har ansvaret for hvad. Mest sandsynligt styrer videnskabsmanden sensorerne, logistikeren er ansvarlig for at levere ressourcer, ingeniøren er ansvarlig for brændstofforsyningen, og piloten styrer boosterne.

Kan vi sige, at dette program ikke er SRP-kompatibelt? Ja sikkert. Men SpaceStation-klassen er et typisk "gudeobjekt", der ved alt og gør alt. Dette er et vigtigt anti-mønster i objektorienteret programmering. For en begynder er sådanne genstande ekstremt vanskelige at vedligeholde. Indtil videre er programmet meget enkelt, ja, men forestil dig, hvad der vil ske, hvis vi tilføjer nye funktioner. Måske får vores rumstation brug for en lægestation eller et mødelokale. Og jo flere funktioner der er, jo mere vil SpaceStation vokse. Nå, da denne facilitet vil være forbundet med andre, vil servicering af hele komplekset blive endnu mere kompleks. Som følge heraf kan vi forstyrre driften af ​​for eksempel acceleratorer. Hvis en forsker anmoder om ændringer af sensorerne, 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", og det bliver meget vanskeligt at fastholde et sådant monster i fremtiden. Det er bedst at opdele programmet i separate kodesektioner, som hver især er ansvarlige for at udføre en bestemt operation. Forstå dette, lad os ændre SpaceStation-klassen.

Lad os fordele ansvaret

Ovenfor definerede vi fire typer operationer, der styres af SpaceStation-klassen. Vi vil have dem i tankerne, når vi refaktorerer. Den opdaterede kode passer bedre til 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} 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

Der er mange ændringer, programmet ser bestemt bedre ud nu. Nu er vores SpaceStation-klasse i højere grad blevet en container, hvori operationer påbegyndes for afhængige dele, herunder et sæt sensorer, et forbrugsstofforsyningssystem, en brændstoftank og boostere.

For enhver af variablerne er der nu en tilsvarende klasse: Sensorer; SupplyHold; Brændstoftank; Thrustere.

Der er flere vigtige ændringer i denne version af koden. Pointen er, at individuelle funktioner ikke kun er indkapslet i deres egne klasser, de er organiseret på en sådan måde, at de bliver forudsigelige og konsistente. Vi grupperer elementer med lignende funktionalitet for at følge princippet om sammenhæng. Hvis vi nu skal ændre den måde, systemet fungerer på, ved at flytte fra en hash-struktur til et array, skal du bare bruge SupplyHold-klassen; vi behøver ikke at røre ved andre moduler. På denne måde, hvis logistikmedarbejderen ændrer noget i sin sektion, vil resten af ​​stationen forblive intakt. I dette tilfælde vil SpaceStation-klassen ikke engang være opmærksom på ændringerne.

Vores officerer, der arbejder på rumstationen, er sandsynligvis glade for ændringerne, fordi de kan anmode om dem, de har brug for. Bemærk, at koden har metoder såsom report_supplies og report_fuel indeholdt i SupplyHold og FuelTank klasserne. Hvad ville der ske, hvis Jorden bad om at ændre den måde, den rapporterer på? Begge klasser, SupplyHold og FuelTank, skal ændres. Hvad hvis du har brug for at ændre måden, brændstof og forbrugsstoffer leveres på? Du bliver sandsynligvis nødt til at skifte alle de samme klasser igen. Og det er allerede et brud på SRP-princippet. Lad os ordne dette.

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 denne seneste version af programmet er ansvaret blevet delt op i to nye klasser, FuelReporter og SupplyReporter. De er begge børn af Reporter-klassen. Derudover har vi tilføjet instansvariabler til SpaceStation-klassen, så den ønskede underklasse kan initialiseres om nødvendigt. Hvis Jorden nu beslutter sig for at ændre noget andet, så vil vi lave ændringer til underklasserne og ikke til hovedklassen.

Selvfølgelig er nogle af vores klasser stadig afhængige af hinanden. SupplyReporter-objektet afhænger således af SupplyHold, og FuelReporter afhænger af FuelTank. Boosterne skal naturligvis tilsluttes brændstoftanken. Men her ser alt allerede logisk ud, og det vil ikke være særlig svært at foretage ændringer - redigering af koden for et objekt vil ikke i høj grad påvirke et andet.

Vi har således lavet en modulær kode, hvor hver enkelt af objekternes/klassernes ansvar er præcist defineret. At arbejde med en sådan kode er ikke et problem, det vil være en simpel opgave at vedligeholde den. Vi har konverteret hele det "guddommelige objekt" til SRP.

Skillbox anbefaler:

Kilde: www.habr.com

Tilføj en kommentar