При розробці додатка на Rails легко допустити, щоб код став заплутаним і важким для підтримки. Принципи SOLID — це набір з п'яти керівних принципів об'єктно-орієнтованого проектування, які допомагають зберігати код чистим, легким для підтримки та масштабування. Давайте розглянемо їх на практиці з прикладами в Rails.
1. Принцип єдиного обов'язку (SRP)
Клас повинен мати лише одну причину для зміни.
Проблема:
Уявіть, що у вас є модель User
, яка не лише обробляє взаємодію з базою даних, але й надсилає електронні листи та керує аутентифікацією. Це робить клас громіздким і важким для підтримки.
Рішення:
Виділімо відповідальність в окремі класи:
class UserMailer < ApplicationMailer
def welcome_email(user)
@user = user
mail(to: @user.email, subject: 'Welcome!')
end
end
class UserAuthenticator
def self.authenticate(email, password)
user = User.find_by(email: email)
user&.authenticate(password)
end
end
Тепер User
займається лише збереженням даних, в той час як UserMailer
обробляє електронні листи, а UserAuthenticator
— аутентифікацією.
2. Принцип відкритості/закритості (OCP)
Клас повинен бути відкритим для розширення, але закритим для модифікації.
Проблема:
У вас є клас PaymentProcessor
, і кожного разу, коли ви додаєте новий метод оплати, вам потрібно змінювати його.
Рішення:
Використовуйте поліморфізм, щоб дозволити додавати нові методи оплати без зміни існуючого коду:
class PaymentMethod
def process(amount)
raise NotImplementedError, "Subclasses must implement the process method"
end
end
class CreditCardPayment < PaymentMethod
def process(amount)
puts "Processing credit card payment of $#{amount}"
end
end
class PayPalPayment < PaymentMethod
def process(amount)
puts "Processing PayPal payment of $#{amount}"
end
end
class PaymentProcessor
def self.process(payment_method, amount)
raise ArgumentError, "Invalid payment method" unless payment_method.is_a?(PaymentMethod)
payment_method.process(amount)
end
end
credit_card = CreditCardPayment.new
paypal = PayPalPayment.new
PaymentProcessor.process(credit_card, 100) # ✅ Works
PaymentProcessor.process(paypal, 200) # ✅ Works
Аналіз реалізації
Базовий клас (PaymentMethod
):
- Клас
PaymentMethod
служить абстрактним базовим класом з методомprocess
. Він викликаєNotImplementedError
, змушуючи кожен підклас реалізувати цей метод.
Підкласи (CreditCardPayment
і PayPalPayment
):
- Обидва класи
CreditCardPayment
таPayPalPayment
є конкретними реалізаціями класуPaymentMethod
. Кожен підклас реалізує методprocess
, забезпечуючи конкретну поведінку для обробки платежів.
Клас PaymentProcessor:
- Клас
PaymentProcessor
відповідає за обробку платежів. Він перевіряє, чи є переданий метод оплати дійсним екземпляром класуPaymentMethod
, і потім викликає відповідний методprocess
. - Такий дизайн дозволяє додавати нові методи оплати (наприклад,
BitcoinPayment
,ApplePayPayment
тощо), не змінюючи існуючий код у класіPaymentProcessor
.
Ви просто створюєте новий підклас класуPaymentMethod
і реалізуєте методprocess
.
Висновок
Реалізація правильно слідує Принципу відкритості/закритості (Open/Closed Principle), тому що:
- Розширюваність: Ви можете легко додавати нові методи оплати, створюючи нові підкласи без зміни існуючих класів.
- Інкапсуляція: Логіка кожного методу оплати інкапсульована в його відповідному класі, що дозволяє зберігати код чистим і підтримуваним.
Приклад розширення функціональності
Якщо ви хочете додати новий метод оплати, наприклад, банківський переказ, ви можете зробити це ось так:
class BankTransferPayment < PaymentMethod
def process(amount)
puts "Processing bank transfer payment of $#{amount}"
end
end
Тепер ви можете обробляти банківські перекази, не змінюючи жодного коду в класах PaymentProcessor
або інших класах методів оплати.
3. Принцип підстановки Ліскова (LSP)
Підтипи повинні бути взаємозамінними без порушення функціональності.
Проблема:
У вас є суперклас Bird
з методом fly
, але не всі птахи можуть літати.
Рішення:
Розділімо на окремі класи:
class Bird
def make_sound
raise NotImplementedError
end
end
module Flying
def fly
puts 'I can fly!'
end
end
class FlyingBird < Bird
include Flying
end
class Penguin < Bird
def swim
puts 'I swim instead of flying!'
end
end
Тепер у класу Penguin
немає методу fly
, який він не може використовувати, що зберігає принцип Ліскова.
4. Принцип сегрегації інтерфейсів (ISP)
Клас не повинен бути змушений реалізовувати методи, які він не використовує.
Проблема:
Клас ReportGenerator
вимагає методи для генерації як PDF, так і CSV, але не всі звіти потребують обох форматів.
Рішення:
Розділімо відповідальності на окремі модулі:
module PdfExportable
def export_to_pdf
puts 'Exporting to PDF'
end
end
module CsvExportable
def export_to_csv
puts 'Exporting to CSV'
end
end
class PdfReport
include PdfExportable
end
class CsvReport
include CsvExportable
end
Тепер кожен клас включає тільки ту функціональність, яка йому потрібна.
Жоден клас не змушений реалізовувати методи, які йому не потрібні. PdfReport
не мусить реалізовувати експорт у CSV, а CsvReport
— експорт у PDF.
5. Принцип інверсії залежностей (DIP)
Залежати від абстракцій, а не від конкретних реалізацій.
Проблема:
NotificationService
безпосередньо ініціалізує EmailNotifier
, що ускладнює перехід на SMS-сповіщення.
Рішення:
Використовуємо ін'єкцію залежностей:
class NotificationService
def initialize(notifier)
@notifier = notifier
end
def send_notification(message)
@notifier.notify(message)
end
end
class EmailNotifier
def notify(message)
puts "Sending Email: #{message}"
end
end
class SmsNotifier
def notify(message)
puts "Sending SMS: #{message}"
end
end
Тепер NotificationService
може працювати з будь-яким сповіщувачем без змін:
service = NotificationService.new(SmsNotifier.new)
service.send_notification('Hello!')
Аналіз реалізації
Високорівневий модуль:
- Клас
NotificationService
— це високорівневий модуль, який відповідає за надсилання сповіщень. Він не знає про конкретні деталі того, як сповіщення відправляються (через електронну пошту, SMS тощо).
Низькорівневі модулі:
- Класи
EmailNotifier
іSmsNotifier
— це низькорівневі модулі, які реалізують фактичну логіку сповіщення.
Ін'єкція залежностей:
- Клас
NotificationService
залежить від абстракції (notifier
), а не від конкретної реалізації.
Це досягається через ін'єкцію залежностей, де екземпляр сповіщувача (абоEmailNotifier
, абоSmsNotifier
) передається вNotificationService
під час ініціалізації.
Гнучкість та розширюваність:
- Ви можете легко додавати нові типи сповіщувачів (наприклад,
PushNotifier
,SlackNotifier
тощо), не змінюючи класNotificationService
. Вам потрібно лише створити новий клас сповіщувача, який реалізує методnotify
.
Висновок
Застосовуючи принципи SOLID у вашому Rails додатку, ви створюєте більш масштабований, підтримуваний та тестований код. Почніть з малого — рефакторіть громіздкий клас, використовуйте ін'єкцію залежностей або вводьте інтерфейси. З часом ці принципи стануть для вас природними, що призведе до чистіших і більш надійних застосунків.
Успіхів у програмуванні! 🚀
Перекладено з: Implementing SOLID Principles in a Rails Application