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

60GB SSD 8Gb DDR4