Schreiben eines flexiblen Codes mit SOLID



Von einem Übersetzer: Severin Peres hat für Sie einen Artikel über die Verwendung von SOLID-Prinzipien in der Programmierung veröffentlicht. Die Informationen aus dem Artikel sind sowohl für Anfänger als auch für Programmierer mit Erfahrung nützlich.

Wenn Sie Entwickler sind, haben Sie wahrscheinlich von den SOLID-Prinzipien gehört. Sie ermöglichen es dem Programmierer, sauberen, gut strukturierten und einfach zu wartenden Code zu schreiben. Es ist erwähnenswert, dass es bei der Programmierung verschiedene Ansätze gibt, wie diese oder jene Arbeit richtig ausgeführt werden kann. Unterschiedliche Spezialisten haben unterschiedliche Vorstellungen und Verständnis für den „richtigen Weg“. Alles hängt von der Erfahrung jedes Einzelnen ab. Dennoch werden die in SOLID proklamierten Ideen von fast allen Vertretern der IT-Community akzeptiert. Sie wurden zum Ausgangspunkt für die Entstehung und Entwicklung vieler guter Entwicklungsmanagementpraktiken.

Mal sehen, was SOLID-Prinzipien sind und wie sie uns helfen.

Skillbox empfiehlt: Praktikum "Mobile Developer PRO" .

Wir erinnern Sie daran: Für alle Leser von „Habr“ - ein Rabatt von 10.000 Rubel bei der Anmeldung für einen Skillbox-Kurs mit dem Promo-Code „Habr“.

Was ist FEST?


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

Grundsatz der alleinigen Verantwortung


Das Prinzip der Einzelverantwortung (Single Responsibility, 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 ihrer Klasse zugeordnet und nicht auf nicht verwandte Klassen verteilt werden. Der Entwickler und leitende Evangelist von SRP, Robert S. Martin, beschreibt Verantwortung als Ursache für Veränderungen. Zunächst schlug er diesen Begriff als eines der Elemente seiner Arbeit „Prinzipien des objektorientierten Designs“ vor. Das Konzept umfasste einen Großteil des zuvor von Tom Demarco definierten Konnektivitätsmusters.

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

Übrigens gab Martin ein interessantes Beispiel mit Führungskräften des Unternehmens (COO, CTO, CFO), die jeweils spezifische Software für Unternehmen mit einem anderen Zweck verwenden. Infolgedessen kann jeder von ihnen Änderungen in der Software implementieren, ohne die Interessen anderer Manager zu beeinträchtigen.

Göttliches Objekt


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

Schauen Sie sich das Beispiel an und versuchen Sie Folgendes festzustellen:
Die Verantwortlichkeiten der Objekte, die in der SpaceStation-Klasse 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 "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 denke, dass ich in naher Zukunft keinen Anruf von der NASA erhalten werde), aber es gibt etwas zu analysieren.

Die SpaceStation-Klasse hat also verschiedene Verantwortlichkeiten (oder Aufgaben). Alle von ihnen können in Typen unterteilt werden:
  • Sensoren
  • Versorgung (Verbrauchsmaterialien);
  • Kraftstoff;
  • Beschleuniger.

Trotz der Tatsache, dass keiner der Stationsmitarbeiter in der Klasse definiert ist, können wir uns leicht vorstellen, wer für was 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 Kraftstoffversorgung verantwortlich und der Pilot steuert die Beschleuniger.

Können wir sagen, dass dieses Programm nicht SRP-konform ist? Ja natürlich. Aber die SpaceStation-Klasse ist ein typisches „göttliches Objekt“, das alles weiß und alles tut. Dies ist das Haupt-Anti-Pattern in der objektorientierten Programmierung. Für einen Anfänger sind solche Objekte äußerst schwer zu pflegen. Bisher ist das Programm sehr einfach, ja, aber stellen Sie sich vor, was passieren wird, wenn wir neue Funktionen hinzufügen. Vielleicht braucht unsere Raumstation ein medizinisches Zentrum oder einen Besprechungsraum. Und je mehr Funktionen es gibt, desto mehr SpaceStation wird wachsen. Nun, da dieses Objekt mit anderen verbunden wird, wird die Wartung des gesamten Komplexes noch komplizierter. Infolgedessen können wir beispielsweise die Arbeit von Beschleunigern stören. Wenn ein Forscher nach Änderungen bei der Arbeit mit Sensoren fragt, kann dies durchaus die Kommunikationssysteme der Station beeinträchtigen.

Ein Verstoß gegen das SRP-Prinzip kann kurzfristig zu einem taktischen Sieg führen, aber am Ende werden wir „den Krieg verlieren“, und es wird sehr schwierig sein, ein solches Monster in Zukunft zu bedienen. Es ist am besten, das Programm in separate Codeabschnitte zu unterteilen, von denen jeder für die Ausführung einer bestimmten Operation verantwortlich ist. In diesem Sinne ändern wir die SpaceStation-Klasse.

Verantwortung teilen

Oben haben wir vier Arten von Vorgängen identifiziert, die von der SpaceStation-Klasse gesteuert werden. Beim Refactoring werden wir sie berücksichtigen. Der aktualisierte Code passt besser zu 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 ein Container geworden, in dem Operationen für abhängige Teile eingeleitet werden, einschließlich einer Reihe von Sensoren, eines Versorgungssystems für Verbrauchsmaterialien, eines Kraftstofftanks und Boostern.

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

Es gibt einige wichtige Änderungen an dieser Version des Codes. Tatsache ist, dass einzelne Funktionen nicht nur in ihren eigenen Klassen eingekapselt sind, sondern auch so organisiert sind, dass sie vorhersehbar und konsistent werden. Wir gruppieren Elemente mit ähnlicher Funktionalität, um dem Prinzip der Konnektivität zu folgen. Wenn wir nun das Prinzip des Systems ändern müssen, indem wir von einer Hash-Struktur zu einem Array wechseln, verwenden Sie einfach die SupplyHold-Klasse. Wir müssen keine anderen Module berühren. Wenn also der für die Logistik zuständige Beamte etwas in seiner Abteilung ändert, bleiben die verbleibenden Elemente der Station unberührt. Gleichzeitig wird die SpaceStation-Klasse die Änderungen nicht einmal bemerken.

Unsere Raumstationsoffiziere sind wahrscheinlich mit den Änderungen zufrieden, da sie diejenigen 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 passiert, wenn die Erde die Art und Weise ändern möchte, wie Berichte erstellt werden? Sie müssen beide Klassen, SupplyHold und FuelTank, ändern. Aber was ist, wenn Sie die Art und Weise ändern müssen, wie Sie Kraftstoff und Verbrauchsmaterialien liefern? Möglicherweise müssen Sie dieselben Klassen erneut ändern. Und dies ist ein Verstoß gegen das SRP-Prinzip. Lass es uns reparieren.

 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 Reporterklasse. Darüber hinaus haben wir der SpaceStation-Klasse Instanzvariablen hinzugefügt, um bei Bedarf die erforderliche Unterklasse zu initialisieren. Wenn die Erde beschließt, etwas anderes zu ändern, werden wir Änderungen an Unterklassen und nicht an der Hauptklasse vornehmen.

Natürlich hängen einige Klassen hier immer noch voneinander ab. Das SupplyReporter-Objekt hängt also vom SupplyHold ab, und der FuelReporter hängt vom FuelTank ab. Natürlich sollten Booster an den Kraftstofftank angeschlossen werden. Aber hier sieht alles logisch aus, und Änderungen werden nicht besonders schwierig sein - das Bearbeiten des Codes eines Objekts wirkt sich nicht zu stark auf das andere aus.

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

Skillbox empfiehlt:

Source: https://habr.com/ru/post/de442928/


All Articles