Опишите принципы SOLID на примере учебного проекта (например, система управления библиотекой): приведите фрагмент кода, нарушающий как минимум два принципа, и опишите шаги рефакторинга для приведения кода в соответствие с этими принципами
Ниже — краткое объяснение принципов SOLID на примере учебного проекта «система управления библиотекой», пример кода, сознательно нарушающий несколько принципов, и последовательность рефакторинга с примером исправленного кода.
Коротко о SOLID однопредложениенапринциподно предложение на принциподнопредложениенапринцип
S SingleResponsibilitySingle ResponsibilitySingleResponsibility — класс должен иметь одну причину для изменения однуответственностьодну ответственностьоднуответственность.O Open/ClosedOpen/ClosedOpen/Closed — сущности открыты для расширения, но закрыты для модификации.L LiskovSubstitutionLiskov SubstitutionLiskovSubstitution — объекты наследников должны быть взаимозаменяемы с объектами базового типа.I InterfaceSegregationInterface SegregationInterfaceSegregation — не заставлять клиентов зависеть от методов, которые они не используют толстыеинтерфейсыразделятьтолстые интерфейсы разделятьтолстыеинтерфейсыразделять.D DependencyInversionDependency InversionDependencyInversion — высокоуровневые модули не должны зависеть от низкоуровневых; оба — от абстракций.
Проблема проекта «библиотека» в учебном примере: часто одна сущность делает всё — хранит книги, выдает, сохраняет на диск, печатает отчёты. Это ведёт к нарушению SRP, DIP, OCP и иногда LSP/ISP.
1) Пример «плохого» кода нарушаеткакминимумSRPиDIP,плюсLSPнарушает как минимум SRP и DIP, плюс LSPнарушаеткакминимумSRPиDIP,плюсLSP: (псевдо-C#; упрощённо)
class Book { public string Id; public string Title; public virtual void BorrowUseruserUser userUseruser
{ // логика выдачи книги } }
class ReferenceBook : Book { // нарушает LSP — наследник бросает исключение вместо корректного поведения public override void BorrowUseruserUser userUseruser
{ throw new InvalidOperationException("Reference books cannot be borrowed"); } }
class LibraryManager { private List books = new List;
public void AddBookBookbBook bBookb => books.Addbbb; public void BorrowBookstringid,Useruserstring id, User userstringid,Useruser
{ var book = books.FirstOrDefault(b => b.Id == id); if book==nullbook == nullbook==null throw new Exception"Notfound""Not found""Notfound"; book.Borrowuseruseruser; // бизнес-логика // сохранение низкоуровневаязависимостьотфайловойсистемынизкоуровневая зависимость от файловой системынизкоуровневаязависимостьотфайловойсистемы
File.WriteAllText"books.json",JsonConvert.SerializeObject(books)"books.json", JsonConvert.SerializeObject(books)"books.json",JsonConvert.SerializeObject(books); // вывод отчёта прямойвызовконсолипрямой вызов консолипрямойвызовконсоли
Console.WriteLine"Report:totalbooks="+books.Count"Report: total books = " + books.Count"Report:totalbooks="+books.Count; }
}
Почему это плохо
LibraryManager отвечает за бизнес-логику, за хранение и за презентацию — нарушен SRP.LibraryManager жёстко зависит от File и Console — нарушен DIP иусложненотестированиеи усложнено тестированиеиусложненотестирование.ReferenceBook переопределяет Borrow, бросая исключение — нарушает LSP: код, работающий с Book, при подстановке ReferenceBook ломается.
2) Цель рефакторинга
Разделить ответственности SRPSRPSRP.Ввести абстракции интерфейсыинтерфейсыинтерфейсы и внедрение зависимостей DIPDIPDIP.Убрать наследование, которое ломает LSP, или пересмотреть модель так, чтобы поведение не нарушало заменяемость.Разделить интерфейсы на узкие ISPISPISP.Обеспечить возможность расширения OCPOCPOCP.
3) Шаги рефакторинга пошаговопошаговопошагово и пример исправленного кода
Шаг 1 — выделить репозиторий для хранения книг SRP,DIPSRP, DIPSRP,DIP
Сделать интерфейс IBookRepository и реализацию FileBookRepository.
class ConsoleReportGenerator : IReportGenerator { public void GenerateLibraryReportIEnumerable<Book>booksIEnumerable<Book> booksIEnumerable<Book>books
{ Console.WriteLine("Total books: " + books.Count()); } }
Шаг 3 — убрать ломающее LSP поведение для ReferenceBook ввестимелкиеинтерфейсыввести мелкие интерфейсыввестимелкиеинтерфейсыLSP,ISPLSP, ISPLSP,ISP
Вместо того, чтобы заставлять все Book поддерживать Borrow, вводим интерфейс IBorrowable — только книги, которые можно выдавать, его реализуют.
class BorrowableBook : Book, IBorrowable { public bool IsAvailable { get; private set; } = true; public void BorrowUseruserUser userUseruser
{ if !IsAvailable!IsAvailable!IsAvailable throw new Exception("Not available"); // отметить как выданную IsAvailable = false; // запись истории и пр. } }
class ReferenceBook : Book { // не реализует IBorrowable — нельзя использовать в LendingService как borrowable }
Шаг 4 — выделить сервис для выдачи книг SRP,DIP,OCPSRP, DIP, OCPSRP,DIP,OCP
LendingService зависит от IBookRepository и IReportGenerator черезабстракциичерез абстракциичерезабстракции.
public LendingServiceIBookRepositoryrepo,IReportGeneratorreportIBookRepository repo, IReportGenerator reportIBookRepositoryrepo,IReportGeneratorreport
{ this.repo = repo; this.report = report; } public void BorrowBookstringid,Useruserstring id, User userstringid,Useruser
{ var book = repo.GetByIdididid; if book==nullbook == nullbook==null throw new Exception"Notfound""Not found""Notfound"; // проверяем, можно ли выдавать if bookisIBorrowableborrowablebook is IBorrowable borrowablebookisIBorrowableborrowable
{ borrowable.Borrowuseruseruser; repo.SaveChanges; report.GenerateLibraryReportrepo.GetAll()repo.GetAll()repo.GetAll(); } else { throw new InvalidOperationException"Thisbookcannotbeborrowed""This book cannot be borrowed""Thisbookcannotbeborrowed"; } }
}
Результат
SRP: LibraryManager больше не выполняет сохранение и печать — это делают специализированные классы.DIP: LendingService зависит от абстракций IBookRepository,IReportGeneratorIBookRepository, IReportGeneratorIBookRepository,IReportGenerator, а не от конкретных File/Console.OCP: Добавление нового формата отчёта например,HtmlReportGeneratorнапример, HtmlReportGeneratorнапример,HtmlReportGenerator не требует изменения LendingService.LSP: ReferenceBook больше не ломает поведение класса Book, т.к. модель разделена — код, ожидающий borrowable, работает только с IBorrowable.ISP: IBorrowable небольшой и точечный — клиенты реализуют только нужные методы.
4) Дополнительные рекомендации и переходный план
Внедрить конструкторное/контейнерное DI втестахподменятьреализациирепозиториевигенераторовв тестах подменять реализации репозиториев и генератороввтестахподменятьреализациирепозиториевигенераторов.Добавить покрытие юнит-тестами: тестирование LendingService с mock-репозиторием и mock-репортером.Постепенно вынести остальные обязанности регистрацияпользователей,штрафы,резервированиерегистрация пользователей, штрафы, резервированиерегистрацияпользователей,штрафы,резервирование в отдельные сервисы.Проверять паттерны: если появляются разные типы книг с разным поведением — предпочесть композицию поле/сервисповеденияполе/сервис поведенияполе/сервисповедения вместо «широкого» наследования.
5) Краткое сравнение «до» и «после»
До: один класс делает всё — сложно тестировать, сложно добавлять фичи, ломается при подстановке наследников.После: классы маленькие и понятные, зависимость через интерфейсы, легко подменять реализации, расширять систему без модификации существующих классов.
Если хотите, могу:
преобразовать примеры в конкретный рабочий пример (полный код C# проекта);показать, как писать тесты для LendingService с использованием заглушек/моков;показать вариант с IoC-контейнером например,Microsoft.Extensions.DependencyInjectionнапример, Microsoft.Extensions.DependencyInjectionнапример,Microsoft.Extensions.DependencyInjection.
Ниже — краткое объяснение принципов SOLID на примере учебного проекта «система управления библиотекой», пример кода, сознательно нарушающий несколько принципов, и последовательность рефакторинга с примером исправленного кода.
Коротко о SOLID однопредложениенапринциподно предложение на принциподнопредложениенапринцип
S SingleResponsibilitySingle ResponsibilitySingleResponsibility — класс должен иметь одну причину для изменения однуответственностьодну ответственностьоднуответственность.O Open/ClosedOpen/ClosedOpen/Closed — сущности открыты для расширения, но закрыты для модификации.L LiskovSubstitutionLiskov SubstitutionLiskovSubstitution — объекты наследников должны быть взаимозаменяемы с объектами базового типа.I InterfaceSegregationInterface SegregationInterfaceSegregation — не заставлять клиентов зависеть от методов, которые они не используют толстыеинтерфейсыразделятьтолстые интерфейсы разделятьтолстыеинтерфейсыразделять.D DependencyInversionDependency InversionDependencyInversion — высокоуровневые модули не должны зависеть от низкоуровневых; оба — от абстракций.Проблема проекта «библиотека» в учебном примере: часто одна сущность делает всё — хранит книги, выдает, сохраняет на диск, печатает отчёты. Это ведёт к нарушению SRP, DIP, OCP и иногда LSP/ISP.
1) Пример «плохого» кода нарушаеткакминимумSRPиDIP,плюсLSPнарушает как минимум SRP и DIP, плюс LSPнарушаеткакминимумSRPиDIP,плюсLSP:
(псевдо-C#; упрощённо)
class Book
{
public string Id;
public string Title;
public virtual void BorrowUseruserUser userUseruser {
// логика выдачи книги
}
}
class ReferenceBook : Book
{
// нарушает LSP — наследник бросает исключение вместо корректного поведения
public override void BorrowUseruserUser userUseruser {
throw new InvalidOperationException("Reference books cannot be borrowed");
}
}
class LibraryManager
public void AddBookBookbBook bBookb => books.Addbbb;{
private List books = new List;
public void BorrowBookstringid,Useruserstring id, User userstringid,Useruser {
var book = books.FirstOrDefault(b => b.Id == id);
if book==nullbook == nullbook==null throw new Exception"Notfound""Not found""Notfound";
book.Borrowuseruseruser; // бизнес-логика
// сохранение низкоуровневаязависимостьотфайловойсистемынизкоуровневая зависимость от файловой системынизкоуровневаязависимостьотфайловойсистемы File.WriteAllText"books.json",JsonConvert.SerializeObject(books)"books.json", JsonConvert.SerializeObject(books)"books.json",JsonConvert.SerializeObject(books);
// вывод отчёта прямойвызовконсолипрямой вызов консолипрямойвызовконсоли Console.WriteLine"Report:totalbooks="+books.Count"Report: total books = " + books.Count"Report:totalbooks="+books.Count;
}
}
Почему это плохо
LibraryManager отвечает за бизнес-логику, за хранение и за презентацию — нарушен SRP.LibraryManager жёстко зависит от File и Console — нарушен DIP иусложненотестированиеи усложнено тестированиеиусложненотестирование.ReferenceBook переопределяет Borrow, бросая исключение — нарушает LSP: код, работающий с Book, при подстановке ReferenceBook ломается.2) Цель рефакторинга
Разделить ответственности SRPSRPSRP.Ввести абстракции интерфейсыинтерфейсыинтерфейсы и внедрение зависимостей DIPDIPDIP.Убрать наследование, которое ломает LSP, или пересмотреть модель так, чтобы поведение не нарушало заменяемость.Разделить интерфейсы на узкие ISPISPISP.Обеспечить возможность расширения OCPOCPOCP.3) Шаги рефакторинга пошаговопошаговопошагово и пример исправленного кода
Шаг 1 — выделить репозиторий для хранения книг SRP,DIPSRP, DIPSRP,DIP
Сделать интерфейс IBookRepository и реализацию FileBookRepository.interface IBookRepository
{
void AddBookbookBook bookBookbook;
Book GetByIdstringidstring idstringid;
IEnumerable GetAll;
void SaveChanges;
}
class FileBookRepository : IBookRepository
public void AddBookbookBook bookBookbook { books.Addbookbookbook; }{
private readonly string path = "books.json";
private List books = LoadFromFile;
public Book GetByIdstringidstring idstringid => books.FirstOrDefault(b => b.Id == id);
public IEnumerable<Book> GetAll => books;
public void SaveChanges => File.WriteAllTextpath,JsonConvert.SerializeObject(books)path, JsonConvert.SerializeObject(books)path,JsonConvert.SerializeObject(books);
// LoadFromFile — чтение/десериализация
}
Шаг 2 — отделить генерацию отчётов/презентацию SRP,DIP,OCPSRP, DIP, OCPSRP,DIP,OCP
Ввести IReportGenerator; для консоли — ConsoleReportGenerator; позже можно добавлять HtmlReportGenerator без изменения существующего кода.interface IReportGenerator
{
void GenerateLibraryReportIEnumerable<Book>booksIEnumerable<Book> booksIEnumerable<Book>books;
}
class ConsoleReportGenerator : IReportGenerator
{
public void GenerateLibraryReportIEnumerable<Book>booksIEnumerable<Book> booksIEnumerable<Book>books {
Console.WriteLine("Total books: " + books.Count());
}
}
Шаг 3 — убрать ломающее LSP поведение для ReferenceBook ввестимелкиеинтерфейсыввести мелкие интерфейсыввестимелкиеинтерфейсы LSP,ISPLSP, ISPLSP,ISP Вместо того, чтобы заставлять все Book поддерживать Borrow, вводим интерфейс IBorrowable — только книги, которые можно выдавать, его реализуют.
interface IBorrowable
{
void BorrowUseruserUser userUseruser;
bool IsAvailable { get; }
}
class BorrowableBook : Book, IBorrowable
{
public bool IsAvailable { get; private set; } = true;
public void BorrowUseruserUser userUseruser {
if !IsAvailable!IsAvailable!IsAvailable throw new Exception("Not available");
// отметить как выданную
IsAvailable = false;
// запись истории и пр.
}
}
class ReferenceBook : Book
{
// не реализует IBorrowable — нельзя использовать в LendingService как borrowable
}
Шаг 4 — выделить сервис для выдачи книг SRP,DIP,OCPSRP, DIP, OCPSRP,DIP,OCP
LendingService зависит от IBookRepository и IReportGenerator черезабстракциичерез абстракциичерезабстракции.class LendingService
public LendingServiceIBookRepositoryrepo,IReportGeneratorreportIBookRepository repo, IReportGenerator reportIBookRepositoryrepo,IReportGeneratorreport {{
private readonly IBookRepository repo;
private readonly IReportGenerator report;
this.repo = repo;
this.report = report;
}
public void BorrowBookstringid,Useruserstring id, User userstringid,Useruser {
var book = repo.GetByIdididid;
if book==nullbook == nullbook==null throw new Exception"Notfound""Not found""Notfound";
// проверяем, можно ли выдавать
if bookisIBorrowableborrowablebook is IBorrowable borrowablebookisIBorrowableborrowable {
borrowable.Borrowuseruseruser;
repo.SaveChanges;
report.GenerateLibraryReportrepo.GetAll()repo.GetAll()repo.GetAll();
}
else
{
throw new InvalidOperationException"Thisbookcannotbeborrowed""This book cannot be borrowed""Thisbookcannotbeborrowed";
}
}
}
Результат
SRP: LibraryManager больше не выполняет сохранение и печать — это делают специализированные классы.DIP: LendingService зависит от абстракций IBookRepository,IReportGeneratorIBookRepository, IReportGeneratorIBookRepository,IReportGenerator, а не от конкретных File/Console.OCP: Добавление нового формата отчёта например,HtmlReportGeneratorнапример, HtmlReportGeneratorнапример,HtmlReportGenerator не требует изменения LendingService.LSP: ReferenceBook больше не ломает поведение класса Book, т.к. модель разделена — код, ожидающий borrowable, работает только с IBorrowable.ISP: IBorrowable небольшой и точечный — клиенты реализуют только нужные методы.4) Дополнительные рекомендации и переходный план
Внедрить конструкторное/контейнерное DI втестахподменятьреализациирепозиториевигенераторовв тестах подменять реализации репозиториев и генератороввтестахподменятьреализациирепозиториевигенераторов.Добавить покрытие юнит-тестами: тестирование LendingService с mock-репозиторием и mock-репортером.Постепенно вынести остальные обязанности регистрацияпользователей,штрафы,резервированиерегистрация пользователей, штрафы, резервированиерегистрацияпользователей,штрафы,резервирование в отдельные сервисы.Проверять паттерны: если появляются разные типы книг с разным поведением — предпочесть композицию поле/сервисповеденияполе/сервис поведенияполе/сервисповедения вместо «широкого» наследования.5) Краткое сравнение «до» и «после»
До: один класс делает всё — сложно тестировать, сложно добавлять фичи, ломается при подстановке наследников.После: классы маленькие и понятные, зависимость через интерфейсы, легко подменять реализации, расширять систему без модификации существующих классов.Если хотите, могу:
преобразовать примеры в конкретный рабочий пример (полный код C# проекта);показать, как писать тесты для LendingService с использованием заглушек/моков;показать вариант с IoC-контейнером например,Microsoft.Extensions.DependencyInjectionнапример, Microsoft.Extensions.DependencyInjectionнапример,Microsoft.Extensions.DependencyInjection.