В этом уроке мы поговорим о первом принципе из пяти, который называется «Принцип единственной ответственности». Также его называют «Принцип единственной обязанности». В английском варианте он называется «Single Responsibility Principle».
Этот принцип звучит так - у класса должна быть только одна причина для изменения.
Как можно понимать это правило? Что значит только одна причина для изменения? Чтобы определить причину для изменения класса, для начала нужно определить, какие у класса обязанности, то есть какие функции этот класс выполняет.
Давайте посмотрим на класс Service. Это публичный статический класс, который выполняет следующие функции: вернуть изображение, сохранить изображение, отправить email-сообщение, сгенерировать новый XML-файл sitemap.xml. То есть это какой-то служебный класс в каком-нибудь приложении. Такие классы создаются довольно часто.
namespace SingleResponsibilityPrinciple
{
//служебный класс
//нарушение принципа Single Responsibility Principle
public static class Service
{
//вернуть изображение
public static Image GetImage(string path)
{
throw new NotImplementedException();
}
//сохранить изображение
public static void SaveImage(Image img)
{
//
}
//отправить email
public static void SendEmailMessage(string address, string subject, string text)
{
//
}
//сгенерировать новый файл sitemap.xml
public static void GenerateSiteMapFile(string[] values)
{
//
}
}
}
Давайте попробуем определить, сколько у этого класса обязанностей. Мы видим, что этот класс умеет работать с изображениями, умеет работать с email и умеет работать с XML-файлами. Конечно же, список методов в этом классе немного упрощен. Вполне может быть, что здесь будут и другие методы, например, не только создавать xml-файл, но также методы для изменения этого xml-файла. То же самое и с email и с изображениями.
Итак, получается, что у этого класса целых три обязанности. Он должен работать с изображениями, должен работать с почтой и должен работать с файлом sitemap.xml.
Каждая из обязанностей класса – это потенциальная причина для изменения этого класса. В классе Service три обязанности, и, соответственно, три причины для изменения этого класса.
Чем это может быть опасно? Такой класс со множеством обязанностей будет очень часто изменяться в процессе жизни приложения. В больших сложных приложениях требования меняются очень часто. Например, заказчик потребует ввести новый функционал. Как результат, класс Service будет меняться чаще других, потому что он взял на себя очень много обязанностей. Например, нам нужно изменить работу с изображениями – обращаемся к классу Service, нам нужно изменить работу с email-сообщениями – обращаемся к классу Service. И так далее. Чем больше у класса обязанностей, тем больше вероятность, что класс будет изменяться. А это очень плохо.
В дизайне нашего приложения появляются такие признаки, как жесткость и хрупкость, вспомните материал прошлого урока. Соответственно, возрастает вероятность появления ошибок в самых неожиданных местах при изменениях кода. Более того, такой код становится труднее изменять. Также затрудняется тестирование этого класса, и тестирования кода, в котором используется этот класс. Также вполне может быть ситуация, что этот класс используется не только внутри одного приложения, но и многими другими приложениями внутри компании. Любое изменение в этом классе Service ставит под вопрос безопасность его использования как в текущем, так и в других приложениях, которые его используют.
В общем, последствия могут быть самыми тяжелыми.
Нам нужно спроектировать класс так, чтобы он как можно реже изменялся. Это подтверждает и правило принципа единственной ответственности – у класса должна быть только одна причина для изменения.
Чтобы этого добиться, нам нужно распределить обязанности, которые сейчас лежат на классе Service, по нескольким разным классам. То есть, чтобы у каждого класса была только одна обязанность.
В контексте этого принципа можно под обязанностью подразумевать причину для изменения. Получается такое несложное правило: если вы можете найти несколько причин для изменения класса, то у такого класса более одной обязанности.
Мы проанализировали класс Service и определили, что у него три обязанности. Давайте разделим эти обязанности по разным классам. Вот что у нас получится.
Все, что относится к работе с изображениями мы вынесли в отдельный класс ImageService. Ответственность этого класса – работа с изображениями.
namespace SingleResponsibilityPrinciple
{
//ответственность класса - работа с изображениями
public static class ImageService
{
//вернуть изображение
public static Image GetImage(string path)
{
throw new NotImplementedException();
}
//сохранить изображение
public static void SaveImage(Image img)
{
//
}
}
}
Таким же образом поступим с почтой и XML-файлами. Класс EmailService отвечает за работу с почтой, а класс SiteMapFileService отвечает за работу с этим xml-файлом.
namespace SingleResponsibilityPrinciple
{
//ответственность класса - работа с email
public static class EmailService
{
//отправить email
public static void SendEmailMessage(string address, string subject, string text)
{
//
}
}
}
namespace SingleResponsibilityPrinciple
{
//ответственность класса - работа с файлом sitemap.xml
public static class SiteMapFileService
{
//сгенерировать новый файл sitemap.xml
public static void GenerateSiteMapFile(string[] values)
{
//
}
}
}
Итак, раньше у нас был класс Service, который умел абсолютно все, начиная от работы с почтой и заканчивая рисованием изображений. То есть он был наделен множеством обязанностей. Чтобы реализовать принцип единственной ответственности мы разделили класс Service на несколько более специализированных классов. У каждого такого класса теперь одна своя обязанность. Какой-то класс отвечает за работу с почтой, какой-то класс отвечает за работу с изображениями, и так далее.
Какие плюсы мы получили от такого разделения?
Во-первых, новые классы стали более легкими и понятными. В них содержится меньше кода, и вообще не составляет труда определить, что тот или иной класс делает. Сразу понятно, что класс EmailService ответственен за работу с почтой, SiteMapFileService ответственен за работу с файлом sitemap.xml и так далее.
Во-вторых, самое главное, мы реализовали принцип единственной обязанности. Теперь у нас каждый класс имеет одну обязанность, и, соответственно, только одну причину для изменения. Если нам, например, нужно будет изменить логику работы с почтой, то мы будем обращаться и изменять класс EmailService, в то время как два других класса, ImageService и SiteMapFileService останутся нетронутыми. А значит, мы будем уверены, что они по-прежнему работают правильно. Нам нужно будет протестировать только работу с классом EmailService.
Давайте рассмотрим еще один пример – класс Student. Этот класс описывает сущность «студент». Он состоит из нескольких свойств, Id, Name, GroupId, в нем также содержатся несколько статических методов: выбрать всех студентов, выбрать конкретного студента по Id, выбрать всех студентов в определенной группе. То есть с этим классом мы можем работать как на уровне его объектов, например, создавать конкретного студента. Также мы можем работать с этим классом в статическом контексте, например, можем выбрать всех студентов из базы данных.
namespace SingleResponsibilityPrinciple
{
//сущность "студент"
//нарушение принципа Single Responsibility Principle
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int GroupId { get; set; }
//выбрать всех студентов из БД
public static IEnumerable<Student> GetAllStudents()
{
throw new NotImplementedException();
}
//выбрать конкретного студента по Id из БД
public static Student GetStudentById(int id)
{
throw new NotImplementedException();
}
//выбрать всех студентов в определенной группе из БД
public static IEnumerable<Student> GetStudentsByGroupId(int id)
{
throw new NotImplementedException();
}
}
}
Давайте проанализируем этот класс, сколько у него обязанностей. На первый взгляд может показаться, что у этого класса всего одна обязанность. То есть он представляет сущность «студент» и все что с ним связано. Но на самом деле у этого класса две обязанности. Во-первых, он представляет непосредственно сущность «студент», то есть описывает эту сущность с помощью свойств. Во-вторых, этот класс также знает, как работать с базой данных, знает как извлекать записи и как сохранять записи в базу данных.
Опять же налицо нарушение принципа единственной ответственности. Здесь ответственности две, и, соответственно, появляется две причины для изменения класса. Класс будет изменяться, если нам нужно будет как-то изменить сущность «студент», например, добавить новое свойство, обозначающее год рождения. Также класс будет изменяться, когда нам будет необходимо изменить логику работы с базой данных, например, добавить метод, который выбирает всех студентов моложе двадцати лет.
Чтобы решить проблему нам опять же нужно разделить обязанности этого класса на два других класса. Это мы и сделали.
namespace SingleResponsibilityPrinciple
{
//сущность "студент"
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int GroupId { get; set; }
}
}
namespace SingleResponsibilityPrinciple
{
//репозиторий студентов
//ответственность класса - работа с базой данных
public class StudentsRepository
{
//выбрать всех студентов
public IEnumerable<Student> GetAllStudents()
{
throw new NotImplementedException();
}
//выбрать конкретного студента по Id
public Student GetStudentById(int id)
{
throw new NotImplementedException();
}
//выбрать всех студентов в определенной группе
public IEnumerable<Student> GetStudentsByGroupId(int id)
{
throw new NotImplementedException();
}
}
}
Теперь класс Student описывает непосредственно эту бизнес-сущность в полной мере, и при этом он не перегружен никакими другими деталями, не берет на себя никаких других обязанностей.
Также мы создали класс StudentsRepository. Вот как раз-таки этот класс теперь и отвечает за доступ к базе данных.
Вот что из себя представляет принцип единственной ответственности. У класса должна быть только одна причина для изменения. Очень важно проанализировать класс и правильно определить все его обязанности. После этого разделить обязанности на отдельные классы.
Важно вовремя заметить тот момент, когда класс из простого и безобидного начинает превращаться в сложный, начинает брать на себя слишком много ответственности. Пример с классом Service – это довольно распространенный пример. Этот класс может и умеет все. Помимо перечисленных методов сюда также могут входить любые другие, например, определение курса валют, работа с текстом, работа с файлами и так далее.
А начинается все совершенно безобидно. К примеру, мы создаем новое приложение, которое будет работать с графикой. И создаем в нем служебный класс Service, где определяем методы для работы с графикой. Постепенно, со временем, наше приложение изменяется, совершенствуется, также изменяется и класс Service. Вдруг нам стало необходимо еще отправлять email-письма, и мы добавляем в класс Service новый функционал. Еще через какое-то время мы решили создавать XML-файлы, и опять включили новый функционал в класс Service. Получается, с каждым новым изменением этот класс Service начинает брать на себя все больше и больше обязанностей. Поэтому очень важно вовремя заметить этот момент и реализовать принцип единственной ответственности.
Может быть, это даже будет не три новых класса, как у нас в примере, а несколько больше. Например, когда мы работаем с изображениями, мы можем непосредственно как-то их изменять, менять цвет, размер, поворачивать и так далее. И при этом у нас будет необходимость сохранять и загружать эти изображения в файловую систему, в базу данных, загружать изображения из интернета.
То есть получается новый класс ImageService по-прежнему имеет несколько обязанностей. Работа с изображениями – это одна обязанность, сохранение\загрузка изображений – это другая обязанность. Вывод напрашивается сам собой. Нам опять же нужно будет разделить эти обязанности по разным классам.
И еще одно очень важное замечание. Оно касается абсолютно всех принципов, в том числе и этого. Не стоит применять этот принцип без необходимости. То есть не надо его применять просто потому что он есть.
Если вы создаете небольшое простенькое приложение в несколько десятков строк кода для личного использования, то вполне можно допустить, чтобы у класса было несколько обязанностей. Это приложение простое, изменяться оно будет нечасто, если вообще будет, поэтому вполне допустимо такое поведение. В этом случае применение принципа единственной ответственности только усложнит ваш код.
В завершении урока давайте подведем небольшой итог. Принцип единственной ответственности – один из самых простых, но при этом его трудно применять правильно. Сочетание обязанностей для нас выглядит совершенно естественно. Их выявление и разделение как раз и является одной из задач, которая встает перед нами. Если мы хотим, чтобы при постоянных изменениях программы дизайн всегда оставался удобным и простым, нам нужно своевременно применять принцип единственной ответственности.