Γράφουμε ευέλικτο κώδικα χρησιμοποιώντας SOLID

Γράφουμε ευέλικτο κώδικα χρησιμοποιώντας SOLID

Από τον μεταφραστή: δημοσιεύεται για εσάς άρθρο του Severin Perez σχετικά με τη χρήση αρχών SOLID στον προγραμματισμό. Οι πληροφορίες από το άρθρο θα είναι χρήσιμες τόσο για αρχάριους όσο και για έμπειρους προγραμματιστές.

Εάν ασχολείστε με την ανάπτυξη, πιθανότατα έχετε ακούσει για τις αρχές SOLID. Επιτρέπουν στον προγραμματιστή να γράφει καθαρό, καλά δομημένο και εύκολα συντηρήσιμο κώδικα. Αξίζει να σημειωθεί ότι στον προγραμματισμό υπάρχουν διάφορες προσεγγίσεις για το πώς να εκτελέσετε σωστά μια συγκεκριμένη εργασία. Διαφορετικοί ειδικοί έχουν διαφορετικές ιδέες και κατανόηση του «σωστού μονοπατιού»· όλα εξαρτώνται από την εμπειρία του καθενός. Ωστόσο, οι ιδέες που διακηρύσσονται στο SOLID γίνονται αποδεκτές από όλους σχεδόν τους εκπροσώπους της κοινότητας της πληροφορικής. Έγιναν το σημείο εκκίνησης για την εμφάνιση και την ανάπτυξη πολλών καλών πρακτικών διαχείρισης ανάπτυξης.

Ας καταλάβουμε ποιες είναι οι ΣΤΕΡΕΙΣ αρχές και πώς μας βοηθούν.

Το Skillbox προτείνει: Πρακτικό μάθημα "Mobile Developer PRO".

Υπενθύμιση: για όλους τους αναγνώστες του "Habr" - έκπτωση 10 ρούβλια κατά την εγγραφή σε οποιοδήποτε μάθημα Skillbox χρησιμοποιώντας τον κωδικό προσφοράς "Habr".

Τι είναι το SOLID;

Αυτός ο όρος είναι μια συντομογραφία, κάθε γράμμα του όρου είναι η αρχή του ονόματος μιας συγκεκριμένης αρχής:

  • Single Αρχή ευθύνης. Μια ενότητα μπορεί να έχει έναν και μόνο λόγο αλλαγής.
  • Η Oστυλό/Κλειστή αρχή (αρχή ανοιχτού/κλειστού). Οι κλάσεις και άλλα στοιχεία θα πρέπει να είναι ανοιχτά για επέκταση, αλλά κλειστά για τροποποίηση.
  •  Η LΑρχή υποκατάστασης iskov (αρχή αντικατάστασης Liskov). Οι συναρτήσεις που χρησιμοποιούν έναν βασικό τύπο θα πρέπει να μπορούν να χρησιμοποιούν υποτύπους του βασικού τύπου χωρίς να το γνωρίζουν.
  • Η IΑρχή διαχωρισμού διεπαφής  (αρχή διαχωρισμού διεπαφής). Οι οντότητες λογισμικού δεν πρέπει να εξαρτώνται από μεθόδους που δεν χρησιμοποιούν.
  • Η DΑρχή αντιστροφής εξάρτησης (αρχή της αναστροφής εξάρτησης). Οι ενότητες σε υψηλότερα επίπεδα δεν πρέπει να εξαρτώνται από ενότητες σε χαμηλότερα επίπεδα.

Αρχή Ενιαίας Ευθύνης


Η Αρχή Ενιαίας Ευθύνης (SRP) δηλώνει ότι κάθε κλάση ή ενότητα σε ένα πρόγραμμα θα πρέπει να είναι υπεύθυνη μόνο για ένα μέρος της λειτουργικότητας αυτού του προγράμματος. Επιπλέον, στοιχεία αυτής της ευθύνης θα πρέπει να ανατίθενται στη δική τους τάξη, αντί να διασκορπίζονται σε άσχετες τάξεις. Ο προγραμματιστής και επικεφαλής ευαγγελιστής του SRP, Robert S. Martin, περιγράφει την υπευθυνότητα ως τον λόγο της αλλαγής. Αρχικά πρότεινε αυτόν τον όρο ως ένα από τα στοιχεία της δουλειάς του «Αρχές Αντικειμενοστρεφούς Σχεδιασμού». Η ιδέα ενσωματώνει μεγάλο μέρος του μοτίβου συνδεσιμότητας που είχε οριστεί προηγουμένως από τον Tom DeMarco.

Το concept περιελάμβανε επίσης αρκετές έννοιες που διατύπωσε ο David Parnas. Τα δύο βασικά είναι η ενθυλάκωση και η απόκρυψη πληροφοριών. Ο Πάρνας υποστήριξε ότι η διαίρεση ενός συστήματος σε ξεχωριστές ενότητες δεν θα πρέπει να βασίζεται στην ανάλυση μπλοκ διαγραμμάτων ή ροών εκτέλεσης. Οποιαδήποτε από τις ενότητες πρέπει να περιέχει μια συγκεκριμένη λύση που παρέχει ελάχιστες πληροφορίες στους πελάτες.

