Принципы SOLID, DRY, KISS и паттерны проектирования
Введение: зачем нужны принципы проектирования
Когда вы пишете код, который работает — это хорошо. Но когда вы пишете код, который легко читать, изменять и поддерживать — это профессионально.
Принципы проектирования — это не догмы, а ориентиры. Они помогают принимать решения при написании кода и избегать типичных ошибок архитектуры.
Принципы — не законы. Иногда их нарушение оправдано. Главное — понимать, почему вы это делаете и какие последствия это несёт.
В этой статье разберём:
- SOLID — 5 принципов объектно-ориентированного дизайна
- DRY — не повторяйся
- KISS — делай проще
- Паттерны — готовые решения типовых задач
Принципы SOLID
SOLID — это аббревиатура из пяти принципов, сформулированных Робертом Мартином (Uncle Bob). Они помогают создавать гибкие, масштабируемые и поддерживаемые системы.
Single Responsibility Principle
Принцип единственной ответственностиКаждый класс отвечает за одну конкретную задачу.
❌ Плохо: класс делает всё
class User
{
public function save(): void
{
// Сохранение в базу данных
}
public function sendEmail(): void
{
// Отправка email
}
public function generateReport(): string
{
// Генерация отчёта
}
public function validateData(): bool
{
// Валидация
}
}
✅ Хорошо: разделение ответственности
// Сущность — только данные
class User
{
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
}
// Репозиторий — работа с БД
class UserRepository
{
public function save(User $user): void { }
public function findById(int $id): ?User { }
}
// Сервис уведомлений
class EmailService
{
public function send(User $user, string $message): void { }
}
// Генератор отчётов
class UserReportGenerator
{
public function generate(User $user): string { }
}
Open/Closed Principle
Принцип открытости/закрытостиДобавляйте новый функционал без изменения существующего кода.
❌ Плохо: изменяем класс при каждом новом типе
class PaymentProcessor
{
public function process(string $type, float $amount): void
{
if ($type === 'card') {
// Оплата картой
} elseif ($type === 'paypal') {
// Оплата PayPal
} elseif ($type === 'crypto') {
// Каждый новый способ = изменение класса
}
}
}
✅ Хорошо: расширяем через интерфейсы
interface PaymentMethodInterface
{
public function pay(float $amount): bool;
}
class CardPayment implements PaymentMethodInterface
{
public function pay(float $amount): bool
{
// Логика оплаты картой
return true;
}
}
class CryptoPayment implements PaymentMethodInterface
{
public function pay(float $amount): bool
{
// Новый способ — новый класс, старый код не меняем
return true;
}
}
// Процессор не меняется при добавлении новых способов
class PaymentProcessor
{
public function process(PaymentMethodInterface $method, float $amount): bool
{
return $method->pay($amount);
}
}
Liskov Substitution Principle
Принцип подстановки Барбары ЛисковОбъект подкласса можно использовать везде, где ожидается объект родителя.
❌ Плохо: подкласс меняет поведение
class Rectangle
{
protected int $width;
protected int $height;
public function setWidth(int $width): void
{
$this->width = $width;
}
public function setHeight(int $height): void
{
$this->height = $height;
}
public function getArea(): int
{
return $this->width * $this->height;
}
}
// Квадрат меняет поведение родителя — нарушение!
class Square extends Rectangle
{
public function setWidth(int $width): void
{
$this->width = $width;
$this->height = $width; // Неожиданное поведение!
}
}
✅ Хорошо: общий интерфейс без наследования
interface ShapeInterface
{
public function getArea(): int;
}
class Rectangle implements ShapeInterface
{
public function __construct(
private int $width,
private int $height,
) {}
public function getArea(): int
{
return $this->width * $this->height;
}
}
class Square implements ShapeInterface
{
public function __construct(
private int $side,
) {}
public function getArea(): int
{
return $this->side ** 2;
}
}
Interface Segregation Principle
Принцип разделения интерфейсовКлиенты не должны зависеть от методов, которые они не используют.
❌ Плохо: один огромный интерфейс
interface WorkerInterface
{
public function work(): void;
public function eat(): void;
public function sleep(): void;
public function code(): void;
public function design(): void;
}
// Робот не ест и не спит — но вынужден реализовать методы
class Robot implements WorkerInterface
{
public function eat(): void
{
throw new Exception('Robots do not eat'); // Костыль!
}
}
✅ Хорошо: разделённые интерфейсы
interface WorkableInterface
{
public function work(): void;
}
interface EatableInterface
{
public function eat(): void;
}
interface CodableInterface
{
public function code(): void;
}
// Человек реализует нужные интерфейсы
class Developer implements WorkableInterface, EatableInterface, CodableInterface
{
public function work(): void { }
public function eat(): void { }
public function code(): void { }
}
// Робот — только то, что умеет
class Robot implements WorkableInterface, CodableInterface
{
public function work(): void { }
public function code(): void { }
}
Dependency Inversion Principle
Принцип инверсии зависимостейВысокоуровневые модули не должны зависеть от низкоуровневых.
❌ Плохо: жёсткая зависимость от реализации
class MySQLDatabase
{
public function query(string $sql): array { }
}
// Жёсткая привязка к MySQL
class UserRepository
{
private MySQLDatabase $db;
public function __construct()
{
$this->db = new MySQLDatabase(); // Проблема!
}
}
✅ Хорошо: зависимость от абстракции
interface DatabaseInterface
{
public function query(string $sql): array;
}
class MySQLDatabase implements DatabaseInterface
{
public function query(string $sql): array { }
}
class PostgreSQLDatabase implements DatabaseInterface
{
public function query(string $sql): array { }
}
// Зависим от интерфейса — можем подменять реализацию
class UserRepository
{
public function __construct(
private DatabaseInterface $db, // Внедряем через конструктор
) {}
}
Принцип DRY
Don't Repeat Yourself
Не повторяйсяИзбегайте дублирования логики, данных и кода.
❌ Плохо: дублирование логики
class OrderController
{
public function create(): void
{
// Валидация email — копия 1
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid email');
}
}
}
class UserController
{
public function register(): void
{
// Валидация email — копия 2
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid email');
}
}
}
class NewsletterController
{
public function subscribe(): void
{
// Валидация email — копия 3
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid email');
}
}
}
✅ Хорошо: единый источник истины
// Один класс для валидации
class EmailValidator
{
public function validate(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException($email);
}
}
}
// Используем везде
class OrderController
{
public function __construct(
private EmailValidator $validator,
) {}
public function create(string $email): void
{
$this->validator->validate($email);
}
}
Не путайте дублирование кода с дублированием знаний. Два похожих куска кода могут меняться по разным причинам — тогда их объединение навредит. Это называется случайное дублирование.
Принцип KISS
Keep It Simple, Stupid
Делай проще, дурачокИзбегайте ненужной сложности. Пишите код, который легко понять.
❌ Плохо: переусложнение
// Зачем так сложно?
class StringManipulatorFactoryAbstractSingleton
{
private static ?self $instance = null;
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function createUpperCaseStrategy(): callable
{
return fn(string $s) => strtoupper($s);
}
}
// Использование
$result = StringManipulatorFactoryAbstractSingleton
::getInstance()
->createUpperCaseStrategy()('hello');
✅ Хорошо: просто и понятно
// Просто используем встроенную функцию
$result = strtoupper('hello');
// Или если нужна абстракция — делаем минимально
class StringHelper
{
public static function toUpper(string $value): string
{
return strtoupper($value);
}
}
Если вы не можете объяснить код коллеге за 30 секунд — возможно, его стоит упростить.
Принцип YAGNI
You Ain't Gonna Need It
вам это не понадобитсяПрограммисты часто пытаются предугадать будущее и пишут код «про запас» (универсальные интерфейсы, дополнительные поля в базе, сложные иерархии классов), думая: «А вдруг завтра заказчик захочет еще и это?» YAGNI говорит: не делайте этого.
Вы создаете систему регистрации пользователей и сразу добавляете возможность входа через 10 разных соцсетей и поддержку 5 языков, хотя заказчик просил просто «вход по email».
YAGNI идет рука об руку с принципом KISS (Keep It Simple, Stupid — «делай проще»). Если KISS призывает не усложнять текущую задачу, то YAGNI призывает вообще не браться за лишние задачи.
Паттерны проектирования
Паттерны — это проверенные решения типовых задач проектирования. Они не изобретают велосипед, а дают готовые архитектурные шаблоны.
Repository
Абстракция над доступом к данным. Скрывает детали хранилища.
DTO
Объект для передачи данных между слоями без логики.
Strategy
Семейство алгоритмов, взаимозаменяемых в рантайме.
Factory
Создание объектов без указания конкретных классов.
Singleton
Гарантирует один экземпляр класса на всё приложение.
Observer
Оповещение подписчиков об изменениях в объекте.
Repository — репозиторий
Repository — это прослойка между бизнес-логикой и источником данных. Он инкапсулирует логику доступа к данным и предоставляет коллекционно-подобный интерфейс.
Когда использовать:
- Хотите абстрагироваться от конкретной БД
- Нужно тестировать код без реальной базы
- Хотите централизовать логику запросов
// Интерфейс репозитория
interface UserRepositoryInterface
{
public function findById(int $id): ?User;
public function findByEmail(string $email): ?User;
public function findAll(): array;
public function save(User $user): void;
public function delete(User $user): void;
}
// Реализация для MySQL
class MySQLUserRepository implements UserRepositoryInterface
{
public function __construct(
private PDO $pdo,
) {}
public function findById(int $id): ?User
{
$stmt = $this->pdo->prepare(
'SELECT * FROM users WHERE id = :id'
);
$stmt->execute(['id' => $id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
return $data ? $this->hydrate($data) : null;
}
public function findByEmail(string $email): ?User
{
$stmt = $this->pdo->prepare(
'SELECT * FROM users WHERE email = :email'
);
$stmt->execute(['email' => $email]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
return $data ? $this->hydrate($data) : null;
}
public function save(User $user): void
{
if ($user->id === null) {
$this->insert($user);
} else {
$this->update($user);
}
}
private function hydrate(array $data): User
{
return new User(
id: $data['id'],
name: $data['name'],
email: $data['email'],
);
}
}
// Реализация для тестов (in-memory)
class InMemoryUserRepository implements UserRepositoryInterface
{
private array $users = [];
public function findById(int $id): ?User
{
return $this->users[$id] ?? null;
}
public function save(User $user): void
{
$this->users[$user->id] = $user;
}
}
// Использование в сервисе
class UserService
{
public function __construct(
private UserRepositoryInterface $repository,
) {}
public function getUser(int $id): User
{
$user = $this->repository->findById($id);
if ($user === null) {
throw new UserNotFoundException($id);
}
return $user;
}
}
DTO — Data Transfer Object
DTO — это простой объект для передачи данных между слоями приложения. Он не содержит бизнес-логики, только данные.
Когда использовать:
- Передача данных из контроллера в сервис
- Формирование ответа API
- Валидация входных данных
- Отделение внутренней модели от внешнего представления
// DTO для создания пользователя (входные данные)
readonly class CreateUserDTO
{
public function __construct(
public string $name,
public string $email,
public string $password,
) {}
// Фабричный метод из запроса
public static function fromRequest(Request $request): self
{
return new self(
name: $request->input('name'),
email: $request->input('email'),
password: $request->input('password'),
);
}
}
// DTO для ответа (выходные данные)
readonly class UserResponseDTO
{
public function __construct(
public int $id,
public string $name,
public string $email,
public string $createdAt,
) {}
// Фабричный метод из сущности
public static function fromEntity(User $user): self
{
return new self(
id: $user->id,
name: $user->name,
email: $user->email,
createdAt: $user->createdAt->format('Y-m-d H:i:s'),
);
}
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->createdAt,
];
}
}
// Использование в контроллере
class UserController
{
public function __construct(
private UserService $userService,
) {}
public function store(Request $request): JsonResponse
{
// Создаём DTO из запроса
$dto = CreateUserDTO::fromRequest($request);
// Передаём в сервис
$user = $this->userService->create($dto);
// Возвращаем DTO ответа
return response()->json(
UserResponseDTO::fromEntity($user)->toArray()
);
}
}
Strategy — стратегия
Strategy — позволяет определить семейство алгоритмов, инкапсулировать каждый из них и сделать взаимозаменяемыми.
Когда использовать:
- Есть несколько вариантов алгоритма
- Нужно переключаться между алгоритмами в рантайме
- Хотите избежать множества условных операторов
// Интерфейс стратегии
interface PaymentStrategyInterface
{
public function pay(float $amount): PaymentResult;
public function getName(): string;
}
// Стратегия оплаты картой
class CreditCardPayment implements PaymentStrategyInterface
{
public function __construct(
private string $cardNumber,
private string $cvv,
) {}
public function pay(float $amount): PaymentResult
{
// Логика оплаты картой
return new PaymentResult(
success: true,
transactionId: uniqid('card_'),
);
}
public function getName(): string
{
return 'Credit Card';
}
}
// Стратегия оплаты PayPal
class PayPalPayment implements PaymentStrategyInterface
{
public function __construct(
private string $email,
) {}
public function pay(float $amount): PaymentResult
{
// Логика оплаты через PayPal
return new PaymentResult(
success: true,
transactionId: uniqid('pp_'),
);
}
public function getName(): string
{
return 'PayPal';
}
}
// Стратегия оплаты криптовалютой
class CryptoPayment implements PaymentStrategyInterface
{
public function __construct(
private string $walletAddress,
) {}
public function pay(float $amount): PaymentResult
{
// Логика оплаты криптой
return new PaymentResult(
success: true,
transactionId: uniqid('crypto_'),
);
}
public function getName(): string
{
return 'Cryptocurrency';
}
}
// Контекст, использующий стратегию
class PaymentProcessor
{
private ?PaymentStrategyInterface $strategy = null;
public function setStrategy(PaymentStrategyInterface $strategy): void
{
$this->strategy = $strategy;
}
public function processPayment(float $amount): PaymentResult
{
if ($this->strategy === null) {
throw new RuntimeException('Payment strategy not set');
}
echo "Processing payment via {$this->strategy->getName()}...\n";
return $this->strategy->pay($amount);
}
}
// Использование
$processor = new PaymentProcessor();
// Оплата картой
$processor->setStrategy(new CreditCardPayment('4111...', '123'));
$result1 = $processor->processPayment(99.99);
// Переключаемся на PayPal
$processor->setStrategy(new PayPalPayment('user@email.com'));
$result2 = $processor->processPayment(49.99);
Factory — фабрика
Factory — создаёт объекты без указания конкретных классов. Инкапсулирует логику создания объектов в одном месте.
Когда использовать:
- Создание объекта требует сложной логики
- Тип объекта определяется в рантайме
- Хотите централизовать создание объектов
// Интерфейс продукта
interface NotificationInterface
{
public function send(string $message): void;
}
// Конкретные реализации
class EmailNotification implements NotificationInterface
{
public function __construct(
private string $email,
) {}
public function send(string $message): void
{
echo "Sending email to {$this->email}: {$message}\n";
}
}
class SmsNotification implements NotificationInterface
{
public function __construct(
private string $phone,
) {}
public function send(string $message): void
{
echo "Sending SMS to {$this->phone}: {$message}\n";
}
}
class TelegramNotification implements NotificationInterface
{
public function __construct(
private string $chatId,
) {}
public function send(string $message): void
{
echo "Sending Telegram to {$this->chatId}: {$message}\n";
}
}
// Фабрика уведомлений
class NotificationFactory
{
public static function create(
string $type,
string $recipient,
): NotificationInterface {
return match ($type) {
'email' => new EmailNotification($recipient),
'sms' => new SmsNotification($recipient),
'telegram' => new TelegramNotification($recipient),
default => throw new InvalidArgumentException(
"Unknown notification type: {$type}"
),
};
}
}
// Использование
$notification = NotificationFactory::create('email', 'user@test.com');
$notification->send('Hello!');
$notification = NotificationFactory::create('telegram', '123456789');
$notification->send('Hello from Telegram!');
Singleton — одиночка
Singleton — гарантирует существование только одного экземпляра класса и предоставляет глобальную точку доступа к нему.
Singleton считается анти-паттерном в современной разработке. Он усложняет тестирование и создаёт скрытые зависимости. Лучше используйте Dependency Injection.
class Database
{
private static ?self $instance = null;
private PDO $connection;
// Приватный конструктор — нельзя создать new Database()
private function __construct()
{
$this->connection = new PDO('mysql:host=localhost;dbname=test');
}
// Запрещаем клонирование
private function __clone() {}
// Получение единственного экземпляра
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function query(string $sql): array
{
return $this->connection->query($sql)->fetchAll();
}
}
// Использование
$db = Database::getInstance();
$users = $db->query('SELECT * FROM users');
// Вместо Singleton используйте DI-контейнер
// Laravel автоматически создаст один экземпляр
// В ServiceProvider
$this->app->singleton(Database::class, function () {
return new Database(config('database.default'));
});
// Внедряем через конструктор
class UserRepository
{
public function __construct(
private Database $db, // Автоматически внедрится
) {}
}
Observer — наблюдатель
Observer — определяет зависимость «один ко многим», при которой изменение состояния одного объекта автоматически уведомляет всех зависимых.
Когда использовать:
- События и подписки
- Реактивное обновление UI
- Логирование, аудит действий
- Отправка уведомлений при изменениях
// Интерфейс наблюдателя
interface ObserverInterface
{
public function update(string $event, mixed $data): void;
}
// Интерфейс субъекта (наблюдаемого)
interface SubjectInterface
{
public function attach(ObserverInterface $observer): void;
public function detach(ObserverInterface $observer): void;
public function notify(string $event, mixed $data): void;
}
// Реализация субъекта
class Order implements SubjectInterface
{
private array $observers = [];
private string $status = 'new';
public function attach(ObserverInterface $observer): void
{
$this->observers[] = $observer;
}
public function detach(ObserverInterface $observer): void
{
$this->observers = array_filter(
$this->observers,
fn($o) => $o !== $observer
);
}
public function notify(string $event, mixed $data): void
{
foreach ($this->observers as $observer) {
$observer->update($event, $data);
}
}
public function setStatus(string $status): void
{
$oldStatus = $this->status;
$this->status = $status;
// Уведомляем всех наблюдателей
$this->notify('status_changed', [
'old' => $oldStatus,
'new' => $status,
'order' => $this,
]);
}
}
// Наблюдатель: отправка email
class EmailNotificationObserver implements ObserverInterface
{
public function update(string $event, mixed $data): void
{
if ($event === 'status_changed') {
echo "📧 Sending email: Order status changed to {$data['new']}\n";
}
}
}
// Наблюдатель: логирование
class LoggingObserver implements ObserverInterface
{
public function update(string $event, mixed $data): void
{
echo "📝 Log: Event '{$event}' triggered\n";
}
}
// Наблюдатель: аналитика
class AnalyticsObserver implements ObserverInterface
{
public function update(string $event, mixed $data): void
{
echo "📊 Analytics: Tracking event '{$event}'\n";
}
}
// Использование
$order = new Order();
// Подписываем наблюдателей
$order->attach(new EmailNotificationObserver());
$order->attach(new LoggingObserver());
$order->attach(new AnalyticsObserver());
// Изменяем статус — все наблюдатели получат уведомление
$order->setStatus('paid');
// Вывод:
// 📧 Sending email: Order status changed to paid
// 📝 Log: Event 'status_changed' triggered
// 📊 Analytics: Tracking event 'status_changed'
Итоги и рекомендации
Что запомнить
SOLID
5 принципов для гибкой архитектуры
DRY
Не дублируйте знания в коде
KISS
Простота — залог поддерживаемости
Паттерны
Готовые решения типовых задач
Практические советы
| Принцип / Паттерн | Когда применять | Когда НЕ применять |
|---|---|---|
| SOLID | Долгосрочные проекты, команды, сложная логика | Прототипы, одноразовые скрипты |
| DRY | Повторяющаяся бизнес-логика | Случайное сходство кода |
| KISS | Всегда | Никогда не пренебрегайте |
| Repository | Работа с данными, тестирование | Простые CRUD без сложной логики |
| DTO | API, передача между слоями | Внутренние методы одного класса |
| Strategy | Несколько алгоритмов, выбор в рантайме | Один фиксированный алгоритм |
| Factory | Сложное создание объектов | Простые объекты с new |
| Observer | События, уведомления, реактивность | Синхронные простые операции |
Принципы и паттерны — это инструменты, а не цели. Используйте их, когда они решают реальную проблему, а не ради самих себя. Лучший код — тот, который легко читать и изменять.
Что изучить дальше
- Clean Architecture — Роберт Мартин
- Domain-Driven Design — Эрик Эванс
- Паттерны GoF — «Банда четырёх»
- Рефакторинг — Мартин Фаулер