Одна причина для изменения
Принцип единственной ответственности — это буква S в SOLID, пяти правилах проектирования кода, который остаётся легко изменять. Он гласит: у типа должна быть одна задача — а значит, и только одна причина для изменения.
Когда один тип сохраняет заказы и отправляет письма, он меняется и при изменении хранилища, и при изменении текста письма. Две задачи, две причины и вдвое больше риска сломать одно, пока трогаешь другое.
В Go это правило действует для структур, их методов и пакетов: пусть каждый из них занимается одной задачей.
Слишком много задач в одном типе
Этот обработчик заказов работает — но он сохраняет заказ, составляет текст подтверждения и «отправляет» его, и всё это в одном методе. Проверить отдельно только текст письма невозможно, а чтобы переиспользовать часть с хранилищем в другом месте, придётся тащить за собой и код писем.
Один тип, который делает всё
Process() смешивает две задачи: сохранение и уведомление. Любое изменение в одной из них означает правку — и повторную проверку — этого же самого типа.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type OrderProcessor struct{}
// Process выполняет две задачи: сохранение И уведомление.
func (p OrderProcessor) Process(order string, customer string) {
fmt.Println("сохраняем заказ в хранилище:", order)
fmt.Println("отправляем письмо на", customer+":")
fmt.Println(" Спасибо! Ваш заказ", order, "получен.")
}
func main() {
p := OrderProcessor{}
p.Process("#1042 (2 книги)", "ana@example.com")
}
Одна задача на тип
Решение: по одному типу на задачу. OrderStore только сохраняет, EmailSender только отправляет. Небольшая функция их связывает.
Вывод остаётся прежним — но теперь каждую часть можно изменять, тестировать и переиспользовать отдельно.
Разделение на два типа
То же поведение, но разделённое: хранилище ничего не знает о письмах, отправитель ничего не знает о хранении. Функция processOrder просто соединяет их.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// OrderStore имеет одну задачу: хранение заказов.
type OrderStore struct{}
func (s OrderStore) Save(order string) {
fmt.Println("сохраняем заказ в хранилище:", order)
}
// EmailSender имеет одну задачу: отправку писем.
type EmailSender struct{}
func (e EmailSender) SendConfirmation(customer, order string) {
fmt.Println("отправляем письмо на", customer+":")
fmt.Println(" Спасибо! Ваш заказ", order, "получен.")
}
func processOrder(order, customer string) {
store := OrderStore{}
sender := EmailSender{}
store.Save(order)
sender.SendConfirmation(customer, order)
}
func main() {
processOrder("#1042 (2 книги)", "ana@example.com")
}
Выгода: переиспользование без страха
Поскольку отправитель — самостоятельный тип, он отправляет ещё и уведомление об отгрузке, и код хранилища тут ни при чём. А хранилище архивирует заказ, не составляя ни одного письма. Каждый тип вырос внутри своей единственной задачи.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type OrderStore struct{}
func (s OrderStore) Save(order string) {
fmt.Println("сохраняем заказ в хранилище:", order)
}
func (s OrderStore) Archive(order string) {
fmt.Println("переносим заказ в архив:", order)
}
type EmailSender struct{}
func (e EmailSender) SendConfirmation(customer, order string) {
fmt.Println("письмо на", customer+": заказ", order, "получен")
}
func (e EmailSender) SendShippingNotice(customer, order string) {
fmt.Println("письмо на", customer+": заказ", order, "отправлен")
}
func main() {
store := OrderStore{}
sender := EmailSender{}
// Каждый тип растёт внутри своей задачи.
sender.SendShippingNotice("ana@example.com", "#1042")
store.Archive("#0997")
}