Wir schreiben flexiblen Code unter Verwendung von SOLID

Wir schreiben flexiblen Code unter Verwendung von SOLID

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

Wenn Sie in der Entwicklung tätig sind, haben Sie wahrscheinlich schon von den SOLID-Prinzipien gehört. Sie ermöglichen es Programmierern, sauberen, gut strukturierten und leicht wartbaren Code zu schreiben. Es ist wichtig zu beachten, dass es in der Programmierung verschiedene Ansätze gibt, wie bestimmte Aufgaben richtig ausgeführt werden sollten. Unterschiedliche Fachleute haben unterschiedliche Ansichten und Verständnisse des „richtigen Weges“, was oft von der Erfahrung jedes Einzelnen abhängt. Trotzdem werden die Ideen, die in SOLID vertreten sind, von nahezu allen Vertretern der IT-Community akzeptiert. Sie sind zu einem Ausgangspunkt für die Entstehung und Entwicklung vieler guter Methoden im Management von Softwareentwicklung geworden.

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

Skillbox empfiehlt: Praktischer Kurs „Mobile Entwickler PRO“.

Wir erinnern daran: alle Leser von „Habr“ erhalten einen Rabatt von 10.000 Rubel bei der Anmeldung zu einem beliebigen Kurs von Skillbox mit dem Aktionscode „Habr“.

Was ist SOLID?

Dieser Begriff ist ein Akronym, wobei jeder Buchstabe der Anfang eines bestimmten Prinzips ist:

  • OSingle Responsibility Principle (Prinzip der einzelnen Verantwortung). Ein Modul sollte nur einen Grund für eine Änderung haben.
  • Die DOpen/Closed Principle (Offenheits-/Geschlossenheitsprinzip). Klassen und andere Elemente sollten für Erweiterungen offen, aber für Modifikationen geschlossen sein.
  •  Das LLiskov-Substitutionsprinzip (Liskovsches Substitutionsprinzip). Funktionen, die den Basistyp verwenden, sollten in der Lage sein, Subtypen des Basistyps zu verwenden, ohne darüber Bescheid zu wissen.
  • Die IInterface-Separationsprinzip  (Prinzip der Schnittstellenauftrennung). Software-Einheiten sollten nicht von Methoden abhängen, die sie nicht verwenden.
  • Die OS-AngriffenAbhängigkeitsinversionsprinzip (Prinzip der Abhängigkeit-Inversion). Module höherer Ebenen sollten nicht von Modulen niedrigerer Ebenen abhängen.

Prinzip der einzelnen Verantwortung


Das Prinzip der Einzelverantwortung (SRP) besagt, dass jede Klasse oder jedes Modul in einem Programm nur für einen bestimmten Teil der Funktionalität dieses Programms verantwortlich sein sollte. Darüber hinaus sollten die Elemente dieser Verantwortung an ihre Klasse gebunden und nicht über nicht verwandte Klassen verteilt werden. Der Entwickler und Hauptverfechter des SRP, Robert C. Martin, beschreibt Verantwortung als Ursache für Veränderungen. Er führte diesen Begriff ursprünglich als eines der Elemente seiner Arbeit "Prinzipien der objektorientierten Programmierung" ein. In das Konzept flossen zahlreiche Aspekte der Kohärenz ein, die zuvor von Tom DeMarco definiert wurden.

In das Konzept flossen auch mehrere Begriffe ein, die von David Parnas formuliert wurden. Die beiden Hauptbegriffe sind Kapselung und Informationsverbergung. Parnas argumentierte, dass die Aufteilung eines Systems in separate Module nicht auf der Analyse von Flussdiagrammen oder Ausführungsströmen basieren sollte. Jedes Modul sollte eine bestimmte Lösung enthalten, die den Kunden nur minimale Informationen bereitstellt.

Übrigens gab Martin ein interessantes Beispiel mit den oberen Führungskräften des Unternehmens (COO, CTO, CFO), von denen jeder spezielle Software für unterschiedliche Geschäftszwecke verwendet. Letztendlich kann jeder von ihnen Änderungen an der Software implementieren, ohne die Interessen der anderen Manager zu beeinträchtigen.

Göttliches Objekt

Wie gewohnt ist der beste Weg, das SRP zu lernen, alles in Aktion zu sehen. Schauen wir uns einen Abschnitt des Programms an, der NICHT dem Prinzip der einzelnen Verantwortung entspricht. Dies ist Ruby-Code, der das Verhalten und die Attribute einer Raumstation beschreibt.

