Примените принципы SOLID к проекту простого сервиса бронирования отелей: опишите конкретные нарушения при монолитном классе BookingManager и предложите рефакторинг с объяснением, какие принципы за что отвечают
Коротко: SOLID — это набор принципов проектирования, который помогает сделать код гибким, расширяемым и тестируемым. Ниже покажу — на конкретных примерах — какие нарушения типично встречаются в монолитном классе BookingManager и как его рефакторить, чтобы каждый принцип SOLID был соблюдён.
1) Пример «монолита» — что чаще всего встречается (псевдокод / C#-стиль для ясности):
class BookingManager { // создаёт бронь, проверяет доступность, рассчитывает цену, // проводит платёж, сохраняет в БД, отправляет уведомления, логирует, обрабатывает отмены и т.п. public BookingResult CreateBookingBookingRequestreqBookingRequest reqBookingRequestreq
{ // 1. Валидация входных данных if !Validate(req)!Validate(req)!Validate(req) throw new ValidationException;
// 2. Проверка доступности номеров if !CheckAvailability(req.RoomType,req.Dates)!CheckAvailability(req.RoomType, req.Dates)!CheckAvailability(req.RoomType,req.Dates) return fail; // 3. Расчёт цены с учётом скидок var price = CalculatePricereqreqreq; // 4. Платёж var paymentResult = ProcessPaymentreq.PaymentDetails,pricereq.PaymentDetails, pricereq.PaymentDetails,price; if !paymentResult.Success!paymentResult.Success!paymentResult.Success return fail; // 5. Сохранение записи в БД SaveBookingToDb.........; // 6. Отправка уведомления клиенту SendEmail.........; SendSms.........; // 7. Логирование/метрики Log.........; return success; } // плюс методы CancelBooking, Refund, GetBookings, ImportRatePlan, etc.
}
Какие проблемы здесь:
один класс делает всё: доступность, расчёт цены, оплата, persist, уведомления, валидация, логирование, интеграции — нарушение SRP;чтобы добавить новый способ оплаты или уведомления, придётся менять BookingManager — нарушение OCP;интерфейс/класс, который обязан реализовать много методов, которые конкретному клиенту не нужны — нарушение ISP впримереэтопроявляется,еслимывынесеминтерфейсIBookingManagerскучейметодовв примере это проявляется, если мы вынесем интерфейс IBookingManager с кучей методоввпримереэтопроявляется,еслимывынесеминтерфейсIBookingManagerскучейметодов;если наследовать BookingManager для особой логики и ломать контракт — потенциальное нарушение LSP;BookingManager напрямую использует реализацию БД/платежного шлюза — нарушение DIP зависитотдеталейзависит от деталейзависитотдеталей.
2) Как рефакторить — целевые сущности и интерфейсы Основная идея: разбить ответственность, ввести абстракции интерфейсыинтерфейсыинтерфейсы, внедрять зависимости DIDIDI и расширять поведение через стратегии/плагины/цепочки.
Предлагаемая структура строгоеразделениеролейстрогое разделение ролейстрогоеразделениеролей:
IBookingService — высокий уровень для создания/отмены броней координаторкоординаторкоординатор.IAvailabilityChecker — проверка доступности номеров.IPricingStrategy илиIPricingServiceили IPricingServiceилиIPricingService — расчёт цены поддерживаетплагины:скидки,налогиподдерживает плагины: скидки, налогиподдерживаетплагины:скидки,налоги.IPaymentProcessor — обработка платежей интерфейс,разныереализациидляStripe,PayPal,testинтерфейс, разные реализации для Stripe, PayPal, testинтерфейс,разныереализациидляStripe,PayPal,test.IBookingRepository — абстракция доступа к данным Save,Get,QuerySave, Get, QuerySave,Get,Query.INotificationSender илинесколько:IEmailSender,ISmsSenderили несколько: IEmailSender, ISmsSenderилинесколько:IEmailSender,ISmsSender — отправка уведомлений.IBookingValidator илинаборвалидаторовили набор валидаторовилинаборвалидаторов — валидация запроса.ILogger / IEventPublisher — логирование и публикация доменных событий.
Patterns:
Strategy: для расчёта цены, для оплаты, для уведомлений.Chain of Responsibility / Composite: для набора валидаторов или правил ценообразования.Unit of Work / Transaction + Domain Events: уведомления и интеграции после успешной записи.
public BookingResult CreateBookingBookingRequestreqBookingRequest reqBookingRequestreq
{ var v = validator.Validatereqreqreq; if !v.Ok!v.Ok!v.Ok return fail; if !availability.IsAvailable(req.RoomType,req.Dates)!availability.IsAvailable(req.RoomType, req.Dates)!availability.IsAvailable(req.RoomType,req.Dates) return fail; var price = pricing.Calculatereqreqreq; var paymentResult = payment.Chargereq.Payment,pricereq.Payment, pricereq.Payment,price; if !paymentResult.Success!paymentResult.Success!paymentResult.Success return fail; var booking = new Booking.........; repo.Savebookingbookingbooking; // публикация события/уведомления после успешной транзакции foreach varninnotifiersvar n in notifiersvarninnotifiers n.SendnewBookingCreated(booking)new BookingCreated(booking)newBookingCreated(booking); log.Info.........; return success; }
}
3) Как SOLID принципы применяются и какие проблемы решают
Проблема в монолите: BookingManager отвечает за 8+ разных вещей.Рефакторинг: выделить отдельные классы для каждой ответственности: AvailabilityChecker, PricingService, PaymentProcessor, Repository, NotificationSender, Validator.Результат: проще менять/тестировать/реиспользовать каждую часть.
Проблема в монолите: чтобы добавить новый способ оплаты или новую акцию, надо править BookingManager.Рефакторинг: ввести абстракции IPaymentProcessor,IPricingService,INotificationSenderIPaymentProcessor, IPricingService, INotificationSenderIPaymentProcessor,IPricingService,INotificationSender и регистрировать новые реализации. Добавление новых стратегий не требует изменения BookingService.Результат: расширяемость через новые реализации, минимальные изменения уже работающего кода.
Потенциальная проблема: если создают подкласс BookingManagerSpecial и меняют семантику CreateBooking например,бросаютexceptionsтамгдебазовыйвозвращалкоднапример, бросают exceptions там где базовый возвращал коднапример,бросаютexceptionsтамгдебазовыйвозвращалкод, то клиенты ломаются.Рефакторинг: проектировать абстракции с ясными контрактами чтовозвращается,какиеисключениядопустимычто возвращается, какие исключения допустимычтовозвращается,какиеисключениядопустимы. Проверяем, что любые реализации интерфейса соблюдают контракт: same input -> compatible output/side-effects.Результат: можно безопасно заменять реализации напримертестовыйплатежныйпроцессорвместобоевогонапример тестовый платежный процессор вместо боевогонапримертестовыйплатежныйпроцессорвместобоевого.
Проблема: один «толстый» интерфейс IBookingManager с множеством методов заставляет клиентов реализовывать/зависеть от ненужных методов.Рефакторинг: разбить интерфейсы на маленькие: IBookingService, IBookingRepository, IPaymentProcessor, IAvailabilityChecker, INotificationSender. Клиентам даём только те интерфейсы, которые им нужны.Результат: уменьшение связности и более понятные контрактные границы.
Проблема: BookingManager напрямую создаёт/использует конкретные классы SqlRepository,StripeClient,SmtpClientSqlRepository, StripeClient, SmtpClientSqlRepository,StripeClient,SmtpClient.Рефакторинг: BookingService зависит от абстракций интерфейсовинтерфейсовинтерфейсов. Конкретные реализации внедряются извне через DI-контейнер или фабрики.Результат: проще тестировать мокимокимоки, менять реализации, конфигурировать систему.
4) Небольшие практические рекомендации по рефакторингу инкрементальноинкрементальноинкрементально
Шаг 1: Написать тесты на текущую логику CreateBooking чтобыповедениесохраненочтобы поведение сохраненочтобыповедениесохранено.Шаг 2: Выделить IAvailabilityChecker и перенести логику проверки доступности; внедрить в BookingManager иливременныйфасадили временный фасадиливременныйфасад через конструктор. Запустить тесты.Шаг 3: Выделить IPricingService первоначальноимплементациякопируетстаруюлогикупервоначально имплементация копирует старую логикупервоначальноимплементациякопируетстаруюлогику. Тесты.Шаг 4: Выделить IPaymentProcessor внедрятьтестовыйstub/моквовремятестоввнедрять тестовый stub/мок во время тестоввнедрятьтестовыйstub/моквовремятестов.Шаг 5: Выделить IBookingRepository persistpersistpersist. Ввести транзакцию/UnitOfWork, чтобы уведомления отправлялись только после успешной коммита.Шаг 6: Создать BookingService как координирующий класс high−levelhigh-levelhigh−level, удалить логику из монолитного BookingManager. Монолит можно оставить как адаптер к новому BookingService, чтобы не ломать внешний API, и постепенно удалить.Шаг 7: Вынести уведомления и логирование, подключать через события/observer.
5) Примеры шаблонов/вариантов
Pricing: реализовать как цепочку правил ChainofResponsibilityChain of ResponsibilityChainofResponsibility или набор стратегий, которые комбинируются.Payment: Strategy для разных провайдеров + Factory для выбора по метаданным.Validation: набор IValidator реализует проверку; BookingService просто прогоняет все валидаторы.Events: после сохранения публикуем BookingCreated event, подписчики нотсифай,аналитиканотсифай, аналитиканотсифай,аналитика реагируют.
6) Контроль ошибок и атомарность
Используйте транзакции/UnitOfWork: сначала сохраняем бронь, затем отправляем уведомления только после подтверждённого коммита.Для взаимодействия с внешними сервисами платёж,уведомленияплатёж, уведомленияплатёж,уведомления применяйте стратегию компенсации/ре.try илиsagaдляраспределённыхпроцессовили saga для распределённых процессовилиsagaдляраспределённыхпроцессов.
SRP: выделить Availability, Pricing, Payment, Repo, Notification, Validation.OCP: ввести IPricingService / IPaymentProcessor / INotificationSender — добавляем реализации без изменения BookingService.LSP: чёткие контракты интерфейсов, предусмотреть поведение в пограничных сценариях невозвращатьnull,использоватьResult−объектыне возвращать null, использовать Result-объектыневозвращатьnull,использоватьResult−объекты.ISP: дробить большие интерфейсы на специализированные.DIP: внедрять зависимости через интерфейсы, использовать DI-контейнер.
8) Заключение — что вы получаете
Меньше «больших классов» — легче понимать и тестировать.Локализация изменений: добавить платежный провайдер или акцию — минимальные изменения.Проще писать unit-тесты можномокатьIPaymentProcessor,IBookingRepositoryможно мокать IPaymentProcessor, IBookingRepositoryможномокатьIPaymentProcessor,IBookingRepository.Система становится более гибкой и готовой к масштабированию.
Если хотите, могу:
предложить конкретную структуру файлов/пакетов для проекта;привести полный пример кода (на C# / Java / TypeScript) с интерфейсами и базовыми реализациями;показать, как организовать миграцию существующего монолита пошагово с примерами тестов.
Коротко: SOLID — это набор принципов проектирования, который помогает сделать код гибким, расширяемым и тестируемым. Ниже покажу — на конкретных примерах — какие нарушения типично встречаются в монолитном классе BookingManager и как его рефакторить, чтобы каждый принцип SOLID был соблюдён.
1) Пример «монолита» — что чаще всего встречается
(псевдокод / C#-стиль для ясности):
class BookingManager
// 2. Проверка доступности номеров{
// создаёт бронь, проверяет доступность, рассчитывает цену,
// проводит платёж, сохраняет в БД, отправляет уведомления, логирует, обрабатывает отмены и т.п.
public BookingResult CreateBookingBookingRequestreqBookingRequest reqBookingRequestreq {
// 1. Валидация входных данных
if !Validate(req)!Validate(req)!Validate(req) throw new ValidationException;
if !CheckAvailability(req.RoomType,req.Dates)!CheckAvailability(req.RoomType, req.Dates)!CheckAvailability(req.RoomType,req.Dates) return fail;
// 3. Расчёт цены с учётом скидок
var price = CalculatePricereqreqreq;
// 4. Платёж
var paymentResult = ProcessPaymentreq.PaymentDetails,pricereq.PaymentDetails, pricereq.PaymentDetails,price;
if !paymentResult.Success!paymentResult.Success!paymentResult.Success return fail;
// 5. Сохранение записи в БД
SaveBookingToDb.........;
// 6. Отправка уведомления клиенту
SendEmail.........;
SendSms.........;
// 7. Логирование/метрики
Log.........;
return success;
}
// плюс методы CancelBooking, Refund, GetBookings, ImportRatePlan, etc.
}
Какие проблемы здесь:
один класс делает всё: доступность, расчёт цены, оплата, persist, уведомления, валидация, логирование, интеграции — нарушение SRP;чтобы добавить новый способ оплаты или уведомления, придётся менять BookingManager — нарушение OCP;интерфейс/класс, который обязан реализовать много методов, которые конкретному клиенту не нужны — нарушение ISP впримереэтопроявляется,еслимывынесеминтерфейсIBookingManagerскучейметодовв примере это проявляется, если мы вынесем интерфейс IBookingManager с кучей методоввпримереэтопроявляется,еслимывынесеминтерфейсIBookingManagerскучейметодов;если наследовать BookingManager для особой логики и ломать контракт — потенциальное нарушение LSP;BookingManager напрямую использует реализацию БД/платежного шлюза — нарушение DIP зависитотдеталейзависит от деталейзависитотдеталей.2) Как рефакторить — целевые сущности и интерфейсы
Основная идея: разбить ответственность, ввести абстракции интерфейсыинтерфейсыинтерфейсы, внедрять зависимости DIDIDI и расширять поведение через стратегии/плагины/цепочки.
Предлагаемая структура строгоеразделениеролейстрогое разделение ролейстрогоеразделениеролей:
Domain model:
Booking, Customer, Room, Payment, Price, BookingRequest, BookingResultCore services / интерфейсы:
IBookingService — высокий уровень для создания/отмены броней координаторкоординаторкоординатор.IAvailabilityChecker — проверка доступности номеров.IPricingStrategy илиIPricingServiceили IPricingServiceилиIPricingService — расчёт цены поддерживаетплагины:скидки,налогиподдерживает плагины: скидки, налогиподдерживаетплагины:скидки,налоги.IPaymentProcessor — обработка платежей интерфейс,разныереализациидляStripe,PayPal,testинтерфейс, разные реализации для Stripe, PayPal, testинтерфейс,разныереализациидляStripe,PayPal,test.IBookingRepository — абстракция доступа к данным Save,Get,QuerySave, Get, QuerySave,Get,Query.INotificationSender илинесколько:IEmailSender,ISmsSenderили несколько: IEmailSender, ISmsSenderилинесколько:IEmailSender,ISmsSender — отправка уведомлений.IBookingValidator илинаборвалидаторовили набор валидаторовилинаборвалидаторов — валидация запроса.ILogger / IEventPublisher — логирование и публикация доменных событий.Patterns:
Strategy: для расчёта цены, для оплаты, для уведомлений.Chain of Responsibility / Composite: для набора валидаторов или правил ценообразования.Unit of Work / Transaction + Domain Events: уведомления и интеграции после успешной записи.Пример интерфейсов псевдокодпсевдокодпсевдокод:
interface IAvailabilityChecker { bool IsAvailableRoomType,DateRangeRoomType, DateRangeRoomType,DateRange; }
interface IPricingService { Money CalculateBookingRequestBookingRequestBookingRequest; }
interface IPaymentProcessor { PaymentResult ChargePaymentDetails,MoneyPaymentDetails, MoneyPaymentDetails,Money; }
interface IBookingRepository { void SaveBookingBookingBooking; Booking Getididid; }
interface INotificationSender { void SendBookingNotificationBookingNotificationBookingNotification; }
interface IBookingValidator { ValidationResult ValidateBookingRequestBookingRequestBookingRequest; }
interface IBookingService { BookingResult CreateBookingBookingRequestBookingRequestBookingRequest; }
Теперь IBookingService реализует координацию, не детали:
class BookingService : IBookingService
public BookingResult CreateBookingBookingRequestreqBookingRequest reqBookingRequestreq {{
private readonly IAvailabilityChecker availability;
private readonly IPricingService pricing;
private readonly IPaymentProcessor payment;
private readonly IBookingRepository repo;
private readonly IEnumerable notifiers;
private readonly IBookingValidator validator;
private readonly ILogger log;
var v = validator.Validatereqreqreq;
if !v.Ok!v.Ok!v.Ok return fail;
if !availability.IsAvailable(req.RoomType,req.Dates)!availability.IsAvailable(req.RoomType, req.Dates)!availability.IsAvailable(req.RoomType,req.Dates) return fail;
var price = pricing.Calculatereqreqreq;
var paymentResult = payment.Chargereq.Payment,pricereq.Payment, pricereq.Payment,price;
if !paymentResult.Success!paymentResult.Success!paymentResult.Success return fail;
var booking = new Booking.........;
repo.Savebookingbookingbooking;
// публикация события/уведомления после успешной транзакции
foreach varninnotifiersvar n in notifiersvarninnotifiers n.SendnewBookingCreated(booking)new BookingCreated(booking)newBookingCreated(booking);
log.Info.........;
return success;
}
}
3) Как SOLID принципы применяются и какие проблемы решают
SRP SingleResponsibilityPrincipleSingle Responsibility PrincipleSingleResponsibilityPrinciple
Проблема в монолите: BookingManager отвечает за 8+ разных вещей.Рефакторинг: выделить отдельные классы для каждой ответственности: AvailabilityChecker, PricingService, PaymentProcessor, Repository, NotificationSender, Validator.Результат: проще менять/тестировать/реиспользовать каждую часть.OCP Open/ClosedPrincipleOpen/Closed PrincipleOpen/ClosedPrinciple
Проблема в монолите: чтобы добавить новый способ оплаты или новую акцию, надо править BookingManager.Рефакторинг: ввести абстракции IPaymentProcessor,IPricingService,INotificationSenderIPaymentProcessor, IPricingService, INotificationSenderIPaymentProcessor,IPricingService,INotificationSender и регистрировать новые реализации. Добавление новых стратегий не требует изменения BookingService.Результат: расширяемость через новые реализации, минимальные изменения уже работающего кода.LSP LiskovSubstitutionPrincipleLiskov Substitution PrincipleLiskovSubstitutionPrinciple
Потенциальная проблема: если создают подкласс BookingManagerSpecial и меняют семантику CreateBooking например,бросаютexceptionsтамгдебазовыйвозвращалкоднапример, бросают exceptions там где базовый возвращал коднапример,бросаютexceptionsтамгдебазовыйвозвращалкод, то клиенты ломаются.Рефакторинг: проектировать абстракции с ясными контрактами чтовозвращается,какиеисключениядопустимычто возвращается, какие исключения допустимычтовозвращается,какиеисключениядопустимы. Проверяем, что любые реализации интерфейса соблюдают контракт: same input -> compatible output/side-effects.Результат: можно безопасно заменять реализации напримертестовыйплатежныйпроцессорвместобоевогонапример тестовый платежный процессор вместо боевогонапримертестовыйплатежныйпроцессорвместобоевого.ISP InterfaceSegregationPrincipleInterface Segregation PrincipleInterfaceSegregationPrinciple
Проблема: один «толстый» интерфейс IBookingManager с множеством методов заставляет клиентов реализовывать/зависеть от ненужных методов.Рефакторинг: разбить интерфейсы на маленькие: IBookingService, IBookingRepository, IPaymentProcessor, IAvailabilityChecker, INotificationSender. Клиентам даём только те интерфейсы, которые им нужны.Результат: уменьшение связности и более понятные контрактные границы.DIP DependencyInversionPrincipleDependency Inversion PrincipleDependencyInversionPrinciple
Проблема: BookingManager напрямую создаёт/использует конкретные классы SqlRepository,StripeClient,SmtpClientSqlRepository, StripeClient, SmtpClientSqlRepository,StripeClient,SmtpClient.Рефакторинг: BookingService зависит от абстракций интерфейсовинтерфейсовинтерфейсов. Конкретные реализации внедряются извне через DI-контейнер или фабрики.Результат: проще тестировать мокимокимоки, менять реализации, конфигурировать систему.4) Небольшие практические рекомендации по рефакторингу инкрементальноинкрементальноинкрементально
Шаг 1: Написать тесты на текущую логику CreateBooking чтобыповедениесохраненочтобы поведение сохраненочтобыповедениесохранено.Шаг 2: Выделить IAvailabilityChecker и перенести логику проверки доступности; внедрить в BookingManager иливременныйфасадили временный фасадиливременныйфасад через конструктор. Запустить тесты.Шаг 3: Выделить IPricingService первоначальноимплементациякопируетстаруюлогикупервоначально имплементация копирует старую логикупервоначальноимплементациякопируетстаруюлогику. Тесты.Шаг 4: Выделить IPaymentProcessor внедрятьтестовыйstub/моквовремятестоввнедрять тестовый stub/мок во время тестоввнедрятьтестовыйstub/моквовремятестов.Шаг 5: Выделить IBookingRepository persistpersistpersist. Ввести транзакцию/UnitOfWork, чтобы уведомления отправлялись только после успешной коммита.Шаг 6: Создать BookingService как координирующий класс high−levelhigh-levelhigh−level, удалить логику из монолитного BookingManager. Монолит можно оставить как адаптер к новому BookingService, чтобы не ломать внешний API, и постепенно удалить.Шаг 7: Вынести уведомления и логирование, подключать через события/observer.5) Примеры шаблонов/вариантов
Pricing: реализовать как цепочку правил ChainofResponsibilityChain of ResponsibilityChainofResponsibility или набор стратегий, которые комбинируются.Payment: Strategy для разных провайдеров + Factory для выбора по метаданным.Validation: набор IValidator реализует проверку; BookingService просто прогоняет все валидаторы.Events: после сохранения публикуем BookingCreated event, подписчики нотсифай,аналитиканотсифай, аналитиканотсифай,аналитика реагируют.6) Контроль ошибок и атомарность
Используйте транзакции/UnitOfWork: сначала сохраняем бронь, затем отправляем уведомления только после подтверждённого коммита.Для взаимодействия с внешними сервисами платёж,уведомленияплатёж, уведомленияплатёж,уведомления применяйте стратегию компенсации/ре.try илиsagaдляраспределённыхпроцессовили saga для распределённых процессовилиsagaдляраспределённыхпроцессов.7) Маппинг нарушений → рефакторинг краткократкократко
SRP: выделить Availability, Pricing, Payment, Repo, Notification, Validation.OCP: ввести IPricingService / IPaymentProcessor / INotificationSender — добавляем реализации без изменения BookingService.LSP: чёткие контракты интерфейсов, предусмотреть поведение в пограничных сценариях невозвращатьnull,использоватьResult−объектыне возвращать null, использовать Result-объектыневозвращатьnull,использоватьResult−объекты.ISP: дробить большие интерфейсы на специализированные.DIP: внедрять зависимости через интерфейсы, использовать DI-контейнер.8) Заключение — что вы получаете
Меньше «больших классов» — легче понимать и тестировать.Локализация изменений: добавить платежный провайдер или акцию — минимальные изменения.Проще писать unit-тесты можномокатьIPaymentProcessor,IBookingRepositoryможно мокать IPaymentProcessor, IBookingRepositoryможномокатьIPaymentProcessor,IBookingRepository.Система становится более гибкой и готовой к масштабированию.Если хотите, могу:
предложить конкретную структуру файлов/пакетов для проекта;привести полный пример кода (на C# / Java / TypeScript) с интерфейсами и базовыми реализациями;показать, как организовать миграцию существующего монолита пошагово с примерами тестов.