Важным шагом к пониманию того, как писать правильный, аккуратный и чистый код является понимание принципов SOLID
.
Итак, SOLID
- это акроним, каждой букве соответствует свой принцип:
- S - SRP - Single responsibility principle
- O - OCP - Open closed principle
- L - LSP - Liskov substitution principle
- I - ISP - Interface segregation principle
- D - DIP - Dependency inversion principle
В полезных ссылках вы найдете примеры кода по каждому принципу.
A class should have one, and only one, reason to change.
Класс должен иметь только одну причину для изменения.
Каждый класс должен иметь одну обязанность(ответственность за что-то) и эта обязанность должна быть полностью инкапсулирована в классе.
Для примера рассмотрим сервис, который занимается составлением и рассылкой отчетов.
Объединив все эти задачи в один класс мы полностью решим поставленную задачу по написанию нашего сервиса. Такой класс будет иметь сразу несколько областей ответственности, а значит мы будем вынуждены вносить правки по каждому из перечисленных случаев:
- Изменяется содержимое отчета
- Изменяется формат отчета
- Изменяется способ рассылки отчета
Из-за того, что класс ответственен сразу за несколько задач в дальнейшем могут возникнуть сложности в поддержке работоспособности кода, в гибкости использования сервиса и т.д.
SRP
говорит как раз о том, что надо разделить такой класс на несколько классов, чтобы каждый класс был ответственен за свою область.
Один класс отвечает за подготовку отчета, другой - за рассылку.
В таком случае наш сервис сначала запрашивает отчет у класса, который занимается отчетами, а после просит класс-отправитель осуществить рассылку.
Следование SRP
принципу даст нам слабо связанное приложение, которое будет легко изменять и дорабатывать в дальнейшем.
Делегируйте ответственность!
И не забывайте: KISS!
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Программные сущности(классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения.
Т.е класс должен быть закрыт к изменению извне, но при этом - должен иметь возможности к расширению реализации.
В качестве примера рассмотрим класс Greeter
, отвечающий за приветствия:
public class Greeter {
String formality;
public String greet() {
if (this.formality == "formal") {
return "Good evening, sir.";
}
else if (this.formality == "casual") {
return "Sup bro?";
}
else if (this.formality == "intimate") {
return "Hello Darling!";
}
else {
return "Hello.";
}
}
public void setFormality(String formality) {
this.formality = formality;
}
}
В зависимости от 'формальности' мы подбираем приветствие.
Чем плох данный код?
Тем, что при добавлении нового приветствия необходимо изменять код Greeter
, класс не открыт для расширения.
В то же время, если постараться сделать его более расширяемым можно ввести понятие интерфейса Personality
, отвечающего за то, какое приветствие будет:
public interface Personality {
public String greet();
}
И класс Greeter
будет выглядеть уже как:
public class Greeter {
private Personality personality;
public Greeter(Personality personality) {
this.personality = personality;
}
public String greet() {
return personality.greet();
}
}
Реализации интерфейса Personality
:
public class FormalPersonality implements Personality {
public String greet() {
return "Good evening, sir.";
}
}
или
public class IntimatePersonality implements Personality {
public String greet() {
return "Hello Darling!";
}
}
Класс Greeter
спроектирован так, что открыт для расширения, мы можем передать любой Personality
, в зависимости от задачи. И получим необходимый результат.
При этом сам код Greeter
не изменяется.
Т.е классы и модули должны проектироваться так, чтобы для изменения их поведения, нам не нужно было изменять их исходный код.
В этом и состоит суть OCP
принципа.
Этот принцип имеет сложное математическое определение, которое можно заменить на:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Или, если совсем упростить:
Объекты могут быть заменены их наследниками без изменения свойств программы.
В качестве примера рассмотрим классы Прямоугольник
и Квадрат
.
Класс Прямоугольник
имеет методы, устанавливающие ширину и длину.
Для переиспользования кода класс Квадрат
наследуется от Прямоугольника
и переопределяет методы, определяющие ширину и длину, так, что любое изменение длины изменяет также и ширину, и наоборот.
Т.е выставляем всегда одинаковые значения для ширины и длины у класса Квадрата
.
class Rectangle {
private int height;
private int width;
public void setHeight(int height) {
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
}
class Square extends Rectangle {
@Override
public void setHeight(int height) {
this.height = height;
this.width = height;
}
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
}
На первый взгляд все отлично, но это явное нарушение Liskov Substitution Principle
.
Почему? Потому что в таком случае нарушается поведение базового класса!
При использовании такого наследования Квадрат
можно использовать везде, где используется родительский класс Прямоуголник
.
Так вот там, где мы используем Квадрат
в качестве Прямоугольник
-а никто не ожидает, что при изменении длины вдруг изменится и ширина, ведь поведение базового класса нарушено.
Есть два варианта решения возникшей проблемы:
- Сделать два независимых класса.
- Сделать абстрактный класс
ГеометрическаяФигура
, от которого отнаследоваться иКвадратом
, иПрямоугольником
.
Применять наследование
в примере выше вообще говоря довольно плохая идея, хотя бы потому, что наследование - это is a
отношение.
А Квадрат
не является Прямоугольником
, также как и Прямоугольник
не является Квадратом
.
Можно привести еще более простой пример, для демонстрации важности LSP
.
Рассмотрим класс java.util.ArrayList
и отнаследуемся от него, при этом переопределив методы так, что индексы элементов будут считаться не с 0
, как обычно, а с 1
.
Нарушение поведения родительского класса влечет за собой масштабные проблемы при работе с таким кодом, так как теперь, в зависимости от реализации, индексы элементов считаются по разному.
Many client-specific interfaces are better than one general-purpose interface.
Принцип разделения интерфейсов говорит о том, что при проектировании интерфейсов необходимо придерживаться минимализма. А слишком "толстые" интерфейсы необходимо разделять(разбивать) на более мелкие и более специфичные. Тот, кто использует интерфейс должен знать только о методах, которые необходимы им в работе и не более того.
При изменении какого-либо метода интерфейса не должны меняться клиенты, которые этот метод не используют.
Для примера рассмотрим интерфейс ReportGenerator
:
interface ReportGenerator {
String generate();
String generateXml();
String generateJson();
}
Классам, реализующим такой интерфейс, потребуется переопределить все перечисленные способы генерации отчетов.
В то время как некоторым из них может вообще не потребоваться возможность генерации отчета в xml
или в json
.
Но с нашим 'толстым' интерфейсом мы не предоставляем никакого выбора.
Поэтому такой интерфейс лучше разбить на несколько, XmlReportGenerator
и JsonReportGenerator
и PlainReportGenerator
.
И предоставлять разработчику возможность выбора, где какая генерация необходима. И там, где это необходимо реализовывать эти специфичные интерфейсы.
Следование этому принципу позволит писать более гибкий и проще поддерживаемый код.
Внимательный читатель должен уже провести аналогию с SRP
!
Depend on abstractions, not on concretions.
-
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
-
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Старайтесь, чтобы различные модули были автономными, и соединялись друг с другом с помощью абстракций.
Модуль верхнего уровня - модуль, работающий с бизнес-логикой. Чем ближе модуль к вводу/выводу, тем ниже уровень модуля.
Например, работа с БД - модуль более низкого уровня, чем модуль, работающий с бизнес-логикой пользователя.
Идея состоит в том, что разделяя уровни бизнес-логики и модули нижних уровней(например, работа с БД), вы в дальнейшем сможете поменять реализацию модуля нижнего уровня без изменения кода бизнес-логики.
Это достигается, если следовать OCP
и LSP
.
Разберем следующий пример:
public class WeatherTracker {
String currentConditions;
Phone phone;
Emailer emailer;
public WeatherTracker() {
phone = new Phone();
emailer = new Emailer();
}
public void setCurrentConditions(String weatherDescription) {
this.currentConditions = weatherDescription;
if (weatherDescription == "rainy") {
String alert = phone.generateWeatherAlert(weatherDescription);
System.out.print(alert);
}
if (weatherDescription == "sunny") {
String alert = emailer.generateWeatherAlert(weatherDescription);
System.out.print(alert);
}
}
}
В зависимости от описания погоды выбирается тип оповещения.
Так вот текущая релизация завязана на реализации phone
и emailer
, что не дает нам гибкости в использовании.
Чтобы исправить текущий недостаток необходимо выделить уровень абстракции, от которой будут зависеть реализации.
В таком случае код будет выглядеть в виде:
interface Notifier {
public void alertWeatherConditions(String weatherConditions);
}
public class MobileDevice implements Notifier {
public void alertWeatherConditions(String weatherConditions) {
if (weatherConditions == "rainy")
System.out.print("It is rainy");
}
}
public class EmailClient implements Notifier {
public void alertWeatherConditions(String weatherConditions) {
if (weatherConditions == "sunny");
System.out.print("It is sunny");
}
}
public class WeatherTracker {
String currentConditions;
public void setCurrentConditions(String weatherDescription) {
this.currentConditions = weatherDescription;
}
public void notify(Notifier notifier) {
notifier.alertWeatherConditions(currentConditions);
}
}
В текущей реализации как раз детали завияст от абстракции и модуль верхнего уровня не зависит от модулей нижнего уровня(реализации Notifier
).
Есть еще полезные принципы, о которых также следует поговорить.
Keep it simple, stupid.
KISS
— это принцип проектирования и программирования, при котором простота системы декларируется в качестве основной цели или ценности.
Основной посыл KISS
в том, что не имеет смысла реализовывать дополнительные функции и модули, которые не нужны или их использование крайне маловероятно.
Также, не стоит перегружать интерфейс теми опциями, которые не нужны большинству пользователей.
В простоте - сила.
Стоит отметить еще то, что надо опасаться неограниченно и бесконтрольно увеличивать уровень абстракций, так как это может отразиться на увеличении сложности архитектуры приложения.
Не стоит также закладывать избыточные функции "про запас", так как к моменту, когда понадобится этот функционал либо изменятся требования, либо этот момент может и вовсе никогда не наступить.
Еще одним важным моментом является контроль за зависимостями проекта. Не стоит тянуть в зависимостях 'огромную' библиотеку, если вам от неё нужна лишь пара функций.
Стремитесь декомпозировать сложную задачу на простые составляющие.
Don’t repeat yourself.
Стоит избегать дублирования кода.
Если ваш код не дублируется, то его изменение и модификация будет происходить всегда в одном месте, что сократит количество ошибок, упростит тестирование и улучшит понимаение. В противном случае вы обрекаете ваш продукт на ужасные муки при тестировании и внесении нового функционала.
You aren't gonna need it.
Старайтесь избегать излишних абстракций, обходите стороной желания эксперимента 'из интереса' и не делайте реализации функционала, который сейчас не нужен, но, по вашему мнению, может либо вскоре понадобиться, либо просто будет полезен.
Как уже было сказано в KISS
, в реальности функционал 'про запас' часто оказывается либо не нужен, либо не готов к текущим реалиям и требует доработок, что сводит ваши усилия на нет.
Avoid Hasty Abstractions
Принцип AHA (Avoid Hasty Abstractions) практически всегда должен быть превыше DRY. Если что-то может быть написано более просто, без дополнительных абстракций, то лучше написать это так, даже, если понадобится задублировать какой-то код. Так как лучше задублировать какой-то код, чем нагородить нелепых абстракций во имя переиспользования.
Принципы из дополнения: