Wir schreiben flexiblen Code mit SOLID

Wir schreiben flexiblen Code mit SOLID

Vom Übersetzer: für Sie veröffentlicht Artikel von Severin Perez über die Verwendung von SOLID-Prinzipien in der Programmierung. Die Informationen aus dem Artikel werden sowohl für Anfänger als auch für erfahrene Programmierer nützlich sein.

Wenn Sie sich für Entwicklung interessieren, haben Sie höchstwahrscheinlich schon von den SOLID-Prinzipien gehört. Sie ermöglichen es dem Programmierer, sauberen, gut strukturierten und leicht zu wartenden Code zu schreiben. Es ist erwähnenswert, dass es in der Programmierung mehrere Ansätze gibt, wie eine bestimmte Aufgabe korrekt ausgeführt werden kann. Verschiedene Spezialisten haben unterschiedliche Vorstellungen und Verständnisse vom „richtigen Weg“; es hängt alles von der Erfahrung jedes Einzelnen ab. Die in SOLID proklamierten Ideen werden jedoch von fast allen Vertretern der IT-Community akzeptiert. Sie wurden zum Ausgangspunkt für die Entstehung und Entwicklung vieler guter Entwicklungsmanagementpraktiken.

Lassen Sie uns verstehen, was die SOLID-Prinzipien sind und wie sie uns helfen.

Skillbox empfiehlt: Praktischer Kurs „Mobile Developer PRO“.

Erinnerung: für alle Leser von „Habr“ – ein Rabatt von 10 Rubel bei der Anmeldung zu einem beliebigen Skillbox-Kurs mit dem Aktionscode „Habr“.

Was ist SOLID?

Dieser Begriff ist eine Abkürzung, jeder Buchstabe des Begriffs ist der Anfang des Namens eines bestimmten Prinzips:

  • SPrinzip der Einzelverantwortung. Ein Modul kann genau einen Änderungsgrund haben.
  • Das OStift/geschlossenes Prinzip (Offen/Geschlossen-Prinzip). Klassen und andere Elemente sollten für Erweiterungen geöffnet, für Änderungen jedoch geschlossen sein.
  •  Das LIskov-Substitutionsprinzip (Liskov-Substitutionsprinzip). Funktionen, die einen Basistyp verwenden, sollten in der Lage sein, Untertypen des Basistyps zu verwenden, ohne es zu wissen.
  • Das IPrinzip der Schnittstellentrennung  (Grenzflächentrennungsprinzip). Softwareeinheiten sollten nicht von Methoden abhängig sein, die sie nicht verwenden.
  • Das DAbhängigkeitsinversionsprinzip (Prinzip der Abhängigkeitsumkehr). Module auf höheren Ebenen sollten nicht von Modulen auf niedrigeren Ebenen abhängen.

Prinzip der Einzelverantwortung


Das Single-Responsibility-Prinzip (SRP) besagt, dass jede Klasse oder jedes Modul in einem Programm nur für einen Teil der Funktionalität dieses Programms verantwortlich sein sollte. Darüber hinaus sollten Elemente dieser Verantwortung einer eigenen Klasse zugewiesen werden und nicht auf nicht verwandte Klassen verteilt werden. Der Entwickler und Chefevangelist von SRP, Robert S. Martin, beschreibt Verantwortung als Grund für Veränderungen. Er schlug diesen Begriff ursprünglich als eines der Elemente seiner Arbeit „Prinzipien des objektorientierten Designs“ vor. Das Konzept beinhaltet einen Großteil des Konnektivitätsmusters, das zuvor von Tom DeMarco definiert wurde.

Das Konzept umfasste auch mehrere von David Parnas formulierte Konzepte. Die beiden wichtigsten sind Kapselung und Informationsversteckung. Parnas argumentierte, dass die Aufteilung eines Systems in separate Module nicht auf der Analyse von Blockdiagrammen oder Ausführungsabläufen basieren sollte. Jedes der Module muss eine spezifische Lösung enthalten, die den Kunden ein Minimum an Informationen bietet.

Martin hat übrigens ein interessantes Beispiel mit leitenden Managern eines Unternehmens (COO, CTO, CFO) angeführt, die jeweils spezifische Unternehmenssoftware für unterschiedliche Zwecke nutzen. Dadurch kann jeder von ihnen Änderungen an der Software vornehmen, ohne die Interessen anderer Manager zu beeinträchtigen.

Göttliches Objekt

Wie immer ist der beste Weg, SRP zu lernen, es in Aktion zu sehen. Schauen wir uns einen Abschnitt des Programms an, der NICHT dem Prinzip der Einzelverantwortung folgt. Dies ist Ruby-Code, der das Verhalten und die Eigenschaften der Raumstation beschreibt.

Sehen Sie sich das Beispiel an und versuchen Sie Folgendes herauszufinden:
Verantwortlichkeiten der Objekte, die in der SpaceStation-Klasse deklariert sind.
Diejenigen, die sich für den Betrieb der Raumstation interessieren könnten.

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

Eigentlich ist unsere Raumstation nicht funktionsfähig (ich glaube nicht, dass ich bald einen Anruf von der NASA bekomme), aber hier gibt es etwas zu analysieren.

Daher hat die SpaceStation-Klasse mehrere unterschiedliche Verantwortlichkeiten (oder Aufgaben). Alle von ihnen können in Typen unterteilt werden:

  • Sensoren;
  • Lieferungen (Verbrauchsmaterialien);
  • Treibstoff;
  • Beschleuniger.

