نوشتن کد انعطاف پذیر با استفاده از SOLID

نوشتن کد انعطاف پذیر با استفاده از SOLID

از مترجم: برای شما منتشر شد مقاله ای از سویرین پرز در مورد استفاده از اصول SOLID در برنامه نویسی اطلاعات این مقاله هم برای مبتدیان و هم برای برنامه نویسان با تجربه مفید خواهد بود.

اگر اهل توسعه هستید، به احتمال زیاد اصول SOLID را شنیده اید. آنها برنامه نویس را قادر می سازند تا کدهای تمیز، ساختار یافته و به راحتی قابل نگهداری بنویسد. شایان ذکر است که در برنامه نویسی چندین رویکرد برای نحوه صحیح انجام یک کار خاص وجود دارد. متخصصان مختلف ایده ها و درک متفاوتی از "مسیر درست" دارند؛ همه اینها به تجربه هر فرد بستگی دارد. با این حال، ایده های اعلام شده در SOLID تقریباً توسط همه نمایندگان جامعه فناوری اطلاعات پذیرفته شده است. آنها نقطه شروعی برای ظهور و توسعه بسیاری از شیوه های مدیریت توسعه خوب شدند.

بیایید درک کنیم که اصول SOLID چیست و چگونه به ما کمک می کند.

Skillbox توصیه می کند: دوره عملی "موبایل توسعه دهنده PRO".

یادآوری می کنیم: برای همه خوانندگان "Habr" - تخفیف 10 روبل هنگام ثبت نام در هر دوره Skillbox با استفاده از کد تبلیغاتی "Habr".

SOLID چیست؟

این اصطلاح مخفف است، هر حرف از اصطلاح ابتدای نام یک اصل خاص است:

  • Sاصل مسئولیت پذیری یک ماژول می تواند یک و تنها یک دلیل برای تغییر داشته باشد.
  • La Oقلم / اصل بسته (اصل باز/بسته). کلاس ها و سایر عناصر باید برای گسترش باز باشند، اما برای اصلاح بسته باشند.
  •  La Lاصل جایگزینی iskov (اصل جایگزینی لیسکوف). توابعی که از یک نوع پایه استفاده می کنند باید بتوانند از زیرگروه های نوع پایه بدون اینکه بدانند استفاده کنند.
  • La Iاصل جداسازی رابط  (اصل جداسازی رابط). نهادهای نرم افزاری نباید به روش هایی وابسته باشند که از آنها استفاده نمی کنند.
  • La Dاصل وارونگی وابستگی (اصل وارونگی وابستگی). ماژول‌های سطوح بالاتر نباید به ماژول‌های سطوح پایین‌تر وابسته باشند.

اصل مسئولیت واحد


اصل مسئولیت واحد (SRP) بیان می کند که هر کلاس یا ماژول در یک برنامه باید تنها مسئول یک قسمت از عملکرد آن برنامه باشد. علاوه بر این، عناصر این مسئولیت به جای پراکنده شدن در کلاس‌های غیرمرتبط، باید به کلاس خودشان اختصاص داده شوند. رابرت اس. مارتین، توسعه‌دهنده و مبشر ارشد SRP، مسئولیت‌پذیری را دلیل تغییر توصیف می‌کند. او در ابتدا این اصطلاح را به عنوان یکی از عناصر کار خود "اصول طراحی شی گرا" مطرح کرد. این مفهوم شامل بسیاری از الگوی اتصال است که قبلا توسط تام دیمارکو تعریف شده بود.

این مفهوم همچنین شامل چندین مفهوم بود که توسط دیوید پارناس فرموله شده بود. دو مورد اصلی عبارتند از کپسوله کردن و پنهان کردن اطلاعات. پارناس استدلال کرد که تقسیم یک سیستم به ماژول های جداگانه نباید بر اساس تجزیه و تحلیل بلوک دیاگرام ها یا جریان های اجرا باشد. هر یک از ماژول ها باید حاوی راه حل خاصی باشد که حداقل اطلاعات را در اختیار مشتریان قرار دهد.

به هر حال، مارتین مثال جالبی با مدیران ارشد یک شرکت (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 یک "شیء خدا" معمولی است که همه چیز را می داند و همه چیز را انجام می دهد. این یک ضد الگوی اصلی در برنامه نویسی شی گرا است. برای یک مبتدی، نگهداری چنین اشیایی بسیار دشوار است. تا اینجا برنامه بسیار ساده است، بله، اما تصور کنید اگر ویژگی های جدید اضافه کنیم چه اتفاقی می افتد. شاید ایستگاه فضایی ما به یک ایستگاه پزشکی یا یک اتاق جلسه نیاز داشته باشد. و هر چه توابع بیشتر باشد، ایستگاه فضایی بیشتر رشد خواهد کرد. خوب، از آنجایی که این تسهیلات به سایرین متصل خواهد شد، خدمات رسانی به کل مجموعه دشوارتر خواهد شد. در نتیجه می توانیم عملکرد مثلاً شتاب دهنده ها را مختل کنیم. اگر محققی درخواست تغییراتی در حسگرها کند، این امر به خوبی می تواند بر سیستم های ارتباطی ایستگاه تاثیر بگذارد.

نقض اصل 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

تغییرات زیادی وجود دارد، برنامه قطعاً اکنون بهتر به نظر می رسد. اکنون کلاس ایستگاه فضایی ما بیشتر به محفظه‌ای تبدیل شده است که در آن عملیات برای قطعات وابسته، از جمله مجموعه‌ای از حسگرها، سیستم تامین مواد مصرفی، مخزن سوخت و تقویت‌کننده‌ها آغاز می‌شود.

اکنون برای هر یک از متغیرها یک کلاس مربوطه وجود دارد: 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

اضافه کردن نظر