Какие принципы и практики применяются при разработке нового поддерживаемого кода?

Ответ

Разработка нового кода с расчетом на долгосрочную поддержку строится на комбинации принципов проектирования, стандартов кода и инженерных практик.

1. Принципы проектирования (SOLID, DRY, YAGNI):

// Пример соблюдения SRP (Single Responsibility) и Dependency Injection
@Service
public class OrderService {
    private final PaymentProcessor paymentProcessor; // Зависимость через интерфейс
    private final NotificationService notificationService;

    // Внедрение зависимостей через конструктор
    public OrderService(PaymentProcessor processor, NotificationService notifier) {
        this.paymentProcessor = processor;
        this.notificationService = notifier;
    }

    public OrderResult placeOrder(OrderRequest request) {
        // Каждый сервис отвечает за одну зону ответственности
        validate(request);
        PaymentReceipt receipt = paymentProcessor.charge(request);
        notificationService.sendOrderConfirmation(request.getUserEmail(), receipt);
        return new OrderResult(receipt);
    }
}

2. Инженерные практики:

  • Test-Driven Development (TDD): Сначала пишется падающий тест, затем — минимальная реализация, потом рефакторинг.
  • Покрытие unit-тестами: Использование JUnit 5, Mockito для изоляции тестируемого модуля.

    @Test
    void placeOrder_ShouldChargePaymentAndSendNotification() {
    // Arrange
    PaymentProcessor mockProcessor = mock(PaymentProcessor.class);
    NotificationService mockNotifier = mock(NotificationService.class);
    OrderService service = new OrderService(mockProcessor, mockNotifier);
    OrderRequest testRequest = new OrderRequest("test@mail.com", 100.0);
    
    when(mockProcessor.charge(testRequest)).thenReturn(new PaymentReceipt("123"));
    
    // Act
    OrderResult result = service.placeOrder(testRequest);
    
    // Assert
    verify(mockProcessor).charge(testRequest);
    verify(mockNotifier).sendOrderConfirmation("test@mail.com", any());
    assertThat(result.getReceiptId()).isEqualTo("123");
    }
  • Согласованный стиль кода: Следование правилам именования, форматирования (используя Checkstyle, Spotless).
  • Ревью кода: Обязательный peer review перед мержем для обмена знаниями и контроля качества.

Ответ 18+ 🔞

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

1. Эти ваши принципы, SOLID там всякие.

Ну, типа, чтобы не получилась одна большая функция-монстр, которая делает всё: и деньги берёт, и письма шлёт, и в БД пишет, и тебе кофе варит. Это пиздец как неудобно потом, когда надо поменять, как именно деньги берут. Представь, блядь, приходит новый закон, и надо не просто списать, а ещё и в налоговую отчёт высрать. А у тебя всё в одном методе placeOrder, который уже на 500 строк раздуло. Ты его тронешь — всё развалится, как карточный домик от чиха.

Вот смотри, в примере-то всё красиво разложено. OrderService только оркестрирует процесс. Платежи — это отдельная сущность, PaymentProcessor. Уведомления — это NotificationService. Каждый, сука, отвечает за свой огород. Это и есть SRP — принцип единственной ответственности. Не пытайся быть и швецом, и жнецом, а то получишь пиздопроебибну архитектуру.

И смотри, как зависимости внедряются — через конструктор. Это называется Dependency Injection. Раньше бы написали new PaymentProcessor() прямо внутри класса, и привязались бы намертво к одной конкретной реализации. А теперь мы говорим: «Да мне похуй, какой именно процессор, лишь бы он интерфейс PaymentProcessor реализовывал». Захотел поменять платёжку с Stripe на PayPal? Подсунул другую реализацию, и основной бизнес-логины даже не чихнул. Красота, ёпта.

// Пример соблюдения SRP (Single Responsibility) и Dependency Injection
@Service
public class OrderService {
    private final PaymentProcessor paymentProcessor; // Зависимость через интерфейс
    private final NotificationService notificationService;

    // Внедрение зависимостей через конструктор
    public OrderService(PaymentProcessor processor, NotificationService notifier) {
        this.paymentProcessor = processor;
        this.notificationService = notifier;
    }

    public OrderResult placeOrder(OrderRequest request) {
        // Каждый сервис отвечает за одну зону ответственности
        validate(request);
        PaymentReceipt receipt = paymentProcessor.charge(request);
        notificationService.sendOrderConfirmation(request.getUserEmail(), receipt);
        return new OrderResult(receipt);
    }
}

2. Инженерные практики, без которых — просто пиздец.

  • TDD (Test-Driven Development). Это когда ты сначала пишешь тест на то, чего ещё нет. Звучит, как бред шизофреника, но, блядь, работает. Ты описываешь, как система должна себя вести. Потом запускаешь тест — он, естественно, падает, потому что кода нет. А потом пишешь самый простой и тупой код, чтобы тест прошёл. И только потом рефакторишь, делаешь красиво. Это заставляет думать о дизайне API сразу, а не как обычно: накодил гору, а потом охуеваешь, как это тестировать.
  • Покрытие unit-тестами. Это святое. Без этого — ты ходишь по охуенно тонкому льду с грузовиком говна за спиной. Каждый чих в коде — и ты не знаешь, что сломалось. Вот смотри, как тестируется наш сервис:
@Test
void placeOrder_ShouldChargePaymentAndSendNotification() {
    // Arrange (Подготовка)
    // Создаём моки — заглушки для зависимостей. Это не реальные процессоры, а куклы.
    PaymentProcessor mockProcessor = mock(PaymentProcessor.class);
    NotificationService mockNotifier = mock(NotificationService.class);
    // Подсовываем эти моки в наш сервис.
    OrderService service = new OrderService(mockProcessor, mockNotifier);
    OrderRequest testRequest = new OrderRequest("test@mail.com", 100.0);

    // Говорим моку процессора: «Когда у тебя вызовут charge с этим request, верни вот такой ресипт».
    when(mockProcessor.charge(testRequest)).thenReturn(new PaymentReceipt("123"));

    // Act (Действие)
    // Вызываем метод, который тестируем.
    OrderResult result = service.placeOrder(testRequest);

    // Assert (Проверка)
    // Проверяем, что мок процессора действительно был вызван с нужными аргументами.
    verify(mockProcessor).charge(testRequest);
    // Проверяем, что мок нотификатора тоже вызвали.
    verify(mockNotifier).sendOrderConfirmation("test@mail.com", any());
    // И проверяем результат.
    assertThat(result.getReceiptId()).isEqualTo("123");
}

Видишь магию? Мы не поднимаем всю банду — базу данных, почтовый сервер, платёжный шлюз. Мы тестируем только логику OrderService: что он правильно координирует вызовы других сервисов. Это быстро, это надёжно, это изолированно.

  • Стиль кода. Да, это скучно, как хуй в пальто, но необходимо. Один пишет с отступами в 2 пробела, другой — в 4, третий табами. В итоге в гите одна сплошная война изменений, где половина коммитов — это «поправил форматирование». Настраиваешь Checkstyle или Spotless, и пусть машина сама приводит всё к одному виду. Читаемость, блядь, важна.
  • Ревью кода. Это, пожалуй, главное. Один человек — это слепая зона. Он мог накосячить в самом неожиданном месте, а сам даже не заметит. Вторые, третьи глаза видят то, что ты уже перестал замечать. Плюс, это обмен знаниями: все в команде понимают, что и как пишется. Без ревью мержить — это как играть в русскую рулетку с автоматом Калашникова. Рано или поздно прилетит так, что мало не покажется.