Маленькие функции и один смысл
Маленькие функции и один смысл
Функция должна делать одну вещь. Если она делает две - это уже две функции.
Это не прихоть - это самый эффективный способ контролировать сложность. Маленькую функцию проще прочитать, протестировать, переиспользовать и заменить. На уровне модулей этот же принцип называется SRP - Single Responsibility.
Признаки «слишком большой функции»
Сигнал Что делать
────────────────────────────────────── ──────────────────────────
В ней есть «и ещё вот это» Вынести в отдельную функцию
Больше 40-60 строк Разбить на шаги
Много уровней if/for Guard clauses + вынос
Она и валидирует, и сохраняет, Каждое действие - отдельно
и шлёт уведомления
Нужен комментарий-разделитель Вместо «// шаг 1» - функция
«// шаг 1», «// шаг 2» с понятным именем
Плохой пример: монолитная функция
func RegisterUser(ctx context.Context, req RegisterRequest) error {
// валидация
if req.Email == "" {
return errors.New("email required")
}
if len(req.Password) < 8 {
return errors.New("password too short")
}
// хеширование
hash, err := bcrypt.GenerateFromPassword(
[]byte(req.Password), bcrypt.DefaultCost,
)
if err != nil {
return fmt.Errorf("hash password: %w", err)
}
// сохранение
user := User{Email: req.Email, PasswordHash: string(hash)}
if err := db.Create(&user).Error; err != nil {
return fmt.Errorf("create user: %w", err)
}
// письмо
msg := fmt.Sprintf("Welcome, %s!", req.Email)
if err := smtp.Send(req.Email, "Welcome", msg); err != nil {
log.Printf("failed to send welcome email: %v", err)
}
// аналитика
analytics.Track("user_registered", map[string]string{
"email": req.Email,
})
return nil
}
public function registerUser(RegisterRequest $req): void {
// валидация
if ($req->email === '') {
throw new InvalidArgumentException('email required');
}
if (strlen($req->password) < 8) {
throw new InvalidArgumentException('password too short');
}
// хеширование
$hash = password_hash($req->password, PASSWORD_BCRYPT);
// сохранение
$this->db->prepare('INSERT INTO users(email, password_hash) VALUES (?, ?)')
->execute([$req->email, $hash]);
// письмо
try {
$this->mailer->send($req->email, 'Welcome', "Welcome, {$req->email}!");
} catch (Throwable $e) {
$this->logger->warning('welcome email failed', ['err' => $e->getMessage()]);
}
// аналитика
$this->analytics->track('user_registered', ['email' => $req->email]);
}
async function registerUser(req) {
// валидация
if (!req.email) {
throw new Error('email required');
}
if ((req.password ?? '').length < 8) {
throw new Error('password too short');
}
// хеширование
const hash = await bcrypt.hash(req.password, 10);
// сохранение
const user = await db.users.create({
data: { email: req.email, passwordHash: hash },
});
// письмо
try {
await mailer.send(req.email, 'Welcome', `Welcome, ${req.email}!`);
} catch (e) {
logger.warn('welcome email failed', { err: e?.message });
}
// аналитика
analytics.track('user_registered', { email: req.email });
return user;
}
В JS та же история: функция валидирует, хеширует, сохраняет, шлёт письмо и трекает - пять обязанностей в одном async-методе.
Эта функция делает пять разных вещей. Если сломается отправка письма - ты будешь дебажить 40-строчную функцию вместо 5-строчной.
Лучше: оркестрация + делегирование
func RegisterUser(ctx context.Context, req RegisterRequest) error {
if err := validateRegistration(req); err != nil {
return err
}
user, err := createUser(ctx, req)
if err != nil {
return err
}
sendWelcomeEmail(user)
trackRegistration(user)
return nil
}
public function registerUser(RegisterRequest $req): void
{
$this->validateRegistration($req);
$user = $this->createUser($req);
$this->sendWelcomeEmail($user);
$this->trackRegistration($user);
}
async function registerUser(req) {
validateRegistration(req);
const user = await createUser(req);
await sendWelcomeEmail(user);
trackRegistration(user);
return user;
}
Функция превратилась в оркестратор: каждая строка - отдельный шаг с осмысленным именем, легко мокается и тестируется.
Теперь registerUser читается как оглавление. Каждый шаг - отдельная функция, которую можно тестировать независимо.
Правило одного уровня абстракции
Внутри функции все операции должны быть на одном уровне абстракции. Нельзя мешать бизнес-логику с деталями реализации:
// Плохо: мешаем уровни абстракции
func PlaceOrder(ctx context.Context, order Order) error {
if order.Total() > 0 { // бизнес-логика
tx := db.Begin() // детали БД
if err := tx.Create(&order).Error; // детали ORM
err != nil {
tx.Rollback()
return err
}
tx.Commit()
smtp.Send(order.Email, "Order placed", // детали SMTP
fmt.Sprintf("Order #%d", order.ID))
}
return nil
}
// Хорошо: один уровень абстракции
func PlaceOrder(ctx context.Context, order Order) error {
if err := validateOrder(order); err != nil {
return err
}
if err := orderRepo.Save(ctx, order); err != nil {
return fmt.Errorf("save order: %w", err)
}
notifier.OrderPlaced(ctx, order)
return nil
}
<?php
declare(strict_types=1);
// Плохо: смешаны бизнес-логика, Doctrine и SMTP в одном методе
final class BadOrderService
{
public function placeOrder(Order $order): void
{
if ($order->total() > 0) { // бизнес-логика
$this->em->getConnection()->beginTransaction(); // детали Doctrine
try {
$this->em->persist($order); // детали ORM
$this->em->flush();
$this->em->getConnection()->commit();
} catch (\Throwable $e) {
$this->em->getConnection()->rollBack();
throw $e;
}
$this->mailer->send( // детали SMTP
$order->email(),
'Order placed',
'Order #' . $order->id(),
);
}
}
}
// Хорошо: один уровень абстракции - оркестратор
final class OrderService
{
public function __construct(
private readonly OrderValidator $validator,
private readonly OrderRepository $orders,
private readonly OrderNotifier $notifier,
) {}
public function placeOrder(Order $order): void
{
$this->validator->validate($order);
$this->orders->save($order);
$this->notifier->orderPlaced($order);
}
}
Extract Method: механика выноса
Когда видишь блок с комментарием-разделителем - это сигнал для Extract Method:
// До: комментарий-разделитель = скрытая функция
func ProcessPayment(ctx context.Context, p Payment) error {
// - validate ---
if p.Amount <= 0 {
return ErrInvalidAmount
}
if p.Currency == "" {
p.Currency = "RUB"
}
// - charge ---
resp, err := gateway.Charge(p.Amount, p.Currency)
if err != nil {
return fmt.Errorf("charge: %w", err)
}
// - save receipt ---
receipt := Receipt{PaymentID: p.ID, GatewayRef: resp.Ref}
return receiptRepo.Save(ctx, receipt)
}
// После: каждый блок стал функцией
func ProcessPayment(ctx context.Context, p Payment) error {
p, err := normalizePayment(p)
if err != nil {
return err
}
resp, err := chargePayment(p)
if err != nil {
return err
}
return saveReceipt(ctx, p.ID, resp.Ref)
}
<?php
declare(strict_types=1);
// До: блоки с комментариями = скрытые методы
final class BadPaymentService
{
public function processPayment(Payment $p): void
{
// --- validate ---
if ($p->amount <= 0) {
throw new \InvalidArgumentException('amount must be positive');
}
if ($p->currency === '') {
$p->currency = 'RUB';
}
// --- charge ---
$resp = $this->gateway->charge($p->amount, $p->currency);
// --- save receipt ---
$receipt = new Receipt($p->id, $resp->ref);
$this->receipts->save($receipt);
}
}
// После: каждый блок - приватный метод с осмысленным именем
final class PaymentService
{
public function processPayment(Payment $p): void
{
$p = $this->normalizePayment($p);
$resp = $this->chargePayment($p);
$this->saveReceipt($p->id, $resp->ref);
}
private function normalizePayment(Payment $p): Payment
{
if ($p->amount <= 0) {
throw new \InvalidArgumentException('amount must be positive');
}
return $p->withCurrency($p->currency ?: 'RUB');
}
}
Цикломатическая сложность
Цикломатическая сложность (CC) - количество независимых путей через функцию. Каждый if, for, case, &&, || добавляет +1.
CC Уровень Что делать
──── ────────────── ────────────────────────────
1-5 Простая Всё хорошо
6-10 Умеренная Подумай о разбиении
11-20 Сложная Разбей обязательно
21+ Очень сложная Срочный рефакторинг
В Go golangci-lint проверяет CC автоматически:
# .golangci.yml
linters-settings:
cyclop:
max-complexity: 10
Сколько параметров у функции?
Идеально - 0-2. Три - допустимо. Больше трёх - сигнал:
// Плохо: 5 параметров, легко перепутать порядок
func CreateUser(name, email, phone string, age int, active bool) error
// Лучше: структура-параметр
type CreateUserInput struct {
Name string
Email string
Phone string
Age int
Active bool
}
func CreateUser(input CreateUserInput) error
<?php
declare(strict_types=1);
// Плохо: 5 позиционных параметров - легко перепутать
final class BadUserService
{
public function createUser(
string $name,
string $email,
string $phone,
int $age,
bool $active,
): void {
// ...
}
}
// Вызов: createUser('Alice', 'a@x.io', '+7...', 25, true) - порядок критичен
// Лучше: readonly DTO с именованными аргументами (named arguments PHP 8.0+)
final readonly class CreateUserInput
{
public function __construct(
public string $name,
public string $email,
public string $phone,
public int $age,
public bool $active,
) {}
}
final class UserService
{
public function createUser(CreateUserInput $input): void
{
// ...
}
}
// Вызов с именованными аргументами - порядок не важен:
$svc->createUser(new CreateUserInput(
name: 'Alice',
email: 'a@example.com',
phone: '+7...',
age: 25,
active: true,
));
Мини-задание
- Найди функцию > 60 строк в своём проекте
- Определи, сколько «обязанностей» она выполняет
- Вынеси 2-3 блока в отдельные функции (Extract Method)
- Проверь: читается ли основная функция как оглавление?