Überprüfen Sie das Beispiel und versuchen Sie, Folgendes zu bestimmen:
Die Verantwortlichkeiten der Objekte, die in der Klasse SpaceStation deklariert sind.
Diejenigen, die an der Arbeit der Raumstation interessiert sein könnten.

class SpaceStation
  def initialize
    @supplies = {}
    @fuel = 0
  end
 
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Sensoren werden aktiviert!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Lade #{quantity} Einheiten von #{type} in das Versorgungsdepot."
    
    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 "Verwende #{quantity} von #{type} aus dem Versorgungsdepot."
      @supplies[type] -= quantity
    else
      puts "Versorgungsfehler: Unzureichende #{type} im Versorgungsdepot."
    end
  end
 
  def report_supplies
    puts "----- Supply Report -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} verfügbar: #{quantity} Einheiten"
      end
    else
      puts "Das Versorgungsdepot ist leer."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Lade #{quantity} Einheiten Kraftstoff in den Tank."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} Einheiten Kraftstoff verfügbar."
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @fuel >= 10
      puts "Schubaktion erfolgreich."
      @fuel -= 10
    else
      puts "Thruster-Fehler: Unzureichender Kraftstoff verfügbar."
    end
  end
end

Nun, unsere Raumstation ist derzeit nicht funktionsfähig (ich denke, ich werde in naher Zukunft keinen Anruf von NASA erhalten), aber es gibt hier einiges zu analysieren.

Der Klasse SpaceStation obliegt eine Vielzahl von Verantwortlichkeiten (oder Aufgaben). Alle können nach Typen unterteilt werden:

  • Sensoren;
  • Versorgung (Verbrauchsmaterial);
  • Kraftstoff;
  • Beschleuniger.

Obwohl keiner der Mitarbeiter der Station einer Klasse zugeordnet ist, können wir leicht nachvollziehen, wer für was verantwortlich ist. Wahrscheinlich überwacht der Wissenschaftler die Sensoren, der Logistiker ist für die Versorgung mit Ressourcen zuständig, der Ingenieur kümmert sich um die Treibstoffvorräte, und der Pilot kontrolliert die Beschleuniger.

Können wir sagen, dass dieses Programm nicht dem SRP entspricht? Ja, natürlich. Aber die Klasse SpaceStation ist ein typisches "göttliches Objekt", das alles weiß und alles tut. Dies ist das Hauptantimuster in der objektorientierten Programmierung. Für einen Anfänger sind solche Objekte äußerst schwierig zu warten. Im Moment ist das Programm sehr einfach, ja, aber stellen Sie sich vor, was passieren könnte, wenn wir neue Funktionen hinzufügen. Vielleicht benötigt unsere Raumstation eine Erste-Hilfe-Station oder einen Konferenzraum. Und je mehr Funktionen es gibt, desto komplizierter wird die SpaceStation. Da dieses Objekt zudem mit anderen verbunden ist, wird die Wartung des gesamten Komplexes noch herausfordernder. Am Ende könnten wir zum Beispiel die Funktionalität von Beschleunigern beeinträchtigen. Wenn ein Wissenschaftler Änderungen im Umgang mit Sensoren anfordert, könnte dies durchaus die Kommunikationssysteme der Station beeinträchtigen.

Eine Verletzung des SRP-Prinzips kann kurzzeitig einen taktischen Vorteil bringen, aber letztendlich „verlieren wir den Krieg“. Die Wartung eines solchen Monsters wird in Zukunft sehr schwierig sein. Es ist am besten, das Programm in separate Codeabschnitte zu unterteilen, wobei jeder Abschnitt für die Ausführung einer bestimmten Operation verantwortlich ist. Vor diesem Hintergrund wollen wir die Klasse SpaceStation anpassen.

Verantwortung verteilen

Wir haben oben vier Arten von Operationen definiert, die von der Klasse SpaceStation kontrolliert werden. Bei der Refaktorisierung werden wir dies im Hinterkopf behalten. Der aktualisierte Code entspricht besser dem 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-Aktion -----"
    puts "Sensoren werden ausgeführt!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Lieferaktion -----"
    puts "Lade #{quantity} Einheiten von #{type} im Versorgungslager."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Lieferaktion -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Verwendung von #{quantity} von #{type} aus dem Versorgungslager."
      @supplies[type] -= quantity
    else
      puts "Lieferfehler: Unzureichendes #{type} im Versorgungslager."
    end
  end
 
  def report_supplies
    puts "----- Lieferbericht -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} verfügbar: #{quantity} Einheiten"
      end
    else
      puts "Das Versorgungslager ist leer."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Kraftstoffaktion -----"
    puts "Lade #{quantity} Einheiten Kraftstoff in den Tank."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Kraftstoffaktion -----"
    puts "Verwendung von #{quantity} Einheiten Kraftstoff aus dem Tank."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Kraftstoffbericht -----"
    puts "#{@fuel} Einheiten Kraftstoff verfügbar."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Triebwerkaktion -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Schubaktion erfolgreich."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Triebwerkfehler: Unzureichender Kraftstoff verfügbar."
    end
  end