Παρεμπιπτόντως, ο Martin έδωσε ένα ενδιαφέρον παράδειγμα με ανώτερα στελέχη μιας εταιρείας (COO, CTO, CFO), καθένας από τους οποίους χρησιμοποιεί συγκεκριμένο επιχειρηματικό λογισμικό για διαφορετικούς σκοπούς. Ως αποτέλεσμα, οποιοσδήποτε από αυτούς μπορεί να εφαρμόσει αλλαγές στο λογισμικό χωρίς να επηρεάσει τα συμφέροντα άλλων διαχειριστών.

Θεϊκό αντικείμενο

Όπως πάντα, ο καλύτερος τρόπος για να μάθετε SRP είναι να το δείτε σε δράση. Ας δούμε ένα τμήμα του προγράμματος που ΔΕΝ ακολουθεί την Αρχή της Ενιαίας Ευθύνης. Αυτός είναι ο κώδικας Ruby που περιγράφει τη συμπεριφορά και τα χαρακτηριστικά του διαστημικού σταθμού.

Εξετάστε το παράδειγμα και προσπαθήστε να προσδιορίσετε τα ακόλουθα:
Ευθύνες εκείνων των αντικειμένων που δηλώνονται στην κλάση SpaceStation.
Όσοι μπορεί να ενδιαφέρονται για τη λειτουργία του διαστημικού σταθμού.

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

Στην πραγματικότητα, ο διαστημικός μας σταθμός είναι δυσλειτουργικός (δεν νομίζω ότι θα λάβω κλήση από τη NASA σύντομα), αλλά υπάρχει κάτι να αναλύσουμε εδώ.

Έτσι, η κλάση SpaceStation έχει πολλές διαφορετικές αρμοδιότητες (ή εργασίες). Όλα μπορούν να χωριστούν σε τύπους:

  • Αισθητήρες;
  • προμήθειες (αναλώσιμα)?
  • καύσιμα;
  • επιταχυντές.

Αν και σε κανέναν από τους υπαλλήλους του σταθμού δεν έχει ανατεθεί τάξη, μπορούμε εύκολα να φανταστούμε ποιος είναι υπεύθυνος για τι. Πιθανότατα, ο επιστήμονας ελέγχει τους αισθητήρες, ο επιμελητής είναι υπεύθυνος για την παροχή πόρων, ο μηχανικός είναι υπεύθυνος για τις προμήθειες καυσίμου και ο πιλότος ελέγχει τους ενισχυτές.

Μπορούμε να πούμε ότι αυτό το πρόγραμμα δεν είναι συμβατό με SRP; Ναι σίγουρα. Αλλά η κλάση SpaceStation είναι ένα τυπικό «θεό αντικείμενο» που ξέρει τα πάντα και κάνει τα πάντα. Αυτό είναι ένα σημαντικό αντί-μοτίβο στον αντικειμενοστραφή προγραμματισμό. Για έναν αρχάριο, τέτοια αντικείμενα είναι εξαιρετικά δύσκολο να διατηρηθούν. Μέχρι στιγμής το πρόγραμμα είναι πολύ απλό, ναι, αλλά φανταστείτε τι θα γίνει αν προσθέσουμε νέες δυνατότητες. Ίσως ο διαστημικός μας σταθμός να χρειαστεί έναν ιατρικό σταθμό ή μια αίθουσα συνεδριάσεων. Και όσο περισσότερες λειτουργίες υπάρχουν, τόσο περισσότερο θα μεγαλώνει ο Διαστημικός Σταθμός. Λοιπόν, δεδομένου ότι αυτή η εγκατάσταση θα συνδεθεί με άλλες, η εξυπηρέτηση ολόκληρου του συγκροτήματος θα γίνει ακόμη πιο περίπλοκη. Ως αποτέλεσμα, μπορούμε να διαταράξουμε τη λειτουργία, για παράδειγμα, των επιταχυντών. Εάν ένας ερευνητής ζητήσει αλλαγές στους αισθητήρες, αυτό θα μπορούσε κάλλιστα να επηρεάσει τα συστήματα επικοινωνιών του σταθμού.

Η παραβίαση της αρχής του SRP μπορεί να δώσει μια βραχυπρόθεσμη τακτική νίκη, αλλά στο τέλος θα "χάσουμε τον πόλεμο" και θα γίνει πολύ δύσκολο να διατηρήσουμε ένα τέτοιο τέρας στο μέλλον. Είναι καλύτερο να χωρίσετε το πρόγραμμα σε ξεχωριστές ενότητες κώδικα, καθεμία από τις οποίες είναι υπεύθυνη για την εκτέλεση μιας συγκεκριμένης λειτουργίας. Κατανοώντας αυτό, ας αλλάξουμε την κατηγορία SpaceStation.

Ας μοιράσουμε την ευθύνη

