Wir schreiben flexiblen Code unter Verwendung von SOLID

Wir schreiben flexiblen Code unter Verwendung von SOLID

Von dem Übersetzer: haben wir 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 hilfreich.

Wenn Sie sich mit Entwicklung beschäftigen, haben Sie wahrscheinlich von den SOLID-Prinzipien gehört. Sie ermöglichen es Programmierern, sauberen, gut strukturierten und leicht wartbaren Code zu schreiben. Es ist anzumerken, dass es in der Programmierung verschiedene Ansätze gibt, wie man bestimmte Aufgaben korrekt ausführt. Unterschiedliche Fachleute haben unterschiedliche Ideen und Auffassungen vom 'richtigen Weg', alles hängt von den Erfahrungen jedes Einzelnen ab. Dennoch werden die Ideen, die in SOLID verkündet werden, von fast allen Vertretern der IT-Community akzeptiert. Sie wurden zum Ausgangspunkt für die Entstehung und Entwicklung vieler guter Methoden im Projektmanagement.

Lassen Sie uns klären, was die SOLID-Prinzipien sind und wie sie uns helfen.

Skillbox empfiehlt: Praktischer Kurs „Mobiler 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, jeder Buchstabe steht für den Anfang eines bestimmten Prinzips:

  • SSingle Responsibility Principle (Prinzip der einzigen Verantwortung). Ein Modul sollte nur einen Grund für eine Änderung haben.
  • Der Open/Closed Principle (Prinzip der Offenheit/Schlossenheit). Klassen und andere Elemente sollten für Erweiterungen offen sein, aber für Modifikationen geschlossen.
  •  Der Liskov Substitution Principle (Prinzip der Liskovschen Substitution). Funktionen, die den Basistyp verwenden, sollten in der Lage sein, Untertypen des Basistyps zu verwenden, ohne davon zu wissen.
  • Der Interface Segregation Principle (Prinzip der Schnittstellen-Trennung). Softwareeinheiten sollten nicht von Methoden abhängen, die sie nicht verwenden.
  • Der Dependency Inversion Principle (Prinzip der Abhängkeitsumkehr). Module höherer Ebenen sollten nicht von Modulen niedrigerer Ebenen abhängen.

Prinzip der einzigen Verantwortung

 
Das Prinzip der einzelnen Verantwortung (SRP) besagt, dass jede Klasse oder jedes Modul im Programm nur für einen Teil der Funktionalität dieses Programms verantwortlich sein sollte. Darüber hinaus sollten die Elemente dieser Verantwortung an ihrer Klasse festgelegt werden und nicht über nicht verwandte Klassen verteilt sein. Der Entwickler und Hauptverfechter des SRP, Robert C. Martin, beschreibt Verantwortung als die Ursache für Veränderungen. Ursprünglich führte er diesen Begriff als eines der Elemente seiner Arbeit 'Prinzipien des objektorientierten Designs' ein. In das Konzept flossen viele Aspekte der Kohäsionsgesetzmäßigkeit ein, die zuvor von Tom DeMarco definiert wurde.

In das Konzept gingen 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 einer Analyse von Flussdiagrammen oder Ausführungsströmen basieren sollte. Jedes der Module sollte eine bestimmte Lösung enthalten, die den Kunden mindestens Informationen bereitstellt.

Übrigens brachte Martin ein interessantes Beispiel mit höheren Managern eines Unternehmens (COO, CTO, CFO), von denen jeder spezifische Software für das Geschäft zu unterschiedlichen Zwecken verwendet. Letztendlich kann jeder von ihnen Änderungen an der Software vornehmen, ohne die Interessen der anderen Manager zu berühren.

Göttliches Objekt

Wie immer ist der beste Weg, SRP zu verstehen, 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.

