Когда проект начинается, разработчику хочется только одного — свободы творчества. Писать код быстро и так же быстро получать результат. Но со временем появляются баги, регрессии, архитектура, миграции... И вот тогда свобода оборачивается болью: оказывается, что один и тот же результат в коде можно выразить десятком способов — и все они несовместимы друг с другом.

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

Один путь лучше, чем два

Именно поэтому в больших командах ценятся языки, которые ограничивают вариативность и задают четкие правила. На протяжении многих лет корпоративная разработка опиралась на языки, которые минимизируют свободу трактовки кода. Яркий представитель этого подхода — Java.

Java

Java — один из немногих языков, который не позволяет сильно отклоняться от «канонического» способа написания кода. Это делает код громоздким, но в то же время снижает вариативность. И в этом основная сила Java, потому что разработчики не тратят время на то, чтобы выработать общий стандарт оформления кода, что критично для масштабных корпоративных проектов.

Предположим, мы хотим написать метод на Java, который проверяет число на чётность.

public class Example {
    public boolean isEven(int x) {
        return x % 2 == 0;
    }
}

Вы не можете пропустить здесь ни одно ключевое слово. Вы должны указать тип возвращаемого значения (даже если метод ничего не возвращает), тип параметра и обязательно написать хотя бы один return.

Однако не все современные языки настолько категоричны в вопросах синтаксиса. Например, Kotlin, который тесно связан с Java, предлагает гораздо больше свободы при написании кода.

Kotlin

Kotlin — родственный Java язык и даже совместим с ней на уровне байт-кода. Мы можем написать целых 3 варианта одного и того же метода, которые будут полностью эквивалентны друг другу:

fun isEven(x: Int) = x % 2 == 0

// то же самое
fun isEven(x: Int): Boolean = x % 2 == 0

// то же самое
fun isEven(x: Int): Boolean {
    return x % 2 == 0
}

Если в команде больше одного разработчика, уже есть риск, что они будут писать методы в разном стиле. То есть даже в таком банальном примере возникает необходимость в выработке единого стиля, а также контроль за выполнением этих договорённостей на code review.

Я уж не говорю, что можно эти же методы оформить в виде методов расширения и получить еще 3 варианта.

Как видно, даже в таких родственных языках, как Kotlin и Java, подходы к написанию кода могут существенно различаться. В строгом языке Java у вас остается некоторая свобода выбора. Вы, например, можете написать весь код в одну строку. Синтаксис языка позволяет так делать, но читаемость от этого, мягко говоря, пострадает.

Python


Если же взглянуть на языки с еще более гибким синтаксисом, например, Python, ситуация становится менее строгой и более непредсказуемой. Python хоть и не требует указания типов, однако требует правильного форматирования вместо использования фигурных скобок.

"Должен быть один — и, желательно, только один — очевидный способ сделать это." (c) Zen of Python

def is_even(x):
    return x % 2 == 0

То есть данный код будет отформатирован любым разработчиком абсолютно одинаково. Иначе он просто не будет работать.

Конечно же, всегда остается место для холивара «табы против пробелов», но давайте считать это культурным багажом индустрии.

Таким образом, форматирование — лишь одна сторона вопроса. Не менее важную роль в поддержании порядка в больших командах играет система типов, которая определяет, насколько предсказуемо и безопасно ведёт себя код.

Статическая типизация лучше динамической

Чем строже типизация — тем больше кода приходится писать. Однако это с лихвой окупается тем, что вы легко можете выполнять рефакторинг любой сложности. Сам факт того, что проект после этого успешно компилируется, уже на 80% позволяет утверждать, что рефакторинг ничего не ломает.

Давайте вернемся к методу определения чётности на Java. Если я вместо boolean захочу возвращать строку в качестве признака чётности, то в точке вызова этого метода код просто не скомпилируется. Это позволяет всегда держать код согласованным.

public class Example {
    public static String isEven(int x) {
        if (x % 2 == 0) {
            return "чётное";
        }
        return "нечётное";
    }
  
    public static void main(String[] args) {
        boolean isEven = isEven(234); // не скомпилируется, т.к. ожидаем boolean
        System.out.println(isEven);
    }
}

В то же время в Python такой «рефакторинг» может остаться незамеченным.

# def is_even(x):
#     return x % 2 == 0
def is_even(n):
    return "да" if n % 2 == 0 else "нет"

is_even = is_even(234) # ошибок нет, хотя мы возвращаем уже другие данные
print(is_even)

Поэтому все языки, в которых изначально была динамическая типизация (Javascript, Python, Groovy, PHP), постепенно обзавелись возможностью указывать типы там, где это необходимо. И это снова порождает необходимость договариваться внутри команды, в каком стиле нужно оформлять код.
Это стремление к большей определенности и безопасности повлекло создание новых инструментов и языков, расширяющих возможности динамически типизированных платформ. В случае с Javascript это вообще привело к появлению отдельного языка под названием Typescript.

В Java, кстати, тоже можно добиться подобного поведения, если злоупотреблять ключевым словом var.

public static void main(String[] args) {
    // компилируется с любым типом, который будет возвращать isEven()
    var isEven = isEven(234);
    System.out.println(isEven);
}

Поэтому var, появившийся в Java 10, был неоднозначно принят сообществом. Хотя изначально его задачей было снизить многословность Java, особенно для составных объектов.

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

Аккуратно работаем с параметрами по умолчанию

Java не позволяет делать значения параметров по умолчанию в сигнатуре метода. Поэтому, если нужно добавить новый параметр, вам придётся либо делать новый метод, либо править ВСЕ вызовы данного метода в вашем проекте.

public class Example {
    public static void doSomething(int a, int b) {
        // ...
    }

    public static void main(String[] args) {
        // было:
        // doSomething(1);
        // стало после добавления второго параметра:
        doSomething(1, 2);
    }
}

В то же время многие современные языки (Kotlin, Python и т.д.) позволяют задавать значения по умолчанию. Если сделать это сразу, не проверив все точки вызова, легко допустить ошибку.

# было:
# def do_something(a):
#     print(a)

# стало:
def do_something(a, b = 3): # добавили новый параметр со значением по умолчанию
    print(a, b)

do_something(1) # не изменили точку вызова - потенциальный баг

Поэтому в таких случаях сначала добавляем новый параметр, а затем внимательно смотрим все точки вызова и при необходимости их правим. И только после этого делаем значение по умолчанию. Хотя оно уже вроде бы и не требуется, так как все параметры везде передаются в явном виде.

Еще одна важная особенность современных языков — контроль за возможностью появления null-значений. Именно эта, казалось бы, мелочь часто становится источником трудноуловимых ошибок.

Контроль за nullability

Некоторые языки со строгой типизацией, такие как C# или тот же Kotlin, делают ее еще строже, позволяя вместе с типом переменной указывать возможность наличия в ней null (nullable-типы помечаются знаком вопроса, например String?). Ведь как известно, "неучтенный" null приводит к ошибке «на миллиард долларов».

Вот пример на Kotlin, который следит за передачей null в метод.

fun main() {
    method1(null) // всё ок
    method2(null) // не скомпилируется
}

fun method1(name: String?) { // name может содержать null
    // ...
}

fun method2(name: String) { // name НЕ может содержать null
    // ...
}

Так мы легко защищаем свой код от NullPointerException. В Java, правда, такой возможности нет.

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

Выводы

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

Поэтому для меня строгие ограничения — это не про ограничение творчества, а про создание прочного фундамента, на котором строится эффективная командная работа и качество конечного результата.

Комментарии (15)


  1. DenSigma
    02.06.2025 10:00

    Имхо, порядок в ИТ будет только тогда, когда введут что-то типа ГОСТ 2.109-73, и в каждой шараге будет свой отдел нормоконтроля.

    Вот у вас, в Райффайзен банке, есть отдел нормоконтроля для кода? Нет? Ну тогда все эти холивары, языки, линты и прочие код-ревью, которые делает "звезда" проекта - все чепуха.


    1. Dair_Targ
      02.06.2025 10:00

      А обновления как ЛМС-901 Байкал делаться будут по срокам и результатам?


      1. DenSigma
        02.06.2025 10:00

        Стандарт на ЕСКД был введен еще 1960 году, а до этого были отраслевые стандарты вроде ГОСТ 3450–46, а до этого ТОЖЕ были стандарты (только я не нашел сходу и не могу сказать). И со сроками и результатами все было в порядке.

        Стандартизация ускоряет.


  1. Lewigh
    02.06.2025 10:00

    И в этом основная сила Java, потому что разработчики не тратят время на то, чтобы выработать общий стандарт оформления кода, что критично для масштабных корпоративных проектов.

    Но, есть и обратная сторона. В той же Java из-за многословности разработчики понапридумывали костылей. Ладно Lombok если более менее на кодогенерации работает и предсказуем. Но огромная часть Java это дикое количество АОП. И вроде бы язык не самый вариативный а приложения порой работают также непредсказуемо как на JS, когда в рантайме выясняется что нет какого-то класса потому что версии библиотек не сошлись. Одного языка мало, нужна еще и адекватная экосистема.

    Если в команде больше одного разработчика, уже есть риск, что они будут писать методы в разном стиле. То есть даже в таком банальном примере возникает необходимость в выработке единого стиля, а также контроль за выполнением этих договорённостей на code review.

    Простой вопрос - зачем? Единственное что приходит на ум это то что разработчики не знают язык на котором пишут, ну тогда и меры другие нужно принимать наверное.

    История развития многих языков программирования показывает, что чем сильнее мы можем задать ограничения в коде — тем проще потом контролировать корректность его работы и искать баги.

    Если бы было так то все бы писали на Haskell или Rust.


    1. devmark Автор
      02.06.2025 10:00

      По поводу непредсказуемости Java не соглашусь с вами.

      В Java всегда можно посмотреть, например, полный стектрейс и найти, в чём именно ошибка. А несовместимость версий в рантайме выясняется легко при первом запуске. Особенно если мы пишем на фреймворке типа Spring.

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


      1. Lewigh
        02.06.2025 10:00

        В Java всегда можно посмотреть, например, полный стектрейс и найти, в чём именно ошибка.

        А если ошибки нет и просто что-то не работает не подавая виду не нужно будет SpringBoot по запчастям разбирать чтобы понять что не так попутно гадая на картах?

        А несовместимость версий в рантайме выясняется легко при первом запуске.

        Видно не сталкивались с проблемами которые легко не выясняются.

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

        А то что мы вместо того чтобы писать решение занимается бюрократией с вызовом расово правильных видов синтаксиса это мы так ценность вносим?


        1. devmark Автор
          02.06.2025 10:00

          Как правило, если мы используем стандартные компоненты Spring Boot и что-то не работает, не подавая виду, то там либо сразу выдаётся ошибка (а подчас ещё и решение предлагается прямо в логах), либо мы просто не повесили где-то аннотацию или не добавили какой-то параметр в конфиг.

          Что касается бюрократии, то от этого никуда не уйти в сколь-нибудь крупных проектах и командах. Иначе проект очень быстро превращается в зоопарк, который сложно поддерживать. Ценность тут заключается в снижении стоимости разработки: все пишут более-менее одинаково и не начинают очередной холивар на ревью.


  1. JVyacheslav
    02.06.2025 10:00

    Вопрос к автору (я так понял, шарит за Котлин)/котлинистам: null safety в реальности реально работает? Ну окей, типа пользовательский ввод будет не null, а "", но этот случай же придётся обработать, ведь он по факту некорректный, верно? Тогда смысл? Или я что-то не так понял?


    1. devmark Автор
      02.06.2025 10:00

      Пустая строка из вашего примера не то же самое, что null, т.к. на пустой строке мы можем спокойно вызывать любой метод из класса String. Тогда как попытка вызова такого метода на переменной, содержащей null, тут же приведёт к NPE. Разница вроде на первый взгляд небольшая, но значительная. Поэтому да, null safety в Kotlin спасает, и ещё как)

      Я вам даже больше скажу, иногда хочется разделять строки на подтипы, чтобы не перепутать их при передаче в метод, где несколько параметров и все - строки. Например, строка-логин, строка-пароль, строка-email и т.п. Хотя технически всё это строки. В Kotlin есть решение и для этого - value class.


      1. JVyacheslav
        02.06.2025 10:00

        Спасибо за пояснение, понял о чём вы. Хотя, конечно, защита от null всё равно не спасает от пустых строк, которые тоже не всегда желанные гости, это, судя по всему, всё же имеет смысл.


        1. devmark Автор
          02.06.2025 10:00

          Хочу ещё дополнить, что null safety в полной мере работает только для кода, написанного на котлине. Если же у вас интероп между котлином и джавой, тут надо быть аккуратнее, иначе ошибка произойдёт в точке вызова java-кода. Но сама возможность сказать, что "в данном коде не может быть NPE" - это очень круто!


          1. JVyacheslav
            02.06.2025 10:00

            Ну да, интеграция с джавой в этом плане конечно бывает проблемно. Я держу проектик на джаве, но объекты dto у меня на котлине (немного уменьшает количество кода + повёлся на null safety) И периодически была проблема, когда null пытался пройти в неналловый объект в итоге пришлось делать доп. проверки, которых хотел избежать


            1. devmark Автор
              02.06.2025 10:00

              Если вы используете котлин только для dto, а всё остальное пишете на Java, наверное, логичнее использовать record-классы.


              1. JVyacheslav
                02.06.2025 10:00

                Я рассматривал этот вариант, но у рекордов вроде нет сеттеров. А в data-классах котлина есть и геттеры и сеттеры, что для меня важно (изначально хотел избавиться от использования ломбока)

                P.s. "а зачем ты хотел избавиться от ломбока, спросите вы?" А я отвечу: "Понятия не имею, хотел поэкспериментировать!"


                1. devmark Автор
                  02.06.2025 10:00

                  То, что хотели избавиться от ломбока - это похвально)

                  Концептуально что record, что data-классы разрабатывались как неизменяемые (т.е. без сеттеров) сущности для хранения данных. Использование var в data-классах - это не совсем в духе котлина. Поэтому если нет каких-то явных технических ограничений (типа Spring Data JPA), рассмотрите возможность ухода от сеттеров в сторону неизменяемых объектов. У таких объектов есть множество преимуществ.