Впровадження принципів SOLID у додатку на Rails

При розробці додатка на Rails легко допустити, щоб код став заплутаним і важким для підтримки. Принципи SOLID — це набір з п'яти керівних принципів об'єктно-орієнтованого проектування, які допомагають зберігати код чистим, легким для підтримки та масштабування. Давайте розглянемо їх на практиці з прикладами в Rails.

pic

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