Программисты часто используют SOLID-принципы для создания чистого кода, но при этом могут нарушать другие стандарты разработки. От того, какие нормы приняты в команде, зависит эффективность и слаженность работы над проектом. В этой статье мы расскажем о пяти основных принципах, различиях между методологиями, правилах их использования и типичных ошибках, которые могут возникнуть при внедрении этих принципов в проект.
Какие принципы есть в программировании
KISS | YAGNI | DRY | BDUF | SOLID | |
Расшифровка | Keep It Simple, Stupid | You are not gonna need it | Don’t Repeat Yourself | Big Design Up Front |
|
Перевод | Будь проще | Вам это не понадобится | Не повторяйся | Сначала большое проектирование |
|
Суть | Выбирать простейшее решение, которое соответствует требованиям | Не добавлять излишнюю функциональность. Важно отталкиваться от актуальности задач в момент написания кода. | Избегать повторений кода, дублирований | Весь дизайн проекта создается заранее, до начала разработки | Включает набор стандартов:
|
Преимущества | Уменьшение вероятности ошибок | Избежание излишней сложности и избыточности | Улучшение поддерживаемости и расширяемости | Предотвращение издержек на детализацию | Улучшение читаемости, поддерживаемости и расширяемости кода |
KISS
Pattern KISS заключается в выборе наиболее легкого способа выполнения задачи. Этот принцип рекомендует избегать излишней сложности и стремиться к простоте на всех уровнях разработки.
Правила применения:
- Предлагайте небольшие методы — до 50 строк.
- Один метод должен решать только одну задачу.
- Позаботьтесь о том, чтобы в будущем не возникло сложностей при модификации кода.
- Не загружайте библиотеку, если из нее требуется только одна функция.
- Используйте понятные и самодокументирующиеся имена переменных и методов, чтобы облегчить чтение и понимание кода.
- Разделяйте код на логически связанные части, используя модули и классы, чтобы упростить его поддержку и повторное использование.
К примеру, чтобы сократить код, используйте стрелочные функции, параметры по умолчанию, деструктуризацию, async/await, блоки try/catch. Помните, что простота — это не отсутствие функциональности, а ясность и предсказуемость кода.
Вот простой пример кода, написанного с учётом принципа KISS:
// Пример с KISS
public class Circle {
private double radius;
public Circle (double radius) {
this. radius = radius;
}
// Метод для расчета площади круга
public double calculateArea () {
return Math. PI * radius * radius;
}
// Метод для расчета длины окружности
public double calculateCircumference () {
return 2 * Math. PI * radius;
}
public static void main (String[] args) {
Circle circle = new Circle (5.0);
System.out.println («Area: «+ circle. calculateArea ());
System.out.println («Circumference: «+ circle. calculateCircumference ());
}
}
- Маленькие методы: Каждый метод решает одну конкретную задачу: calculateArea () вычисляет площадь, calculateCircumference () вычисляет длину окружности. Каждый из методов короткий и выполняет одну функцию.
- Понятные имена: Имена методов и переменных описывают их назначение, делая код легко читаемым и самодокументирующимся.
- Отсутствие избыточности: Код простой, без лишних сложностей. Для выполнения вычислений используются встроенные функции (Math.PI), что позволяет избежать излишней нагрузки и ошибок.
- Переиспользование: Класс Circle можно легко использовать в других частях программы или проекта, что делает его универсальным и поддерживаемым.
Типичные ошибки:
- Желание придумать новый метод. Потратьте время на поиск готовых решений и выберите подходящий.
- Сложность логики. Проверьте понятность цепочки. Это уменьшит риск ошибок при работе.
- Применение новинок ради новинок. Выбирайте только те технологии, которые действительно подходят для написания кода.
- Сложные и запутанные методы.
Пример: Создание метода, который выполняет множество разных задач и включает в себя сложные логические конструкции и вложенные циклы. Это делает метод трудным для понимания и отладки.
Решение: Разделите метод на более простые и понятные функции, каждая из которых выполняет одну конкретную задачу.
YAGNI
YAGNI принцип состоит в своевременности разработки кода. Не стоит придумывать методы, которые понадобятся через время. Опирайтесь на актуальные запросы. Принцип YAGNI помогает избежать избыточной сложности и сосредоточиться на том, что действительно нужно сейчас.
Правила применения:
- Отдавайте приоритет практической пользе, а не догадкам, что код пригодится в будущем.
- Удалите ненужный код. При очистке важных фрагментов, вернитесь к ним с помощью Git.
- Не включайте в программу функционал, о котором не просили заказчики.
- Регулярно проверяйте код на наличие избыточности и удаляйте или упрощайте участки, которые не приносят реальной пользы.
- Используйте минимально необходимый набор библиотек и инструментов, чтобы избежать ненужной сложности и зависимостей.
Пример кода с YAGNI:
public class OrderProcessor {
// Метод для обработки заказа
public void processOrder (Order order) {
// Проверка и обработка заказа
if (order.isValid ()) {
System.out.println («Processing order #" + order. getId ());
// Дополнительная логика обработки заказа
}
}
// Метод для расчета стоимости заказа
public double calculateTotal (Order order) {
return order. getItems ().stream ().mapToDouble (Item:getPrice).sum ();
}
// Удален метод резервирования товара, так как он пока не нужен
// public void reserveItem (Item item) {
// // Логика резервирования
// }
}
- Минимум необходимого функционала: В этом примере реализованы только те методы, которые действительно необходимы для текущих задач (processOrder () и calculateTotal ()). Метод reserveItem () закомментирован и не реализован, так как нет текущей потребности в его использовании.
- Избежание избыточности: Метод reserveItem () мог бы добавлять лишнюю сложность в программу, если бы он был реализован без явной необходимости. Следуя принципу YAGNI, этот метод не был включен, что делает код более чистым и понятным.
- Фокус на актуальных запросах: Код ориентирован на текущие потребности — обработку и расчет стоимости заказа. Это соответствует принципу YAGNI, который гласит, что нужно писать только тот код, который нужен прямо сейчас.
Типичные ошибки:
- Трата времени на несуществующие задачи вместо того, чтобы улучшить имеющийся код. Работа на опережение часто оказывается впустую, потому что планы и цели компании могут измениться.
- Добавление ненужного функционала заранее.
Пример: Реализация сложной функциональности, такой как расширенная система настроек, которая не востребована в текущем проекте, но предполагается, что может быть полезна в будущем. Это приводит к увеличению сложности и времени разработки.
Решение: Сосредоточьтесь на текущих требованиях и добавляйте функционал только по мере необходимости, на основе конкретных запросов.
DRY
Принцип DRY (Don't Repeat Yourself) сформулирован Энди Хантом и Дэйвом Томасом в книге «Программист-прагматик: путь от подмастерье к мастеру». Смысл заключается в отсутствии дублирования кода. Принцип DRY помогает избежать избыточности и облегчить поддержку кода, минимизируя количество мест, где нужно вносить изменения.
Паттерны использования:
- Не допускайте копирования. Создавайте функции и классы, которые можно переиспользовать вместо дублирования кода в разных местах.
- Соблюдайте единую логическую цепочку. Поддерживайте общую структуру и организацию кода, чтобы избежать повторного изобретения колеса.
- Перед добавлением новой функции, проверьте, возможно её аналог уже присутствует в проекте. Преимущественно улучшайте и рефакторите существующий код, а не создавайте новый, чтобы избежать дублирования функциональности.
- Используйте библиотеки и фреймворки, которые обеспечивают общие решения для часто встречающихся задач.
- Регулярно проводите ревью кода, чтобы обнаруживать и устранять дублирование на ранних стадиях.
Пример кода с DRY:
public class Invoice {
private double amount;
private double taxRate;
public Invoice (double amount, double taxRate) {
this. amount = amount;
this. taxRate = taxRate;
}
// Метод для расчета общей суммы
public double calculateTotal () {
return amount + calculateTax ();
}
// Метод для расчета налога
private double calculateTax () {
return amount * taxRate;
}
}
- Избежание дублирования: Метод calculateTotal () вызывает метод calculateTax (), который вычисляет налог. Вместо того, чтобы повторять вычисление налога в разных местах, мы создаем один метод для его расчета. Это позволяет легко изменить логику расчета налога в одном месте, если потребуется.
- Переиспользование: Метод calculateTax () используется в методе calculateTotal (), что предотвращает повторение кода и упрощает поддержку. Если потребуется изменить способ расчета налога, достаточно обновить только один метод.
- Упрощение изменений: Если налоговая ставка изменится, достаточно будет изменить значение taxRate, и все расчеты будут автоматически обновлены, так как нет дублирующего кода.
Типичные ошибки:
- Дублирование кода. Результат — трата времени и сил на тестирование нескольких похожих фрагментов.
- Незнание системы приводит к повторам. Изучите функционал внимательно.
- Копирование и вставка кода.
Пример: Повторение одной и той же логики в разных частях приложения, что приводит к дублированию кода. Например, вычисление одних и тех же значений в нескольких местах без использования общих функций или классов.
Решение: Выделите повторяющийся код в отдельные функции или методы и переиспользуйте их по всему проекту.
BDUF
BDUF (Big Design Up Front) предполагает глубокую подготовку перед реализацией проекта. Этот подход помогает избежать значительных изменений и доработок на поздних стадиях проекта, что может сэкономить время и ресурсы.
Правила:
- Убедитесь, что продуманы все логические связи и функции. Проведите тщательный анализ требований и проектирование, чтобы иметь четкое представление о структуре и функциональности системы.
- Доведите проектирование до конца и только потом переходите к реализации. Старайтесь завершить проектирование на этапе планирования, чтобы все детали были учтены до начала кодирования.
- При проверке опирайтесь на приоритетные задачи. Разделите цели на этапы и проанализируйте их друг за другом. Постепенно переходите к реализации, начиная с наиболее важных и критичных задач.
- Согласуйте архитектуру кода с другими участниками команды. Исправьте ошибки до старта. Командная работа и согласование архитектуры помогают избежать несоответствий и повышают согласованность работы.
- Регулярно проводите ревизию проектной документации и прототипов, чтобы убедиться, что все элементы согласованы и правильно реализованы.
- Проводите оценку рисков и разрабатывайте планы на случай возникновения проблем, чтобы минимизировать влияние непредвиденных ситуаций.
Пример кода с BDUF:
public class Library {
private List<Book> books;
private Map<String, Member> members;
public Library () {
books = new ArrayList<>();
members = new HashMap<>();
}
// Метод для добавления книги
public void addBook (Book book) {
books. add (book);
}
// Метод для добавления члена
public void addMember (Member member) {
members. put (member.getId (), member);
}
// Метод для выдачи книги члену
public void lendBook (String bookTitle, String memberId) {
Book book = findBook (bookTitle);
Member member = members. get (memberId);
if (book ≠ null && member ≠ null) {
member. borrowBook (book);
books. remove (book);
}
}
private Book findBook (String title) {
return books. stream ()
.filter (book -> book. getTitle ().equals (title))
.findFirst ()
.orElse (null);
}
}
Глубокое проектирование: В этом примере перед реализацией методов были продуманы все основные сущности и их взаимодействие: книги (Book), члены библиотеки (Member), и библиотека (Library). Архитектура была определена до написания кода.
Согласованная архитектура: Код демонстрирует предварительное проектирование и четкую структуру классов, что позволяет легко добавлять функциональность и поддерживать проект.
Планирование этапов: Методы реализованы поэтапно: добавление книг и членов, а затем выдача книг. Это позволяет сначала сосредоточиться на базовых функциях и улучшать их по мере необходимости.
Ревизия и согласование: Проектирование и планирование этапов разработки помогают минимизировать ошибки и предотвращают проблемы на этапе реализации. Например, метод findBook () реализован отдельно для поиска книги, что упрощает тестирование и поддержку.
Типичные ошибки:
- Недооценка подготовительного этапа. Добавление правок в спецификации занимает пару часов, а в код — дни.
- Стремление видеть результат уже сейчас. Проектирование — важный процесс. Он помогает заметить и устранить ошибки.
- Чрезмерное проектирование и игнорирование изменений.
Пример: Создание очень детализированного плана и архитектуры системы, которые быстро становятся устаревшими по мере изменения требований и новых находок в процессе разработки. Это может привести к большой разнице между изначальным проектом и конечным продуктом.
Решение: Рассмотрите возможность использования гибких методов проектирования, таких как Agile, которые позволяют адаптироваться к изменениям и корректировать проект в процессе разработки.
SOLID
SOLID — это набор пяти принципов объектно-ориентированного проектирования, которые помогают разработчикам создавать более гибкий, понятный и поддерживаемый код. Эти принципы были популяризованы Робертом Мартином (также известным как «Чистый Архитектор») и представляют собой акроним, где каждая буква обозначает один из принципов:
- S — Single Responsibility Principle (SRP)
Принцип единственной ответственности: Класс должен иметь только одну зону ответственности, то есть должен быть ответственным за выполнение одной задачи или функционала. Это упрощает понимание и поддержку кода, так как каждая часть системы решает только одну задачу.
- O — Open/Closed Principle (OCP)
Принцип открытости/закрытости: Модули (классы, функции
и т. д. ) должны быть открыты для расширения, но закрыты для изменений. Это означает, что существующий код не должен изменяться при добавлении нового функционала; вместо этого, функционал должен расширяться новыми модулями или классами. - L — Liskov Substitution Principle (LSP)
Принцип подстановки Барбары Лисков: Объекты производных классов должны быть заменяемы объектами базового класса без нарушения корректности программы. Это позволяет использовать полиморфизм и наследование, сохраняя при этом корректность работы программы.
- I — Interface Segregation Principle (ISP)
Принцип разделения интерфейсов: Интерфейсы должны быть узкими и специализированными. Это означает, что классы не должны реализовывать интерфейсы, методы которых они не используют. Это упрощает изменения и расширения, предотвращая создание «тяжелых» и избыточных интерфейсов.
- D — Dependency Inversion Principle (DIP)
Принцип инверсии зависимостей: Модули высшего уровня не должны зависеть от модулей низшего уровня. Оба должны зависеть от абстракций (интерфейсов или абстрактных классов). Это уменьшает связность между компонентами и повышает гибкость системы.
Солид включает пять разных принципов, направленных на улучшение структуры и поддерживаемости кода:
- У функции должна быть только одна зона ответственности (Single Responsibility Principle, SRP).
Каждый класс или метод должен иметь одну, четко определенную цель и отвечать за выполнение одной задачи. Это облегчает понимание и изменение кода. - Модули должны быть открыты для расширения, но закрыты для изменений (Open/Closed Principle, OCP).
Это означает, что существующий код не должен изменяться при добавлении нового функционала. Вместо этого, система должна быть расширена с помощью новых кодов. - Функция, которая применяет базовые объекты, может использовать их подтипы без дополнительных сложных конструкций (Liskov Substitution Principle, LSP).
Объекты производных классов должны быть заменяемы объектами базового класса без нарушения корректности программы. Это позволяет использовать полиморфизм для повышения гибкости кода. - Не переусердствуйте с функциональностью интерфейсов. Разделяйте их на части (Interface Segregation Principle, ISP).
Интерфейсы должны быть узкими и специализированными, чтобы классы реализовывали только те методы, которые им действительно нужны. Это упрощает изменения и расширения. - Модули должны зависеть от абстракций (Dependency Inversion Principle, DIP).
Модули высшего уровня не должны зависеть от модулей низшего уровня. Оба должны зависеть от абстракций (интерфейсов или абстрактных классов). Это уменьшает связность между компонентами и повышает гибкость системы.
Пример кода SOLID:
// Принцип Single Responsibility
public class Invoice {
private double amount;
public Invoice (double amount) {
this. amount = amount;
}
// Отвечает только за расчет суммы
public double calculateTotal () {
return amount + calculateTax ();
}
// Метод для расчета налога
private double calculateTax () {
return amount * 0.2; // Простой налог 20%
}
}
// Принцип Open/Closed
public interface DiscountStrategy {
double applyDiscount (double amount);
}
public class SeasonalDiscount implements DiscountStrategy {
@Override
public double applyDiscount (double amount) {
return amount * 0.9; // 10% скидка
}
}
// Принцип Liskov Substitution
public abstract class Shape {
public abstract double area ();
}
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle (double width, double height) {
this. width = width;
this. height = height;
}
@Override
public double area () {
return width * height;
}
}
// Принцип Interface Segregation
public interface Printable {
void print ();
}
public interface Scannable {
void scan ();
}
public class MultiFunctionPrinter implements Printable, Scannable {
@Override
public void print () {
System.out.println («Printing…»);
}
@Override
public void scan () {
System.out.println («Scanning…»);
}
}
// Принцип Dependency Inversion
public interface Repository {
void save (Object object);
}
public class UserRepository implements Repository {
@Override
public void save (Object object) {
System.out.println («User saved!»);
}
}
public class UserService {
private Repository repository;
public UserService (Repository repository) {
this. repository = repository;
}
public void createUser (Object user) {
repository. save (user);
}
}
- Single Responsibility Principle (SRP): Класс Invoice отвечает только за расчёт суммы, а метод calculateTax () используется для вычисления налога. Класс не занимается выводом или другими задачами.
- Open/Closed Principle (OCP): Интерфейс DiscountStrategy позволяет добавлять новые стратегии скидок, не изменяя существующий код. Класс SeasonalDiscount расширяет функциональность, не изменяя основного кода.
- Liskov Substitution Principle (LSP): Класс Rectangle является подтипом Shape и реализует метод area (), что позволяет использовать его без нарушения логики при работе с объектами типа Shape.
- Interface Segregation Principle (ISP): Интерфейсы Printable и Scannable разделены, что позволяет классам реализовывать только те методы, которые им действительно нужны. MultiFunctionPrinter реализует оба интерфейса.
- Dependency Inversion Principle (DIP): UserService зависит от абстракции Repository, а не от конкретной реализации. Это позволяет легко менять реализацию репозитория без изменения кода сервиса.
Типичные ошибки:
- Учет некоторых из пяти принципов, а не всех в совокупности.
- Нарушение принципа Single Responsibility (SRP).
Пример: Класс UserManager, который отвечает за управление пользователями, хранение данных о пользователях и выполнение операций с базой данных. Это делает класс сложным для поддержки и тестирования.
Решение: Разделите класс на несколько классов, каждый из которых будет отвечать за одну конкретную задачу, например, один класс для управления пользователями, другой — для работы с базой данных.
Главное, что нужно знать
- KISS (Keep It Simple, Stupid)
- Принцип простоты: Стремитесь к простоте и ясности в коде. Избегайте излишней сложности и сложных решений.
- Маленькие методы: Создавайте небольшие, понятные методы, которые выполняют одну задачу.
- YAGNI (You Aren’t Gonna Need It)
- Своевременность разработки: Реализуйте только тот функционал, который действительно нужен сейчас. Не добавляйте предполагаемые функции для будущих сценариев.
- Удаление ненужного: Удаляйте ненужный код и не реализуйте функции заранее.
- DRY (Don't Repeat Yourself)
- Избегайте дублирования: Не повторяйте код. Переиспользуйте существующие решения и создавайте универсальные функции.
- Единая логика: Централизуйте повторяющуюся логику в одном месте для упрощения поддержки и изменений.
- BDUF (Big Design Up Front)
- Детальное проектирование: Завершите полный дизайн и планирование системы до начала реализации.
- Снижение изменений: Минимизируйте изменения в процессе разработки, полагаясь на заранее спроектированную архитектуру.
- SOLID
- Single Responsibility Principle (SRP): Каждый класс и метод должны иметь только одну зону ответственности.
- Open/Closed Principle (OCP): Модули должны быть открыты для расширений, но закрыты для изменений.
- Liskov Substitution Principle (LSP): Подтипы должны быть заменяемы базовыми типами без нарушения корректности.
- Interface Segregation Principle (ISP): Интерфейсы должны быть узкими и специализированными.
- Dependency Inversion Principle (DIP): Модули должны зависеть от абстракций, а не от конкретных реализаций.