
От преводача: публикувано за вас за използването на принципите SOLID в програмирането. Информацията от статията ще бъде полезна както за начинаещи, така и за опитни програмисти.
Ако се занимавате с разработка, вероятно сте чували за принципите SOLID. Те дават възможност на програмистите да пишат чист, добре структуриран и лесно поддържан код. Струва си да се отбележи, че в програмирането има различни подходи към това как правилно да се извършва всяка работа. Различните специалисти имат различни идеи и разбирания за "правилния път", всичко зависи от опита на всеки. Въпреки това, идеите, провъзгласени в SOLID, се приемат почти от всички представители на ИТ обществото. Те станаха отправна точка за създаването и развитието на много добри методи за управление на разработките.
Нека разгледаме какво представляват принципите SOLID и как те ни помагат.
Skillbox препоръчва: Практически курс .
Напомняме: за всички читатели на "Хабра" — отстъпка от 10 000 рубли при записване на всякакъв курс на Skillbox с промокод "Хабр".
Какво е SOLID?
Термът е абревиатура, всяка буква от термина е началото на името на определен принцип:
- SSingle Responsibility 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