end

Es gibt viele Änderungen, die Software sieht jetzt definitiv besser aus. Unser SpaceStation-Klasse ist jetzt eher ein Container, in dem die Operationen für abhängige Teile initiiert werden, einschließlich einer Sensorgruppe, einem Liefermaterial-System, einem Tank und Triebwerken.

Für jede der Variablen gibt es jetzt die entsprechende Klasse: Sensors; SupplyHold; FuelTank; Thrusters.

In dieser Version des Codes gibt es einige wichtige Änderungen. Die einzelnen Funktionen sind nicht nur in eigenen Klassen gekapselt, sondern sie sind auch so organisiert, dass sie vorhersehbar und konsistent werden. Wir gruppieren funktional ähnliche Elemente, um den Kohäsionsprinzipien zu folgen. Wenn wir das System ändern müssen, indem wir von einer Hash-Struktur zu einem Array wechseln, verwenden wir einfach die Klasse SupplyHold, ohne andere Module berühren zu müssen. So bleibt, wenn der für die Logistik zuständige Offizier etwas in seinem Bereich ändert, der Rest der Station unberührt. Die SpaceStation-Klasse wird von den Änderungen sogar nichts mitbekommen.

Unsere Offiziere, die auf der Raumstation arbeiten, freuen sich wahrscheinlich über die Änderungen, da sie die für sie notwendigen anfordern können. Beachten Sie, dass es im Code Methoden wie report_supplies und report_fuel gibt, die in den Klassen SupplyHold und FuelTank enthalten sind. Was passiert, wenn die Erde darum bittet, den Berichtserstellungsprozess zu ändern? Es müsste in beiden Klassen, SupplyHold und FuelTank, eine Änderung vorgenommen werden. Und was ist, wenn die Art der Kraftstoff- und Verbrauchsmittelbelieferung geändert werden muss? Wahrscheinlich müssten dann wieder alle diese Klassen geändert werden. Das widerspricht bereits dem SRP-Prinzip. Lassen Sie uns das beheben.

class Raumstation
  attr_reader :sensoren, :vorratslager, :vorratsberichtgeber,
              :kraftstofftank, :kraftstoffberichtgeber, :triebwerke
 
  def initialize
    @sensoren = Sensoren.new
    @vorratslager = Vorratslager.new
    @vorratsberichtgeber = Vorratsberichtgeber.new(@vorratslager)
    @kraftstofftank = Kraftstofftank.new
    @kraftstoffberichtgeber = Kraftstoffberichtgeber.new(@kraftstofftank)
    @triebwerke = Triebwerke.new(@kraftstofftank)
  end
end
 
class Sensoren
  def run_sensoren
    puts "----- Sensor Aktion -----"
    puts "Sensoren aktivieren!"
  end
end
 
class Vorratslager
  attr_accessor :vorräte
  attr_reader :berichtgeber
 
  def initialize
    @vorräte = {}
  end
 
  def get_vorräte
    @vorräte
  end
 
  def lade_vorräte(typ, menge)
    puts "----- Vorrats Aktion -----"
    puts "Lade #{menge} Einheiten von #{typ} in das Vorratslager."
    
    if @vorräte[typ]
      @vorräte[typ] += menge
    else
      @vorräte[typ] = menge
    end
  end
 
  def verwende_vorräte(typ, menge)
    puts "----- Vorrats Aktion -----"
    if @vorräte[typ] != nil && @vorräte[typ] > menge
      puts "Verwende #{menge} von #{typ} aus dem Vorratslager."
      @vorräte[typ] -= menge
    else
      puts "Vorratsfehler: Unzureichende #{typ} im Vorratslager."
    end
  end
end
 
class Kraftstofftank
  attr_accessor :kraftstoff
  attr_reader :berichtgeber
 
  def initialize
    @kraftstoff = 0
  end
 
  def get_kraftstoffniveaus
    @kraftstoff
  end
 
  def lade_kraftstoff(menge)
    puts "----- Kraftstoff Aktion -----"
    puts "Lade #{menge} Einheiten Kraftstoff in den Tank."
    @kraftstoff += menge
  end
 
  def verwende_kraftstoff(menge)
    puts "----- Kraftstoff Aktion -----"
    puts "Verwende #{menge} Einheiten Kraftstoff aus dem Tank."
    @kraftstoff -= menge
  end
end
 
class Triebwerke
  KRAFTSTOFF_PRO_TRIEB = 10
 
  def initialize(kraftstofftank)
    @verknüpft_kraftstofftank = kraftstofftank
  end
 
  def aktiviere_triebwerke
    puts "----- Triebwerk Aktion -----"
    
    if @verknüpft_kraftstofftank.get_kraftstoffniveaus >= KRAFTSTOFF_PRO_TRIEB
      puts "Schubaktion erfolgreich."
      @verknüpft_kraftstofftank.verwendekraftstoff(KRAFTSTOFF_PRO_TRIEB)
    else
      puts "Triebwerk Fehler: Unzureichender Kraftstoff vorhanden."
    end
  end
end
 
class Berichtgeber
  def initialize(element, typ)
    @verknüpft_element = element
    @typ = typ
  end
 
  def bericht
    puts "----- #{@typ.capitalize} Bericht -----"
  end
end
 
class Kraftstoffberichtgeber < Berichtgeber
  def initialize(element)
    super(element, "kraftstoff")
  end
 
  def bericht
    super
    puts "#{@verknüpft_element.get_kraftstoffniveaus} Einheiten Kraftstoff verfügbar."
  end
end
 
class Vorratsberichtgeber  0
      @verknüpft_element.get_vorräte.each do |typ, menge|
        puts "#{typ} verfügbar: #{menge} Einheiten"
      end
    else
      puts "Vorratslager ist leer."
    end
  end
end
 
iss = Raumstation.new
 
iss.sensoren.run_sensoren
  # ----- Sensor Aktion -----
  # Sensoren aktivieren!
 
iss.vorratslager.verwende_vorräte("teile", 2)
  # ----- Vorrats Aktion -----
  # Vorratsfehler: Unzureichende Teile im Vorratslager.
iss.vorratslager.lade_vorräte("teile", 10)
  # ----- Vorrats Aktion -----
  # Lade 10 Einheiten von Teilen in das Vorratslager.
iss.vorratslager.verwende_vorräte("teile", 2)
  # ----- Vorrats Aktion -----
  # Verwende 2 von Teilen aus dem Vorratslager.
iss.vorratsberichtgeber.bericht
  # ----- Vorrats Bericht -----
  # Teile verfügbar: 8 Einheiten
 
iss.triebwerke.aktiviere_triebwerke
  # ----- Triebwerk Aktion -----
  # Triebwerk Fehler: Unzureichender Kraftstoff vorhanden.
iss.kraftstofftank.lade_kraftstoff(100)
  # ----- Kraftstoff Aktion -----
  # Lade 100 Einheiten Kraftstoff in den Tank.
iss.triebwerke.aktiviere_triebwerke
  # ----- Triebwerk Aktion -----
  # Schubaktion erfolgreich.
  # ----- Kraftstoff Aktion -----
  # Verwende 10 Einheiten Kraftstoff aus dem Tank.
iss.kraftstoffberichtgeber.bericht
  # ----- Kraftstoff Bericht -----
# 90 Einheiten Kraftstoff verfügbar.

In dieser letzten Version des Programms wurden die Verantwortlichkeiten auf zwei neue Klassen aufgeteilt: FuelReporter und SupplyReporter. Beide sind Unterklassen der Klasse Reporter. Außerdem haben wir Instanzvariablen zur Klasse SpaceStation hinzugefügt, um bei Bedarf die richtige Unterklasse zu initialisieren. Wenn die Erde also etwas ändern möchte, werden wir Änderungen in den Unterklassen vornehmen und nicht in der Hauptklasse.

Natürlich hängen einige Klassen immer noch voneinander ab. So hängt das Objekt SupplyReporter von SupplyHold ab, während FuelReporter von FuelTank abhängt. Außerdem müssen die Treibstoffe mit dem Treibstofftank verbunden sein. Aber das macht hier alles Sinn, und Änderungen werden nicht besonders schwierig sein – die Bearbeitung des Codes eines Objekts hat nicht allzu große Auswirkungen auf ein anderes.

So haben wir modularen Code erstellt, in dem die Verantwortlichkeiten jedes Objekts/Klasse klar definiert sind. Mit solch einem Code zu arbeiten, ist kein Problem; seine Wartung wird eine einfache Aufgabe sein. Das gesamte "göttliche Objekt" haben wir in SRP umgewandelt.

Skillbox empfiehlt:

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster