DRY, KISS, YAGNI, Composition over Inheritance, Law of Demeter
Суть: каждый кусок знания (логика, правило, формула) должен существовать в системе в единственном представлении. Если одну и ту же логику приходится менять в нескольких местах — это нарушение DRY.
Формула расчёта скидки продублирована в двух методах. Если правило скидки изменится, легко забыть поправить одно из мест — и получить рассинхронизацию.
class OrderService {
double calculateWebPrice(Order order) {
double price = order.getBasePrice();
if (order.getQuantity() > 10) {
price = price * 0.9; // скидка 10%
}
return price;
}
double calculateInvoicePrice(Order order) {
double price = order.getBasePrice();
if (order.getQuantity() > 10) {
price = price * 0.9; // та же логика скопирована
}
return price;
}
}
class OrderService {
double calculateWebPrice(Order order) {
return applyBulkDiscount(order);
}
double calculateInvoicePrice(Order order) {
return applyBulkDiscount(order);
}
private double applyBulkDiscount(Order order) {
double price = order.getBasePrice();
if (order.getQuantity() > 10) {
price = price * 0.9;
}
return price;
}
}
Суть: из двух решений, дающих одинаковый результат, выбирай более простое. Излишняя гибкость и абстракции, которые не нужны прямо сейчас, усложняют чтение и поддержку кода.
Простая проверка чётности числа реализована через громоздкую конструкцию с побитовыми операциями и тернарными выражениями — вместо очевидного оператора остатка от деления.
boolean isEven(int number) {
return ((number & 1) == 0)
? true
: (number % 2 == 0)
? true
: false;
}
boolean isEven(int number) {
return number % 2 == 0;
}
Суть: не добавляй функциональность, пока она реально не понадобится. Код "про запас" увеличивает сложность и стоимость поддержки, а часто вообще никогда не используется.
Сервис уведомлений с самого начала поддерживает Email, SMS, push и "универсальный" механизм плагинов для будущих каналов — хотя на практике используется только Email.
interface NotificationChannel {
void send(String message);
}
class NotificationService {
private List<NotificationChannel> channels = new ArrayList<>();
private Map<String, Object> pluginRegistry = new HashMap<>();
void registerPlugin(String name, Object plugin) {
// "на будущее", никто пока не использует
pluginRegistry.put(name, plugin);
}
void notifyUser(String message) {
for (NotificationChannel channel : channels) {
channel.send(message);
}
}
}
class NotificationService {
void notifyUser(String email, String message) {
sendEmail(email, message);
}
private void sendEmail(String email, String message) {
// отправка email
}
}
// Абстракцию под несколько каналов
// добавим, когда появится вторая реальная
// потребность — не раньше
Суть: вместо жёсткой иерархии наследования лучше собирать поведение объекта из независимых компонентов, переданных ему (обычно через конструктор). Это снижает связанность и упрощает изменение поведения без переписывания иерархии классов.
Поведение "летать" и "крякать" зашито в базовый класс Duck через наследование. Когда появляется вид утки, который не умеет летать, приходится переопределять метод и городить исключения в иерархии.
class Duck {
void fly() {
System.out.println("Летит");
}
}
class RubberDuck extends Duck {
@Override
void fly() {
// резиновая утка не летает,
// но вынуждена переопределять метод
throw new UnsupportedOperationException();
}
}
interface FlyBehavior {
void fly();
}
class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("Летит");
}
}
class CannotFly implements FlyBehavior {
public void fly() {
System.out.println("Не летает");
}
}
class Duck {
private final FlyBehavior flyBehavior;
Duck(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
void performFly() {
flyBehavior.fly();
}
}
Duck rubberDuck = new Duck(new CannotFly());
Duck realDuck = new Duck(new FlyWithWings());
| Аспект | ❌ Наследование | ✅ Композиция |
|---|---|---|
| Смена поведения | Требует переопределения методов или новой ветки иерархии | Достаточно передать другой объект-поведение |
| Связанность | Жёсткая (compile-time) | Слабая, поведение можно менять в рантайме |
| Переиспользование | Только через цепочку родителей | Любой объект может использовать любое поведение |
| Тестирование | Нужно поднимать всю иерархию | Легко подставить mock-поведение |
Суть: объект должен общаться только со своими "непосредственными знакомыми" — не вызывать методы через цепочку чужих объектов (a.getB().getC().doSomething()). Такие цепочки жёстко привязывают код к внутренней структуре чужих классов.
Чтобы узнать город клиента, код "пролезает" через три чужих объекта. Если поменяется структура Address или Wallet — сломается код, который вообще не должен был знать об их существовании.
class OrderProcessor {
void printCity(Customer customer) {
String city = customer
.getAddress()
.getCity()
.getName();
System.out.println(city);
}
}
class Customer {
private Address address;
String getCityName() {
return address.getCityName();
}
}
class Address {
private City city;
String getCityName() {
return city.getName();
}
}
class OrderProcessor {
void printCity(Customer customer) {
System.out.println(customer.getCityName());
}
}