Пишем гъвкав код, използвайки SOLID

Пишем гъвкав код, използвайки SOLID

От преводача: публикувано за вас статия на Северин Перес за използването на принципите SOLID в програмирането. Информацията от статията ще бъде полезна както за начинаещи, така и за опитни програмисти.

Ако се занимавате с разработка, вероятно сте чували за принципите SOLID. Те дават възможност на програмистите да пишат чист, добре структуриран и лесно поддържан код. Струва си да се отбележи, че в програмирането има различни подходи към това как правилно да се извършва всяка работа. Различните специалисти имат различни идеи и разбирания за "правилния път", всичко зависи от опита на всеки. Въпреки това, идеите, провъзгласени в SOLID, се приемат почти от всички представители на ИТ обществото. Те станаха отправна точка за създаването и развитието на много добри методи за управление на разработките.

Нека разгледаме какво представляват принципите SOLID и как те ни помагат.

Skillbox препоръчва: Практически курс «Мобилен разработчик PRO».

Напомняме: за всички читатели на "Хабра" — отстъпка от 10 000 рубли при записване на всякакъв курс на Skillbox с промокод "Хабр".

Какво е SOLID?

Термът е абревиатура, всяка буква от термина е началото на името на определен принцип:

  • SSingle Responsibility Principle (принцип на единствената отговорност). Модулът може да има само една причина за промяна.
  • The OOpen/Closed Principle (принцип на отвореност/затвореност). Класовете и другите елементи трябва да са отворени за разширение, но затворени за модификация.
  •  The LLiskov Substitution Principle (принцип на подмяна на Лисков). Функциите, които използват базов тип, трябва да могат да използват подтипове на базовия тип, без да знаят за това.
  • The IInterface Segregation Principle (принцип на разделяне на интерфейса). Програмните единици не трябва да зависят от методи, които не използват.
  • The DDependency Inversion Principle (принцип на инверсия на зависимостите). Модулите от по-високите нива не трябва да зависят от модулите от по-ниските нива.

Принцип на единствената отговорност

 
Принцип на единствена отговорност (SRP) гласи, че всеки клас или модул в програмата трябва да носи отговорност само за една част от функционалността на тази програма. Освен това, елементите на тази отговорност трябва да са свързани с техния клас, а не разпределени между несвързани класове. Разработчикът и главен евангелист на SRP, Робърт С. Мартин, описва отговорността като причина за промени. Той първоначално предлага този термин като един от елементите на своята работа „Принципи на обектно-ориентирано проектиране“. В концепцията е включено много от закономерността на свързаността, която по-рано е определена от Том Демарко.

В концепцията са включени и няколко понятия, формулирани от Дейвид Парнас. Два основни — инкапсулация и скриване на информация. Парнас твърди, че разделянето на системата на отделни модули не трябва да се базира на анализ на блок-схеми или потоци на изпълнение. Всеки модул трябва да съдържа определено решение, което предоставя минимално количество информация на клиентите.

Мартин дава интересен пример с висшите мениджъри на компанията (COO, CTO, CFO), всеки от които използва специфичен софтуер за бизнеса с различна цел. В крайна сметка, всеки от тях може да внедрява промени в софтуера, без да засяга интересите на другите мениджъри.

Божествен обект

Както обикновено, най-добрият начин да изучите SRP е да видите всичко в действие. Нека разгледаме участък от програма, който НЕ отговаря на принципа на единствена отговорност. Това е Ruby код, описващ поведението и атрибутите на космическа станция.

Прегледайте примера и се опитайте да определите следното:
Отговорностите на обектите, които са обявени в класа SpaceStation.
Тези, които могат да се интересуват от работата на космическата станция.