Παραπάνω ορίσαμε τέσσερις τύπους λειτουργιών που ελέγχονται από την κλάση SpaceStation. Θα τα έχουμε υπόψη μας κατά την αναπαράσταση. Ο ενημερωμένος κώδικας ταιριάζει καλύτερα με το 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

Υπάρχουν πολλές αλλαγές, το πρόγραμμα φαίνεται σίγουρα καλύτερο τώρα. Τώρα η κατηγορία μας SpaceStation έχει γίνει περισσότερο ένα κοντέινερ στο οποίο ξεκινούν οι λειτουργίες για εξαρτημένα μέρη, όπως ένα σύνολο αισθητήρων, ένα σύστημα τροφοδοσίας αναλώσιμων, μια δεξαμενή καυσίμου και ενισχυτές.

Για οποιαδήποτε από τις μεταβλητές υπάρχει τώρα μια αντίστοιχη κλάση: Sensors; SupplyHold; Δεξαμενή καυσίμων; Προωστήρες.

Υπάρχουν πολλές σημαντικές αλλαγές σε αυτήν την έκδοση του κώδικα. Το θέμα είναι ότι οι μεμονωμένες συναρτήσεις δεν ενσωματώνονται μόνο στις δικές τους τάξεις, αλλά είναι οργανωμένες με τέτοιο τρόπο ώστε να γίνονται προβλέψιμες και συνεπείς. Ομαδοποιούμε στοιχεία με παρόμοια λειτουργικότητα για να ακολουθήσουμε την αρχή της συνοχής. Τώρα, εάν πρέπει να αλλάξουμε τον τρόπο λειτουργίας του συστήματος, μεταβαίνοντας από μια δομή κατακερματισμού σε έναν πίνακα, απλώς χρησιμοποιήστε την κλάση SupplyHold· δεν χρειάζεται να αγγίξουμε άλλες μονάδες. Έτσι, αν ο υπεύθυνος επιμελητείας αλλάξει κάτι στο τμήμα του, ο υπόλοιπος σταθμός θα παραμείνει άθικτος. Σε αυτήν την περίπτωση, η κλάση SpaceStation δεν θα γνωρίζει καν τις αλλαγές.

Οι αξιωματικοί μας που εργάζονται στον διαστημικό σταθμό είναι πιθανώς ευχαριστημένοι με τις αλλαγές επειδή μπορούν να ζητήσουν αυτές που χρειάζονται. Σημειώστε ότι ο κώδικας έχει μεθόδους όπως report_supplies και report_fuel που περιέχονται στις κλάσεις SupplyHold και FuelTank. Τι θα συνέβαινε αν η Γη ζητούσε να αλλάξει τον τρόπο με τον οποίο αναφέρει; Και οι δύο κατηγορίες, SupplyHold και FuelTank, θα πρέπει να αλλάξουν. Τι γίνεται αν χρειαστεί να αλλάξετε τον τρόπο παράδοσης των καυσίμων και των αναλωσίμων; Μάλλον θα χρειαστεί να αλλάξετε ξανά όλες τις ίδιες τάξεις. Και αυτό αποτελεί ήδη παραβίαση της αρχής του SRP. Ας το διορθώσουμε αυτό.

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.

Σε αυτήν την τελευταία έκδοση του προγράμματος, οι αρμοδιότητες έχουν χωριστεί σε δύο νέες κατηγορίες, FuelReporter και SupplyReporter. Είναι και οι δύο παιδιά της τάξης του Ρεπόρτερ. Επιπλέον, προσθέσαμε μεταβλητές στιγμιότυπου στην κλάση SpaceStation έτσι ώστε η επιθυμητή υποκλάση να μπορεί να αρχικοποιηθεί εάν είναι απαραίτητο. Τώρα, εάν η Γη αποφασίσει να αλλάξει κάτι άλλο, τότε θα κάνουμε αλλαγές στις υποκατηγορίες, και όχι στην κύρια τάξη.

Φυσικά, μερικές από τις τάξεις μας εξακολουθούν να εξαρτώνται η μία από την άλλη. Έτσι, το αντικείμενο SupplyReporter εξαρτάται από το SupplyHold και το FuelReporter από το FuelTank. Φυσικά, οι ενισχυτές πρέπει να συνδέονται με τη δεξαμενή καυσίμου. Αλλά εδώ όλα φαίνονται ήδη λογικά και η πραγματοποίηση αλλαγών δεν θα είναι ιδιαίτερα δύσκολη - η επεξεργασία του κώδικα ενός αντικειμένου δεν θα επηρεάσει πολύ ένα άλλο.

Έτσι, δημιουργήσαμε έναν αρθρωτό κώδικα όπου οι ευθύνες καθενός από τα αντικείμενα/τάξεις καθορίζονται επακριβώς. Η εργασία με τέτοιο κώδικα δεν είναι πρόβλημα, η διατήρησή του θα είναι μια απλή εργασία. Έχουμε μετατρέψει ολόκληρο το «θείο αντικείμενο» σε SRP.

Το Skillbox προτείνει:

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο