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