class SpaceStation
  def initialize
    @supplies = {}
    @fuel = 0
  end
 
  def run_sensors
    puts "----- Действие сенсоров -----"
    puts "Запуск сенсоров!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Действие по загрузке запасов -----"
    puts "Загрузка #{quantity} единиц #{type} в хранилище запасов."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Действие по использованию запасов -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Использование #{quantity} единиц #{type} из хранилища запасов."
      @supplies[type] -= quantity
    else
      puts "Ошибка запаса: недостающее количество #{type} в хранилище запасов."
    end
  end
 
  def report_supplies
    puts "----- Отчет о запасах -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} доступно: #{quantity} единиц"
      end
    else
      puts "Хранилище запасов пусто."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Действие по загрузке топлива -----"
    puts "Загрузка #{quantity} единиц топлива в бак."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Отчет о топливе -----"
    puts "#{@fuel} единиц топлива доступно."
  end
 
  def activate_thrusters
    puts "----- Действие по активации ускорителей -----"
    if @fuel >= 10
      puts "Действие по ускорению успешно."
      @fuel -= 10
    else
      puts "Ошибка ускорителя: недостаточно топлива."
    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} available: #{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; FuelTank; Thrusters.

В тази версия на кода има няколко важни изменения. Функциите не само са инкапсулирани в собствени класове, те са организирани по начин, който ги прави предсказуеми и последователни. Групираме подобни по функционалност елементи, следвайки принципа на свързаността. Сега, ако трябва да променим начина на работа на системата, преминавайки от хеш структура на масив, просто ще използваме класа SupplyHold, без да е необходимо да засягаме другите модули. По този начин, ако служителят, отговорен за логистиката, направи промяна в своята секция, останалите елементи на станцията ще останат незасегнати. Класът SpaceStation дори няма да е в течение на промените.

Нашите служители на космическата станция вероятно са доволни от промените, тъй като могат да заявяват точно това, от което се нуждаят. Обърнете внимание, че в кода има методи като report_supplies и report_fuel, съдържащи се в класовете SupplyHold и FuelTank. Какво ще се случи, ако Земята поиска да промени начина на генериране на отчетите? Ще трябва да променим и двата класа - SupplyHold и FuelTank. А какво, ако трябва да променим начина на доставка на гориво и консумативи? Вероятно отново ще се наложи да променим същите класи. А това вече е нарушение на принципа SRP. Нека да го решим.

class КосмическаСтанция
  attr_reader :сензори, :снабдителен_отдел, :снабдителен_докладчик,
              :резервоар_с гориво, :докладчик_с_гориво, :други_двигатели
 
  def initialize
    @сензори = Сензори.new
    @снабдителен_отдел = СнабдителенОтдел.new
    @снабдителен_докладчик = СнабдителенДокладчик.new(@снабдителен_отдел)
    @резервоар_с_гориво = РезервоарСГориво.new
    @докладчик_с_гориво = ДокладчикСГориво.new(@резервоар_с_гориво)
    @други_двигатели = ДругиДвигатели.new(@резервоар_с_гориво)
  end
end
 
class Сензори
  def run_sensors
    puts "----- Действие на сензорите -----"
    puts "Сензорите работят!"
  end
end
 