Auch wenn keiner der Mitarbeiter der Station einer Klasse zugeordnet ist, können wir uns gut vorstellen, wer wofür verantwortlich ist. Höchstwahrscheinlich steuert der Wissenschaftler die Sensoren, der Logistiker ist für die Bereitstellung von Ressourcen verantwortlich, der Ingenieur ist für die Treibstoffversorgung verantwortlich und der Pilot steuert die Booster.

Können wir sagen, dass dieses Programm nicht SRP-konform ist? Ja natürlich. Aber die SpaceStation-Klasse ist ein typisches „Gottobjekt“, das alles weiß und alles tut. Dies ist ein wichtiges Anti-Pattern in der objektorientierten Programmierung. Für einen Anfänger sind solche Objekte äußerst schwierig zu warten. Bisher ist das Programm zwar sehr einfach, aber stellen Sie sich vor, was passieren wird, wenn wir neue Funktionen hinzufügen. Vielleicht braucht unsere Raumstation eine medizinische Station oder einen Besprechungsraum. Und je mehr Funktionen es gibt, desto größer wird die SpaceStation. Nun, da diese Anlage mit anderen verbunden wird, wird die Wartung des gesamten Komplexes noch komplexer. Dadurch können wir beispielsweise den Betrieb von Beschleunigern stören. Wenn ein Forscher Änderungen an den Sensoren wünscht, könnte dies durchaus Auswirkungen auf die Kommunikationssysteme der Station haben.

Ein Verstoß gegen das SRP-Prinzip kann zu einem kurzfristigen taktischen Sieg führen, aber am Ende werden wir „den Krieg verlieren“ und es wird sehr schwierig, ein solches Monster in Zukunft aufrechtzuerhalten. Am besten unterteilen Sie das Programm in separate Codeabschnitte, von denen jeder für die Ausführung einer bestimmten Operation verantwortlich ist. Nachdem wir dies verstanden haben, ändern wir die SpaceStation-Klasse.

Teilen wir die Verantwortung auf

Oben haben wir vier Arten von Operationen definiert, die von der SpaceStation-Klasse gesteuert werden. Wir werden sie beim Refactoring berücksichtigen. Der aktualisierte Code entspricht besser der 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

Es gibt viele Änderungen, das Programm sieht jetzt definitiv besser aus. Jetzt ist unsere SpaceStation-Klasse eher zu einem Container geworden, in dem Vorgänge für abhängige Teile eingeleitet werden, darunter eine Reihe von Sensoren, ein Verbrauchsmaterialversorgungssystem, ein Kraftstofftank und Booster.

Für jede der Variablen gibt es jetzt eine entsprechende Klasse: Sensoren; SupplyHold; Treibstofftank; Triebwerke.

In dieser Version des Codes gibt es mehrere wichtige Änderungen. Der Punkt ist, dass einzelne Funktionen nicht nur in ihren eigenen Klassen gekapselt sind, sondern auch so organisiert sind, dass sie vorhersehbar und konsistent sind. Wir gruppieren Elemente mit ähnlicher Funktionalität, um dem Prinzip der Kohärenz zu folgen. Wenn wir nun die Funktionsweise des Systems ändern müssen, indem wir von einer Hash-Struktur zu einem Array wechseln, verwenden wir einfach die SupplyHold-Klasse; wir müssen keine anderen Module berühren. Wenn also der Logistiker in seinem Bereich etwas ändert, bleibt der Rest der Station erhalten. In diesem Fall wird die SpaceStation-Klasse die Änderungen nicht einmal bemerken.

Unsere auf der Raumstation arbeitenden Beamten freuen sich wahrscheinlich über die Änderungen, da sie die Änderungen anfordern können, die sie benötigen. Beachten Sie, dass der Code Methoden wie „report_supplies“ und „report_fuel“ enthält, die in den Klassen „SupplyHold“ und „FuelTank“ enthalten sind. Was würde passieren, wenn die Erde darum bitten würde, ihre Berichterstattung zu ändern? Beide Klassen, SupplyHold und FuelTank, müssen geändert werden. Was ist, wenn Sie die Art und Weise der Lieferung von Kraftstoff und Verbrauchsmaterialien ändern müssen? Sie müssen wahrscheinlich alle gleichen Klassen erneut ändern. Und das ist bereits ein Verstoß gegen das SRP-Prinzip. Lassen Sie uns das beheben.

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.

In dieser neuesten Version des Programms wurden die Verantwortlichkeiten in zwei neue Klassen aufgeteilt: FuelReporter und SupplyReporter. Sie sind beide Kinder der Reporter-Klasse. Darüber hinaus haben wir der SpaceStation-Klasse Instanzvariablen hinzugefügt, damit bei Bedarf die gewünschte Unterklasse initialisiert werden kann. Wenn die Erde nun beschließt, etwas anderes zu ändern, werden wir Änderungen an den Unterklassen vornehmen und nicht an der Hauptklasse.

Natürlich sind einige unserer Kurse weiterhin voneinander abhängig. Somit hängt das SupplyReporter-Objekt von SupplyHold ab und FuelReporter hängt von FuelTank ab. Natürlich müssen die Booster an den Kraftstofftank angeschlossen werden. Aber hier sieht alles schon logisch aus und Änderungen werden nicht besonders schwierig sein – die Bearbeitung des Codes eines Objekts hat keine großen Auswirkungen auf ein anderes.

Daher haben wir einen modularen Code erstellt, in dem die Verantwortlichkeiten der einzelnen Objekte/Klassen genau definiert sind. Die Arbeit mit einem solchen Code ist kein Problem, die Wartung wird eine einfache Aufgabe sein. Wir haben das gesamte „göttliche Objekt“ in SRP umgewandelt.

Skillbox empfiehlt:

Source: habr.com

Kommentar hinzufügen