
Освоение принципов 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, разработчики создают более понятное, сопровождаемое и расширяемое программное обеспечение.
На примерах систем платежей иллюстрируется, как соблюдением этих принципов повышаются качество кода и надежность системы в целом.
Реализацией этих принципов в проектах совершенствуются проектирование ПО и процесс разработки.
Читайте также:
- Принцип открытости/закрытости: расширение кода без модификации
- Конкурентность и синхронизация на Go: горутины, мьютексы и WaitGroup
- Принципы SOLID в инженерии данных. Часть 3
Читайте нас в Telegram, VK и Дзен
Перевод статьи Shantanu Saini: Go SOLID