class СнабдителенОтдел
  attr_accessor :снабдява
  attr_reader :докладчик
 
  def initialize
    @снабдява = {}
  end
 
  def get_supplies
    @снабдява
  end
 
  def load_supplies(type, quantity)
    puts "----- Действие на снабдяването -----"
    puts "Зареждане на #{quantity} единици от #{type} в снабдителния отдел."
    
    if @снабдява[type]
      @снабдява[type] += quantity
    else
      @снабдява[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Действие на снабдяването -----"
    if @снабдява[type] != nil && @снабдява[type] > quantity
      puts "Използване на #{quantity} от #{type} от снабдителния отдел."
      @снабдява[type] -= quantity
    else
      puts "Грешка в снабдяването: Недостатъчно #{type} в снабдителния отдел."
    end
  end
end
 
class РезервоарСГориво
  attr_accessor :гориво
  attr_reader :докладчик
 
  def initialize
    @гориво = 0
  end
 
  def get_fuel_levels
    @гориво
  end
 
  def load_fuel(quantity)
    puts "----- Действие на горивото -----"
    puts "Зареждане на #{quantity} единици гориво в резервоара."
    @гориво += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Действие на горивото -----"
    puts "Използване на #{quantity} единици гориво от резервоара."
    @гориво -= quantity
  end
end
 
class ДругиДвигатели
  FUEL_PER_THRUST = 10
 
  def initialize(fuel_tank)
    @свързан_резервоар_с_гориво = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Действие на двигателите -----"
    
    if @свързан_резервоар_с_гориво.get_fuel_levels >= FUEL_PER_THRUST
      puts "Действието на тласкането е успешно."
      @свързан_резервоар_с_гориво.use_fuel(FUEL_PER_THRUST)
    else
      puts "Грешка на двигателя: Недостатъчно гориво."
    end
  end
end
 
class Докладчик
  def initialize(item, type)
    @свързан_елемент = item
    @type = type
  end
 
  def report
    puts "----- #{@type.capitalize} Доклад -----"
  end
end
 
class ДокладчикСГориво < Докладчик
  def initialize(item)
    super(item, "гориво")
  end
 
  def report
    super
    puts "#{@свързан_елемент.get_fuel_levels} единици гориво налични."
  end
end
 
class СнабдителенДокладчик  0
      @свързан_елемент.get_supplies.each do |type, quantity|
        puts "#{type} налично: #{quantity} единици"
      end
    else
      puts "Снабдителният отдел е празен."
    end
  end
end
 
iss = КосмическаСтанция.new
 
iss.сензори.run_sensors
  # ----- Действие на сензорите -----
  # Сензорите работят!
 
iss.снабдителен_отдел.use_supplies("части", 2)
  # ----- Действие на снабдяването -----
  # Грешка в снабдяването: Недостатъчно части в снабдителния отдел.
iss.снабдителен_отдел.load_supplies("части", 10)
  # ----- Действие на снабдяването -----
  # Зареждане на 10 единици части в снабдителния отдел.
iss.снабдителен_отдел.use_supplies("части", 2)
  # ----- Действие на снабдяването -----
  # Използване на 2 от части от снабдителния отдел.
iss.снабдителен_докладчик.report
  # ----- Доклад за снабдяването -----
  # части налични: 8 единици
 
iss.други_двигатели.activate_thrusters
  # ----- Действие на двигателите -----
  # Грешка на двигателя: Недостатъчно гориво.
iss.резервоар_с_гориво.load_fuel(100)
  # ----- Действие на горивото -----
  # Зареждане на 100 единици гориво в резервоара.
iss.други_двигатели.activate_thrusters
  # ----- Действие на двигателите -----
  # Действието на тласкането е успешно.
  # ----- Действие на горивото -----
  # Използване на 10 единици гориво от резервоара.
iss.докладчик_с_гориво.report
  # ----- Доклад за горивото -----
# 90 единици гориво налични.

В тази, последна версия на програмата, отговорностите са разделени на два нови класа, FuelReporter и SupplyReporter. И двата са наследници на класа Reporter. Освен това добавихме променливи на инстанцията към класа SpaceStation, така че при необходимост да инициализираме нужния подклас. Сега, ако Земята реши да промени нещо друго, ще направим корекции в подкласовете, а не в основния клас.

Разбира се, някои класове все още зависят един от друг. Например, обектът SupplyReporter зависи от SupplyHold, а FuelReporter зависи от FuelTank. Само по себе си, ускорителите трябва да бъдат свързани с бензиновия резервоар. Но тук всичко изглежда логично, а промяната няма да бъде особено сложна — редактирането на кода на един обект няма да повлияе значително на друг.

Така ние създадохме модулен код, където отговорностите на всеки от обектите/класовете са точно определени. Работата с такъв код не е проблем, а обслужването му ще бъде лесна задача. Всяка "божествена" функция бяхме трансформирали в SRP.

Skillbox препоръчва:

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster