Освоение принципов SOLID на примере систем платежей

В программной разработке при создании сопровождаемых и эффективных систем важно следовать передовым практикам. К ним относятся и пять принципов проектирования SOLID, призванных сделать программную разработку более понятной, гибкой и сопровождаемой. Изучим их сквозь призму системы платежей, проиллюстрировав примерами плохого и хорошего кода Go.

1. Принцип единственной ответственности

Плохой код:

type Invoice struct {
ProductName string
Quantity int
Price float64
}

func (i Invoice) CalculateTotal() float64 {
return i.Price * float64(i.Quantity)
}

func (i Invoice) Print() {
// Выводятся реквизиты
fmt.Printf("Product: %s, Quantity: %d, Total: %.2f\n", i.ProductName, i.Quantity, i.CalculateTotal())
}

func (i Invoice) SaveToDatabase() {
// Счет-фактура сохраняется в базе данных
}

В этом примере класс Invoice занимается вычислением итоговых значений, выводом и сохранением в базе данных, чем нарушается принцип единственной ответственности.

Хороший код:

type Invoice struct {
ProductName string
Quantity int
Price float64
}

func (i Invoice) CalculateTotal() float64 {
return i.Price * float64(i.Quantity)
}

type InvoicePrinter struct {
Invoice Invoice
}

func (p InvoicePrinter) Print() {
fmt.Printf("Product: %s, Quantity: %d, Total: %.2f\n", p.Invoice.ProductName, p.Invoice.Quantity, p.Invoice.CalculateTotal())
}

type InvoiceRepository struct {
// Подключение к базе данных
}

func (r InvoiceRepository) Save(invoice Invoice) {
// Счет-фактура сохраняется в базе данных
}

В доработанной версии задачи выведены в отдельные классы, принцип единственной ответственности соблюдается.

2. Принцип открытости/закрытости

Плохой код:

type Payment struct {
Amount float64
}

func (p Payment) ProcessPayment(method string) {
if method == "credit" {
// Обрабатывается платеж по кредитной карте
} else if method == "paypal" {
// Обрабатывается платеж по PayPal
}
}

В этом примере при добавлении новых способов оплаты требуется поменять метод ProcessPayment, чем нарушается принцип открытости/закрытости.

Хороший код:

type Payment interface {
Process() string
}

type CreditCardPayment struct {
Amount float64
}

func (c CreditCardPayment) Process() string {
return "Processing credit card payment of " + fmt.Sprintf("%.2f", c.Amount)
}

type PayPalPayment struct {
Amount float64
}

func (p PayPalPayment) Process() string {
return "Processing PayPal payment of " + fmt.Sprintf("%.2f", p.Amount)
}

func ProcessPayment(payment Payment) {
fmt.Println(payment.Process())
}

При помощи интерфейсов легко добавляются новые способы оплаты без изменения имеющегося кода, принцип открытости/закрытости соблюдается.

3. Принцип подстановки Лисков

Плохой код:

type PaymentProcessor struct{}

func (p PaymentProcessor) ProcessPayment(payment Payment) {
if payment.Amount < 0 {
panic("Invalid amount")
}
// Платеж обрабатывается
}

Здесь, если производным классом попробовать поменять поведение метода ProcessPayment на прием отрицательных сумм, функциональность нарушится.

Хороший код:

type ValidatedPayment struct {
Payment
}

func (v ValidatedPayment) ProcessPayment() {
if v.Amount < 0 {
panic("Invalid amount")
}
// Платеж обрабатывается
}

В доработанной версии родительский класс без проблем заменяется всеми своими подклассами, принцип подстановки Лисков соблюдается.

4. Принцип разделения интерфейса

Плохой код:

type PaymentProcessor interface {
ProcessPayment()
RefundPayment()
GenerateReport()
}

type CreditCardProcessor struct{}

func (c CreditCardProcessor) ProcessPayment() {}
func (c CreditCardProcessor) RefundPayment() {}
func (c CreditCardProcessor) GenerateReport() {}

Классу CreditCardProcessor приходится реализовывать не применяемые им методы, чем нарушается принцип разделения интерфейса.

Хороший код:

type PaymentProcessor interface {
ProcessPayment()
}

type Refundable interface {
RefundPayment()
}

type Reportable interface {
GenerateReport()
}

type CreditCardProcessor struct{}

func (c CreditCardProcessor) ProcessPayment() {}

С интерфейсами поменьше в реализации включаются только релевантные для них методы, принцип разделения интерфейса соблюдается.

5. Принцип инверсии зависимостей

Плохой код:

type PaymentService struct {
Processor CreditCardProcessor
}

func (s PaymentService) MakePayment(amount float64) {
s.Processor.ProcessPayment(amount)
}

Здесь PaymentService напрямую зависит от конкретного класса, чем нарушается принцип инверсии зависимостей.

Хороший код:

type PaymentProcessor interface {
ProcessPayment(amount float64)
}

type PaymentService struct {
Processor PaymentProcessor
}

func (s PaymentService) MakePayment(amount float64) {
s.Processor.ProcessPayment(amount)
}

В доработанной версии PaymentService зависит не от конкретного класса, а от интерфейса. Как итог  —  больше гибкости, принцип инверсии зависимостей соблюдается.

Заключение

Эффективно применяя принципы SOLID, разработчики создают более понятное, сопровождаемое и расширяемое программное обеспечение.

На примерах систем платежей иллюстрируется, как соблюдением этих принципов повышаются качество кода и надежность системы в целом.

Реализацией этих принципов в проектах совершенствуются проектирование ПО и процесс разработки.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Shantanu Saini: Go SOLID

Предыдущая статьяРеализация тематических фильтров новостей в приложении TrendNow. Часть 3
Следующая статьяДевять вопросов на собеседованиях для разработчиков Android