📚 Руководство

Принципы SOLID, DRY, KISS и паттерны проектирования

Введение: зачем нужны принципы проектирования

Когда вы пишете код, который работает — это хорошо. Но когда вы пишете код, который легко читать, изменять и поддерживать — это профессионально.

Принципы проектирования — это не догмы, а ориентиры. Они помогают принимать решения при написании кода и избегать типичных ошибок архитектуры.

💡 Важно понимать

Принципы — не законы. Иногда их нарушение оправдано. Главное — понимать, почему вы это делаете и какие последствия это несёт.

В этой статье разберём:

  • SOLID — 5 принципов объектно-ориентированного дизайна
  • DRY — не повторяйся
  • KISS — делай проще
  • Паттерны — готовые решения типовых задач

Принципы SOLID

SOLID — это аббревиатура из пяти принципов, сформулированных Робертом Мартином (Uncle Bob). Они помогают создавать гибкие, масштабируемые и поддерживаемые системы.

S

Single Responsibility Principle

Принцип единственной ответственности
Класс должен иметь только одну причину для изменения.
Каждый класс отвечает за одну конкретную задачу.

❌ Плохо: класс делает всё

PHP Нарушение SRP
class User
{
    public function save(): void
    {
        // Сохранение в базу данных
    }

    public function sendEmail(): void
    {
        // Отправка email
    }

    public function generateReport(): string
    {
        // Генерация отчёта
    }

    public function validateData(): bool
    {
        // Валидация
    }
}

✅ Хорошо: разделение ответственности

PHP Соблюдение SRP
// Сущность — только данные
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 { }
}
O

Open/Closed Principle

Принцип открытости/закрытости
Открыт для расширения, закрыт для модификации.
Добавляйте новый функционал без изменения существующего кода.

❌ Плохо: изменяем класс при каждом новом типе

PHP Нарушение OCP
class PaymentProcessor
{
    public function process(string $type, float $amount): void
    {
        if ($type === 'card') {
            // Оплата картой
        } elseif ($type === 'paypal') {
            // Оплата PayPal
        } elseif ($type === 'crypto') {
            // Каждый новый способ = изменение класса
        }
    }
}

✅ Хорошо: расширяем через интерфейсы

PHP Соблюдение OCP
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);
    }
}
L

Liskov Substitution Principle

Принцип подстановки Барбары Лисков
Подклассы должны дополнять, а не изменять поведение базового класса.
Объект подкласса можно использовать везде, где ожидается объект родителя.

❌ Плохо: подкласс меняет поведение

PHP Нарушение LSP
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; // Неожиданное поведение!
    }
}

✅ Хорошо: общий интерфейс без наследования

PHP Соблюдение LSP
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;
    }
}
I

Interface Segregation Principle

Принцип разделения интерфейсов
Много специализированных интерфейсов лучше одного универсального.
Клиенты не должны зависеть от методов, которые они не используют.

❌ Плохо: один огромный интерфейс

PHP Нарушение ISP
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'); // Костыль!
    }
}

✅ Хорошо: разделённые интерфейсы

PHP Соблюдение ISP
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 { }
}
D

Dependency Inversion Principle

Принцип инверсии зависимостей
Зависьте от абстракций, а не от конкретных реализаций.
Высокоуровневые модули не должны зависеть от низкоуровневых.

❌ Плохо: жёсткая зависимость от реализации

PHP Нарушение DIP
class MySQLDatabase
{
    public function query(string $sql): array { }
}

// Жёсткая привязка к MySQL
class UserRepository
{
    private MySQLDatabase $db;

    public function __construct()
    {
        $this->db = new MySQLDatabase(); // Проблема!
    }
}

✅ Хорошо: зависимость от абстракции

PHP Соблюдение DIP
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

D

Don't Repeat Yourself

Не повторяйся
Каждый фрагмент знания должен иметь единственное представление в системе.
Избегайте дублирования логики, данных и кода.

❌ Плохо: дублирование логики

PHP Нарушение DRY
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');
        }
    }
}

✅ Хорошо: единый источник истины

PHP Соблюдение DRY
// Один класс для валидации
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);
    }
}
⚠️ Осторожно с DRY

Не путайте дублирование кода с дублированием знаний. Два похожих куска кода могут меняться по разным причинам — тогда их объединение навредит. Это называется случайное дублирование.

Принцип KISS

K

Keep It Simple, Stupid

Делай проще, дурачок
Простота — главная цель проектирования.
Избегайте ненужной сложности. Пишите код, который легко понять.

❌ Плохо: переусложнение

PHP Нарушение KISS
// Зачем так сложно?
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');

✅ Хорошо: просто и понятно

PHP Соблюдение KISS
// Просто используем встроенную функцию
$result = strtoupper('hello');

// Или если нужна абстракция — делаем минимально
class StringHelper
{
    public static function toUpper(string $value): string
    {
        return strtoupper($value);
    }
}
💡 Правило KISS

Если вы не можете объяснить код коллеге за 30 секунд — возможно, его стоит упростить.

Принцип YAGNI

D

You Ain't Gonna Need It

вам это не понадобится
Этот принцип проектирования ПО гласит: не добавляйте функциональность до тех пор, пока в ней не возникнет реальной необходимости
Программисты часто пытаются предугадать будущее и пишут код «про запас» (универсальные интерфейсы, дополнительные поля в базе, сложные иерархии классов), думая: «А вдруг завтра заказчик захочет еще и это?» YAGNI говорит: не делайте этого.
⚠️ Плохо (Нарушение YAGNI)

Вы создаете систему регистрации пользователей и сразу добавляете возможность входа через 10 разных соцсетей и поддержку 5 языков, хотя заказчик просил просто «вход по email».

YAGNI идет рука об руку с принципом KISS (Keep It Simple, Stupid — «делай проще»). Если KISS призывает не усложнять текущую задачу, то YAGNI призывает вообще не браться за лишние задачи.

Паттерны проектирования

Паттерны — это проверенные решения типовых задач проектирования. Они не изобретают велосипед, а дают готовые архитектурные шаблоны.

📦

Repository

Абстракция над доступом к данным. Скрывает детали хранилища.

📋

DTO

Объект для передачи данных между слоями без логики.

🎯

Strategy

Семейство алгоритмов, взаимозаменяемых в рантайме.

🏭

Factory

Создание объектов без указания конкретных классов.

1️⃣

Singleton

Гарантирует один экземпляр класса на всё приложение.

👀

Observer

Оповещение подписчиков об изменениях в объекте.

Repository — репозиторий

Repository — это прослойка между бизнес-логикой и источником данных. Он инкапсулирует логику доступа к данным и предоставляет коллекционно-подобный интерфейс.

Когда использовать:

  • Хотите абстрагироваться от конкретной БД
  • Нужно тестировать код без реальной базы
  • Хотите централизовать логику запросов
PHP Repository Pattern
// Интерфейс репозитория
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
  • Валидация входных данных
  • Отделение внутренней модели от внешнего представления
PHP DTO Pattern
// 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 — позволяет определить семейство алгоритмов, инкапсулировать каждый из них и сделать взаимозаменяемыми.

Когда использовать:

  • Есть несколько вариантов алгоритма
  • Нужно переключаться между алгоритмами в рантайме
  • Хотите избежать множества условных операторов
PHP Strategy Pattern
// Интерфейс стратегии
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 — создаёт объекты без указания конкретных классов. Инкапсулирует логику создания объектов в одном месте.

Когда использовать:

  • Создание объекта требует сложной логики
  • Тип объекта определяется в рантайме
  • Хотите централизовать создание объектов
PHP Factory Pattern
// Интерфейс продукта
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

Singleton считается анти-паттерном в современной разработке. Он усложняет тестирование и создаёт скрытые зависимости. Лучше используйте Dependency Injection.

PHP Singleton (не рекомендуется)
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');
PHP Лучше: DI Container
// Вместо 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
  • Логирование, аудит действий
  • Отправка уведомлений при изменениях
PHP Observer Pattern
// Интерфейс наблюдателя
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 — «Банда четырёх»
  • Рефакторинг — Мартин Фаулер
👨‍💻

Автор статьи

Fullstack-разработчик. Пишу о PHP, Laravel, архитектуре и чистом коде.

📱 Telegram: @rexzavr