نكتب كودًا مرنًا باستخدام SOLID

نكتب كودًا مرنًا باستخدام SOLID

من المترجم: نشرت لك مقال بقلم سيفيرين بيريز حول استخدام مبادئ SOLID في البرمجة. ستكون المعلومات الواردة في المقالة مفيدة لكل من المبرمجين المبتدئين وذوي الخبرة.

إذا كنت مهتمًا بالتنمية، فمن المرجح أنك سمعت عن مبادئ SOLID. إنها تمكن المبرمج من كتابة تعليمات برمجية نظيفة وجيدة التنظيم ويمكن صيانتها بسهولة. تجدر الإشارة إلى أنه يوجد في البرمجة عدة طرق لكيفية أداء مهمة معينة بشكل صحيح. لدى المتخصصين المختلفين أفكار وفهم مختلف لـ "الطريق الصحيح"، وكل هذا يعتمد على تجربة كل شخص. ومع ذلك، فإن الأفكار المعلنة في SOLID مقبولة من قبل جميع ممثلي مجتمع تكنولوجيا المعلومات تقريبًا. لقد أصبحت نقطة البداية لظهور وتطوير العديد من ممارسات إدارة التطوير الجيدة.

دعونا نفهم ما هي مبادئ SOLID وكيف تساعدنا.

يوصي Skillbox بما يلي: دورة عملية "Mobile Developer PRO".

نذكر: لجميع قراء "Habr" - خصم 10 روبل عند التسجيل في أي دورة Skillbox باستخدام رمز "Habr" الترويجي.

ما هو الصلبة؟

هذا المصطلح هو اختصار، كل حرف من هذا المصطلح هو بداية لاسم مبدأ محدد:

  • Sمبدأ المسؤولية الوحيد. يمكن أن يكون للوحدة سبب واحد فقط للتغيير.
  • Oالقلم/مبدأ مغلق (مبدأ مفتوح / مغلق). يجب أن تكون الفئات والعناصر الأخرى مفتوحة للتوسيع، ولكنها مغلقة للتعديل.
  •  • Lمبدأ استبدال إيسكوف (مبدأ استبدال ليسكوف). يجب أن تكون الوظائف التي تستخدم النوع الأساسي قادرة على استخدام الأنواع الفرعية من النوع الأساسي دون معرفتها.
  • Iمبدأ فصل الواجهة  (مبدأ فصل الواجهة). لا ينبغي للكيانات البرمجية أن تعتمد على أساليب لا تستخدمها.
  • Dمبدأ انعكاس التبعية (مبدأ انعكاس التبعية). لا ينبغي أن تعتمد الوحدات في المستويات الأعلى على الوحدات في المستويات الأدنى.

مبدأ المسؤولية الفردية


ينص مبدأ المسؤولية الفردية (SRP) على أن كل فئة أو وحدة في البرنامج يجب أن تكون مسؤولة عن جزء واحد فقط من وظائف هذا البرنامج. بالإضافة إلى ذلك، يجب تعيين عناصر هذه المسؤولية إلى فئتها الخاصة، بدلاً من توزيعها عبر فئات غير مرتبطة. ويصف مطور برنامج SRP وكبير المبشرين، روبرت س. مارتن، المساءلة بأنها سبب التغيير. لقد اقترح هذا المصطلح في الأصل كأحد عناصر عمله "مبادئ التصميم الموجه للكائنات". يتضمن المفهوم الكثير من نمط الاتصال الذي تم تحديده مسبقًا بواسطة Tom DeMarco.

كما تضمن المفهوم عدة مفاهيم صاغها ديفيد بارناس. العاملان الرئيسيان هما التغليف وإخفاء المعلومات. جادل بارناس بأن تقسيم النظام إلى وحدات منفصلة لا ينبغي أن يعتمد على تحليل المخططات الكتلية أو تدفقات التنفيذ. يجب أن تحتوي أي وحدة من الوحدات على حل محدد يوفر الحد الأدنى من المعلومات للعملاء.

بالمناسبة، أعطى مارتن مثالًا مثيرًا للاهتمام مع كبار المديرين في الشركة (COO، CTO، CFO)، حيث يستخدم كل منهم برامج أعمال محددة لأغراض مختلفة. ونتيجة لذلك، يمكن لأي منهم تنفيذ التغييرات في البرنامج دون التأثير على مصالح المديرين الآخرين.

كائن إلهي

كما هو الحال دائمًا، فإن أفضل طريقة لتعلم SRP هي رؤيتها أثناء العمل. دعونا نلقي نظرة على قسم من البرنامج لا يتبع مبدأ المسؤولية الفردية. هذا هو رمز روبي الذي يصف سلوك وسمات المحطة الفضائية.

راجع المثال وحاول تحديد ما يلي:
مسؤوليات تلك الكائنات التي تم الإعلان عنها في فئة 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

في الواقع، محطتنا الفضائية معطلة (لا أعتقد أنني سأتلقى مكالمة من وكالة ناسا في أي وقت قريب)، ولكن هناك شيء يجب تحليله هنا.

وبالتالي، فإن فئة SpaceStation لديها عدة مسؤوليات (أو مهام) مختلفة. يمكن تقسيمها جميعًا إلى أنواع:

  • أجهزة الاستشعار.
  • الإمدادات (المواد الاستهلاكية) ؛
  • وقود؛
  • المسرعات.

على الرغم من عدم تعيين أي فصل دراسي لأي من موظفي المحطة، يمكننا بسهولة أن نتخيل من المسؤول عن ماذا. على الأرجح، يتحكم العالم في أجهزة الاستشعار، والخبير اللوجستي مسؤول عن توفير الموارد، والمهندس مسؤول عن إمدادات الوقود، ويتحكم الطيار في المعززات.

هل يمكننا القول أن هذا البرنامج غير متوافق مع SRP؟ نعم بالتأكيد. لكن فئة SpaceStation هي "كائن إلهي" نموذجي يعرف كل شيء ويفعل كل شيء. يعد هذا نمطًا رئيسيًا مضادًا في البرمجة الشيئية. بالنسبة للمبتدئين، فإن صيانة هذه الأشياء صعبة للغاية. حتى الآن البرنامج بسيط جدًا، نعم، لكن تخيل ماذا سيحدث إذا أضفنا ميزات جديدة. ربما تحتاج محطتنا الفضائية إلى محطة طبية أو غرفة اجتماعات. وكلما زاد عدد الوظائف، زاد نمو 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 الخاصة بنا أقرب إلى حاوية يتم فيها بدء العمليات للأجزاء التابعة، بما في ذلك مجموعة من أجهزة الاستشعار، ونظام إمداد المواد الاستهلاكية، وخزان الوقود، والمعززات.

يوجد الآن فئة مقابلة لأي من المتغيرات: أجهزة الاستشعار؛ 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

إضافة تعليق