Sehen Sie sich das Beispiel an 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 Raumstation
  def initialize
    @vorräte = {}
    @kraftstoff = 0
  end
 
  def run_sensors
    puts "----- Sensor Aktion -----"
    puts "Sensoren laufen!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Lieferaktion -----"
    puts "Lade #{quantity} Einheiten von #{type} in den Lagerraum."
    
    if @vorräte[type]
      @vorräte[type] += quantity
    else
      @vorräte[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Lieferaktion -----"
    if @vorräte[type] != nil && @vorräte[type] > quantity
      puts "Verwende #{quantity} von #{type} aus dem Lager."
      @vorräte[type] -= quantity
    else
      puts "Lieferfehler: Unzureichende #{type} im Lager."
    end
  end
 
  def report_supplies
    puts "----- Lieferbericht -----"
    if @vorräte.keys.length > 0
      @vorräte.each do |type, quantity|
        puts "#{type} verfügbar: #{quantity} Einheiten"
      end
    else
      puts "Lager ist leer."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Kraftstoffaktion -----"
    puts "Lade #{quantity} Einheiten Kraftstoff in den Tank."
    @kraftstoff += quantity
  end
 
  def report_fuel
    puts "----- Kraftstoffbericht -----"
    puts "#{@kraftstoff} Einheiten Kraftstoff verfügbar."
  end
 
  def activate_thrusters
    puts "----- Triebwerksaktion -----"
    if @kraftstoff >= 10
      puts "Triebwerksaktion erfolgreich."
      @kraftstoff -= 10
    else
      puts "Triebwerkfehler: Unzureichender Kraftstoff verfügbar."
    end
  end
end

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

Die Klasse Raumstation hat mehrere verschiedene Verantwortlichkeiten (oder Aufgaben). Alle können nach Typen unterteilt werden:

  • Sensoren;
  • Vorräte (Verbrauchsmaterialien);
  • Kraftstoff;
  • Triebwerke.

Obwohl kein Mitarbeiter der Station in die Klasse eingeordnet ist, können wir leicht annehmen, wer für was verantwortlich ist. Höchstwahrscheinlich überwacht ein Wissenschaftler die Sensoren, ein Logistiker ist für die Bereitstellung der Ressourcen zuständig, ein Ingenieur kümmert sich um die Kraftstoffvorräte und ein Pilot kontrolliert die Triebwerke.

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 grundlegende Antimuster in der objektorientierten Programmierung. Für Anfänger sind solche Objekte äußerst schwierig zu warten. Bisher ist das Programm sehr einfach, ja, aber stellen Sie sich vor, was passiert, wenn wir neue Funktionen hinzufügen. Vielleicht benötigt unsere Raumstation einen medizinischen Raum oder einen Besprechungsraum. Und je mehr Funktionen es gibt, desto größer wird die SpaceStation. Und da dieses Objekt mit anderen verbunden sein wird, wird die Wartung des gesamten Komplexes noch komplizierter. Letztendlich könnten wir die Funktionsweise beispielsweise der Beschleuniger beeinträchtigen. Wenn ein Wissenschaftler Änderungen an der Sensortechnologie anfordert, könnte dies durchaus die Kommunikationssysteme der Station betreffen.

Eine Verletzung des SRP-Prinzips kann einen kurzfristigen taktischen Sieg bringen, aber letztendlich „verlieren wir den Krieg“, und es wird ziemlich kompliziert, dieses Monster in der Zukunft zu warten. Am besten ist es, das Programm in separate Codeabschnitte zu unterteilen, von denen jeder für die Durchführung einer bestimmten Operation verantwortlich ist. Unter Berücksichtigung dessen, lassen Sie uns die Klasse SpaceStation ändern.

Verantwortung verteilen

Wir haben oben vier Arten von Operationen definiert, die von der Klasse SpaceStation gesteuert werden. Bei der Refaktorisierung werden wir dies im Hinterkopf behalten. Der aktualisierte Code entspricht besser dem SRP.

class Raumstation
  attr_reader :sensoren, :vorratslager, :kraftstofftank, :triebwerke
 
  def initialize
    @vorratslager = Vorratslager.new
    @sensoren = Sensoren.new
    @kraftstofftank = Kraftstofftank.new
    @triebwerke = Triebwerke.new(@kraftstofftank)
  end
end
 
class Sensoren
  def run_sensoren
    puts "----- Sensoraktion -----"
    puts "Sensoren werden ausgeführt!"
  end
end
 
class Vorratslager
  attr_accessor :vorräte
 
  def initialize
    @vorräte = {}
  end
 
  def lade_vorräte(typ, menge)
    puts "----- Vorratsaktion -----"
    puts "Lade #{menge} Einheiten von #{typ} ins Vorratslager."
    
    if @vorräte[typ]
      @vorräte[typ] += menge
    else
      @vorräte[typ] = menge
    end
  end
 
  def verwende_vorräte(typ, menge)
    puts "----- Vorratsaktion -----"
    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
 
  def berichte_vorräte
    puts "----- Vorratsbericht -----"
    if @vorräte.keys.length > 0
      @vorräte.each do |typ, menge|
        puts "#{typ} verfügbar: #{menge} Einheiten"
      end
    else
      puts "Vorratslager ist leer."
    end
  end
end
 
class Kraftstofftank
  attr_accessor :kraftstoff
 
  def initialize
    @kraftstoff = 0
  end
 
  def get_kraftstoffstände
    @kraftstoff
  end
 
  def lade_kraftstoff(menge)
    puts "----- Kraftstoffaktion -----"
    puts "Lade #{menge} Einheiten Kraftstoff in den Tank."
    @kraftstoff += menge
  end
 
  def verwende_kraftstoff(menge)
    puts "----- Kraftstoffaktion -----"
    puts "Verwende #{menge} Einheiten Kraftstoff aus dem Tank."
    @kraftstoff -= menge
  end
 
  def berichte_kraftstoff
    puts "----- Kraftstoffbericht -----"
    puts "#{@kraftstoff} Einheiten Kraftstoff verfügbar."
  end
end
 
class Triebwerke
  def initialize(kraftstofftank)
    @linked_kraftstofftank = kraftstofftank
  end
 
  def aktiviere_triebwerke
    puts "----- Triebwerksaktion -----"
    if @linked_kraftstofftank.get_kraftstoffstände >= 10
      puts "Schubaktion erfolgreich."
      @linked_kraftstofftank.verwendekraftstoff(10)
    else
      puts "Triebwerksfehler: Unzureichender Kraftstoff verfügbar."
    end
  end
end

Es gibt viele Änderungen, das Programm sieht jetzt definitiv besser aus. Unser Raumstation-Klasse ist nun eher ein Container, in dem Operationen für die abhängigen Teile initiiert werden, einschließlich einer Reihe von Sensoren, einem System zur Bereitstellung von Verbrauchsmaterialien, einem Kraftstofftank und Triebwerken.

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

In dieser Version des Codes gibt es mehrere wichtige Änderungen. Die Tatsache ist, dass einzelne Funktionen nicht nur in eigenen Klassen gekapselt sind, sondern auch so organisiert sind, dass sie vorhersehbar und konsistent werden. Wir gruppieren funktional ähnliche Elemente, um dem Kohärenzprinzip zu folgen. Wenn wir nun das Funktionsprinzip des Systems ändern müssen, von einer Hash-Struktur auf ein Array wechseln, nutzen Sie einfach die Klasse SupplyHold, ohne dass andere Module betroffen sind. Auf diese Weise bleibt der Rest der Station unberührt, selbst wenn der für die Logistik zuständige Offizier etwas in seinem Bereich ändert. Die Klasse SpaceStation wird nichts von den Änderungen mitbekommen.

Unsere Offiziere, die auf der Raumstation arbeiten, sind wahrscheinlich erfreut ü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 um eine Änderung der Berichtserstellung bittet? Dann müssen beide Klassen, SupplyHold und FuelTank, geändert werden. Und was ist, wenn der Lieferweg für Treibstoff und Verbrauchsmaterialien verändert werden muss? Wahrscheinlich müssen ebenfalls wieder all diese Klassen geändert werden. Das wäre bereits ein Verstoß gegen das SRP-Prinzip. Lassen Sie uns das beheben.

class Raumstation
  attr_reader :sensoren, :vorratskammer, :vorrats_reporter,
              :kraftstofftank, :kraftstoff_reporter, :thruster
 
  def initialize
    @sensoren = Sensoren.new
    @vorratskammer = Vorratskammer.new
    @vorrats_reporter = VorratsReporter.new(@vorratskammer)
    @kraftstofftank = Kraftstofftank.new
    @kraftstoff_reporter = KraftstoffReporter.new(@kraftstofftank)
    @thruster = Thruster.new(@kraftstofftank)
  end
end
 
class Sensoren
  def run_sensoren
    puts "----- Sensoraktion -----"
    puts "Sensoren laufen!"
  end
end
 
class Vorratskammer
  attr_accessor :vorräte
  attr_reader :reporter
 
  def initialize
    @vorräte = {}
  end
 
  def get_vorräte
    @vorräte
  end
 
  def load_vorräte(typ, menge)
    puts "----- Vorratsaktion -----"
    puts "Lade #{menge} Einheiten von #{typ} in die Vorratskammer."
    
    if @vorräte[typ]
      @vorräte[typ] += menge
    else
      @vorräte[typ] = menge
    end
  end
 
  def use_vorräte(typ, menge)
    puts "----- Vorratsaktion -----"
    if @vorräte[typ] != nil && @vorräte[typ] > menge
      puts "Verwende #{menge} von #{typ} aus der Vorratskammer."
      @vorräte[typ] -= menge
    else
      puts "Vorratsfehler: Unzureichende #{typ} in der Vorratskammer."
    end
  end
end
 
class Kraftstofftank
  attr_accessor :kraftstoff
  attr_reader :reporter
 
  def initialize
    @kraftstoff = 0
  end
 
  def get_kraftstoffstände
    @kraftstoff
  end
 
  def load_kraftstoff(menge)
    puts "----- Kraftstoffaktion -----"
    puts "Lade #{menge} Einheiten von Kraftstoff in den Tank."
    @kraftstoff += menge
  end
 
  def use_kraftstoff(menge)
    puts "----- Kraftstoffaktion -----"
    puts "Verwende #{menge} Einheiten von Kraftstoff aus dem Tank."
    @kraftstoff -= menge
  end
end
 
class Thruster
  KRAFTSTOFF_PRO_THRUST = 10
 
  def initialize(kraftstofftank)
    @verbunden_kraftstofftank = kraftstofftank
  end
 
  def activate_thruster
    puts "----- Thruster Aktion -----"
    
    if @verbunden_kraftstofftank.get_kraftstoffstände >= KRAFTSTOFF_PRO_THRUST
      puts "Thrust-Aktion erfolgreich."
      @verbunden_kraftstofftank.use_kraftstoff(KRAFTSTOFF_PRO_THRUST)
    else
      puts "Thrusterfehler: Unzureichender Kraftstoff verfügbar."
    end
  end
end
 
class Reporter
  def initialize(item, typ)
    @verbunden_item = item
    @typ = typ
  end
 
  def report
    puts "----- #{@typ.capitalize} Bericht -----"
  end
end
 
class KraftstoffReporter < Reporter
  def initialize(item)
    super(item, "kraftstoff")
  end
 
  def report
    super
    puts "#{@verbunden_item.get_kraftstoffstände} Einheiten Kraftstoff verfügbar."
  end
end
 
class VorratsReporter  0
      @verbunden_item.get_vorräte.each do |typ, menge|
        puts "#{typ} verfügbar: #{menge} Einheiten"
      end
    else
      puts "Vorratskammer ist leer."
    end
  end
end
 
iss = Raumstation.new
 
iss.sensoren.run_sensoren
  # ----- Sensoraktion -----
  # Sensoren laufen!
 
iss.vorratskammer.use_vorräte("teile", 2)
  # ----- Vorratsaktion -----
  # Vorratsfehler: Unzureichende Teile in der Vorratskammer.
iss.vorratskammer.load_vorräte("teile", 10)
  # ----- Vorratsaktion -----
  # Lade 10 Einheiten Teile in die Vorratskammer.
iss.vorratskammer.use_vorräte("teile", 2)
  # ----- Vorratsaktion -----
  # Verwende 2 Teile aus der Vorratskammer.
iss.vorrats_reporter.report
  # ----- Vorratsbericht -----
  # Teile verfügbar: 8 Einheiten
 
iss.thruster.activate_thruster
  # ----- Thruster Aktion -----
  # Thrusterfehler: Unzureichender Kraftstoff verfügbar.
iss.kraftstofftank.load_kraftstoff(100)
  # ----- Kraftstoffaktion -----
  # Lade 100 Einheiten Kraftstoff in den Tank.
iss.thruster.activate_thruster
  # ----- Thruster Aktion -----
  # Thrust-Aktion erfolgreich.
  # ----- Kraftstoffaktion -----
  # Verwende 10 Einheiten Kraftstoff aus dem Tank.
iss.kraftstoff_reporter.report
  # ----- Kraftstoffbericht -----
# 90 Einheiten Kraftstoff verfügbar.

In dieser letzten Version des Programms wurden die Aufgaben auf zwei neue Klassen, FuelReporter und SupplyReporter, aufgeteilt. Beide sind Unterklassen der Klasse Reporter. Darüber hinaus haben wir Instanzvariablen zur Klasse SpaceStation hinzugefügt, um bei Bedarf die benötigte Unterklasse zu initialisieren. Jetzt, wenn die Erde etwas ändern möchte, werden wir die Unterklassen anpassen und nicht die Hauptklasse.

Natürlich hängen einige Klassen immer noch voneinander ab. So ist das Objekt SupplyReporter von SupplyHold abhängig, während FuelReporter von FuelTank abhängt. Selbstverständlich müssen die Booster mit dem Treibstofftank verbunden sein. Aber hier scheint alles sinnvoll und Änderungen werden nicht besonders schwierig sein – die Bearbeitung des Codes eines Objekts wird den anderen nicht stark beeinflussen.

So haben wir modularen Code erstellt, bei dem die Aufgaben jedes Objekts/Klasse eindeutig definiert sind. Mit diesem Code zu arbeiten ist kein Problem, die Wartung wird eine einfache Aufgabe sein. Das gesamte "göttliche Objekt" haben wir in SRP umgewandelt.

Skillbox empfiehlt:

Quelle: habr.com

Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server 🔥 Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster