software-development
May 4, 2023

Проблемы современной разработки. Часть 1: многоcловность

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

Многословный и вычурный код современного проекта на Java

Части: вторая, третья, четвертая.

Два мира

Начнем с характерных примеров, чтобы было понятно что именно мы тут обсуждаем. Сначала ярчайший пример современной деградации и декаденства высокоуровневой разработки, кусочек из Spring Framework:

public abstract class AbstractRefreshableConfigApplicationContext 
extends AbstractRefreshableApplicationContext
implements BeanNameAware, InitializingBean {
..
@Nullable
private String[] configLocations;
private boolean setIdCalled = false;
/**
* Create a new AbstractRefreshableConfigApplicationContext with no parent.
*/
public AbstractRefreshableConfigApplicationContext() {}
/**
* Create a new AbstractRefreshableConfigApplicationContext with the given parent context.
* @param parent the parent context
*/
public AbstractRefreshableConfigApplicationContext(
@Nullable ApplicationContext parent) {super(parent);}
/**
* Set the config locations for this application context in init-param style,
* i.e. with distinct locations separated by commas, semicolons or whitespace.
* <p>If not set, the implementation may use a default as appropriate.
*/
public void setConfigLocation(String location) {
setConfigLocations(StringUtils.tokenizeToStringArray(location, CONFIG_LOCATION_DELIMITERS));
}
/**
* Set the config locations for this application context.
* <p>If not set, the implementation may use a default as appropriate.
*/
public void setConfigLocations(@Nullable String... locations) {
 if (locations != null) {
  Assert.noNullElements(locations, "Config locations must not be null");
  this.configLocations = new String[locations.length];
  for (int i = 0; i < locations.length; i++) {
   this.configLocations[i] = resolvePath(locations[i]).trim();
  }
 } else {
  this.configLocations = null;
 }
 ...
}

Ряд характерных особенностей современной разработки, которые данный код отлично иллюстрирует:

Отсутствие смысловой нагрузки — весь код выше сам по себе бессмысленен и не несет никакой ценности.

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

Смысл в этом всем лишь более четкая организация проекта и вроде как удобство для разработчика.

Смешение нескольких разных видов программирования, в примере выше это ООП и АОП (аспектно-ориентированное программирование)

Немного расскажу про второе, благо ООП и так все знают. Ну, думают что знают конечно.

Обратите внимание на аннотацию @Nullable, вот что она означает:

A common Spring annotation to declare that annotated elements can be null under some circumstance.

А вот на что влияет:

Leverages JSR-305 meta-annotations to indicate nullability in Java to common tools with JSR-305 support and used by Kotlin to infer nullability of Spring API.

Те наличие или отсутствие этой аннотации напрямую влияет на поведение аж компилятора при определении факта: может указанное поле иметь пустое значение или нет.

Вот еще пара примеров такого подхода:

@Modifying(clearAutomatically = true)
@Query("update ScriptTask sr set sr.state =:newState where sr.id =:taskId")
@Transactional
void updateState(@Param("taskId") long taskId,
        @Param("newState") TaskState state);

Код этого метода целиком генерируется при запуске Spring, согласно описанным спецификациям. Называется весь этот цирк — Spring Repository.

Автоматическое управление транзакцией, SQL-запрос (JPQL на самом деле) и подстановка полей — все генерируется самим фреймворком.

Что произойдет если например в JPQL-запросе будет поле, которого нет у указанной сущности:

Приложение успешно скомпилируется, но упадет при запуске.

Правда весело?

Особенно если это будет происходить на продуктовом сервере.

Второй пример:

@Bean
@ConfigurationProperties("app.spring.datasource")
@Primary
public DataSourceProperties configDataSourceProperties() {
    return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("app.spring.datasource")
@Primary
public DataSource configDataSource() {
    return configDataSourceProperties()
            .initializeDataSourceBuilder().build();
}

Тут еще веселее:

каждый метод с @Bean аннотацией считается фабрикой, создающей экземпляры объектов — он вызывается фреймворком Spring при запуске приложения.

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

Но и это еще не все, аннотация @Primary означает что если будет несколько методов с одной и той же сигнатурой в разных классах, с аннотацией @Bean — т.е порождающих один и тот же объект, то использоваться будет только версия с @Primary.

Ах да, еще тут @ConfigurationProperties — это (внезапно) указание для сервиса, отвечающего за чтение настроек, который тут даже не виден. Ему передается префикс для полей с настройками.

Вообщем метаданные, которыми обвешан современный код стали иметь ценность сопоставимую с самим кодом:

@Configuration
@EnableJpaRepositories(value = "com.x0x08.calcman.repo", 
considerNestedRepositories = true)
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
public class DatabaseConfig extends AbstractConfig {
..
}

Это лишь часть метаданных, отвечающих за типичную настройку приложения на Spring: JPA, Repository, аудит и транзакции.

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

Вот еще отличный пример:

@Entity
@Table(name = "cl_api_key")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class ApiKey extends AbstractAuditingEntity implements Serializable {
    @NotNull
    @Column(name = "key_details", nullable = false)
    private String keyDetails;
    @Column(name = "enabled")
    private Boolean enabled;
    @NotNull
    @Column(name = "api_token", nullable = false)
    private String apiToken;
    @Column(name = "expired_at")
    private LocalDate expiredAt;
    ...
}

Это часть т.н класса «сущности» в JPA — модели данных, описывающих связи между таблицами в базе данных и полями объектов.

Как видите тут же описаны и настройки кеширования и типы полей и частично их валидация. И все это — в виде метаданных, а не кода как такового.

А какая самая характерная особенность у большей части метаданных во всех языках:

Они проверяются только в рантайме, а не на стадии компиляции

Вообщем для любого проекта на Spring вероятность успешного запуска после успешной сборки — 50/50, либо запустится либо нет.

Так и живем.

Старая школа

А теперь старая школа — пример кода на Си, взятый из ядра Linux:

static void intel_detect_tlb(struct cpuinfo_x86 *c)
{
int i, j, n;
unsigned int regs[4];
unsigned char *desc = (unsigned char *)regs;
if (c->cpuid_level < 2)
  return;
/* Number of times to iterate */
n = cpuid_eax(2) & 0xFF;
for (i = 0 ; i < n ; i++) {
  cpuid(2, &regs[0], &regs[1], &regs[2], &regs[3]);
  /* If bit 31 is set, this is an unknown format */
  for (j = 0 ; j < 3 ; j++)
    if (regs[j] & (1 << 31))
      regs[j] = 0;
  /* Byte 0 is level count, not a descriptor */
  for (j = 1 ; j < 16 ; j++)
     intel_tlb_lookup(desc[j]);
  }
}

Ключевые отличия:

Четкий и явный смысл и ценность этого кода — тут происходит вычисление буфера ассоциативной трансляции для процессоров Intel.

Один стиль программирования — процедурный, без пересечений и дополнений.

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

Называется оно:

Обработка исключений

Посмотрите на эту прелесть выше — выглядит страшно для непричастных, правда?

Сколько же ругани я наслушался про все эти длинные простыни трассировок, естественно от ИТ-молодежи, которые просто не представляют как выглядит падение нативного приложения на чистом Си:

Информативно?

К слову веб-приложение на первом скриншоте выше все же продолжило работать после отработки исключения, а вот Segmentation fault это все, п#здец конец.

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

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

Вообщем есть куда более объективные причины.

Что такое современная разработка

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

Текучка кадров

Наша тестовая команда — допустим стандартные 5-6 человек, делают некий онлайн сервис, с веб-интерфейсом. Для B2B, статистически таких проектов больше.

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

Думаю очевидно, что особой любви к изучению уже написанного кода у новых разработчиков нет, разбирать написанное вообще мало кто любит.

Отсутствие глубоких компетенций

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

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

Поэтому в команде будет пара разработчиков с более-менее приличными компетенциями, достаточными для развития проекта, а все остальные будут просто ходить на работу.

«Таски закрывать в трекере».

Само собой что при таких реалиях, ни смысла ни интереса к глубокому изучению чего-то нет и быть не может.

Отсутствие перспектив

Врядли кого-то удивлю утверждением, что у типичного B2B проекта нет ни будущего ни прошлого — есть только мрачное настоящее и набор тасков в трекере.

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

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

Даже если сервис разрастется во второй Амазон — на говнокод внутри это никак не повлияет, его просто станет на порядки больше.

Отсутствие ценности исходного кода

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

Не верите?

Взгляните на код:

      PROGRAM EUCLID
        PRINT *, 'A?'
        READ *, NA
        IF (NA.LE.0) THEN
          PRINT *, 'A must be a positive integer.'
          STOP
        END IF
        PRINT *, 'B?'
        READ *, NB
        IF (NB.LE.0) THEN
          PRINT *, 'B must be a positive integer.'
          STOP
        END IF
        PRINT *, 'The GCD of', NA, ' and', NB, ' is', NGCD(NA, NB), '.'
        STOP
      END

      FUNCTION NGCD(NA, NB)
        IA = NA
        IB = NB
    1   IF (IB.NE.0) THEN
          ITEMP = IA
          IA = IB
          IB = MOD(ITEMP, IB)
          GOTO 1
        END IF
        NGCD = IA
        RETURN
      END

Да, это FORTRAN 77.

А теперь оцените сколько ему лет:

Final drafts of this revised standard circulated in 1977, leading to formal approval of the new FORTRAN standard in April 1978. The new standard, called FORTRAN 77 and officially denoted X3.9-1978, added a number of significant features to address many of the shortcomings of FORTRAN 66

46 лет!

Код который старше меня и до сих пор отлично работает:

Пакет называется "gcc-fortran", если вдруг захотите запустить самостоятельно.

Но вот почему-то код на Java/.NET такой жизнеспособностью не страдает — его натурально переписывают под ноль каждые 3-5 лет, если не выбрасывают проект целиком.

По колено в говнокоде

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

Чтобы вы не отвлекались от трекера и не думали о великом.

Думать вообще вредно, как показывает опыт.

Что Java, что C# что Typescript — это такая своеобразная клетка из заранее определенных абстракций и паттернов поведения, выход за пределы которой как минимум не приветствуется.

Естественно я утрирую и упрощаю, да и как показывает практика:

изучить на приемлемом уровне даже такую специально ограниченную платформу как Java или .NET мало кому удается.

Поэтому основная масса современных разработчиков вообще ударилась в шаманизм и традиционные верования — они на полном серьезе верят в «паттерны проектирования» и «best practices»:

“A forEach operation that does anything more than present the result of the computation performed by a stream is a “bad smell in code,” as is a lambda that mutates state.”
― Joshua Bloch, Effective Java : Programming Language Guide

Все понимаю, но бл#ть г.н Блох — не Иисус Христос, которого распяли сослали в Google за мои грехи в разработке.

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

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

Be sure to use as above for Spring Boot 3+ (that uses Spring 6) to have URL ending with '/' still being processed by mapping in Controllers without ending '/'.
Note for the snippet the value is exactly true
and @EnableWebMvc is not used with Spring Boot (as it would in fact disable autoconfiguration for web MVC)
That was exactly recommended in https://github.com/spring-projects-experimental/spring-boot-migrator/issues/206 "Spring Boot 3.0.0 M4 Release Notes"

Это уже практически как у техножрецов из Warhammer 40k, разве что пока ладаном сервера окуривать не додумались.

Про вычурность

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

Характерный пример на C#:

x => x + 1                             // Implicitly typed, expression body
x => { return x + 1; }                 // Implicitly typed, block body
(int x) => x + 1                       // Explicitly typed, expression body
(int x) => { return x + 1; }           // Explicitly typed, block body
(x, y) => x * y                        // Multiple parameters
() => Console.WriteLine()              // No parameters
async (t1,t2) => await t1 + await t2   // Async
delegate (int x) { return x + 1; }     // Anonymous method expression
delegate { return 1 + 1; }             // Parameter list omitted

Между прочим - из официальной спецификации.

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

Я уже говорил что часть таких синтаксических конструкций имеет тенденцию к устареванию и отмене (deprecated ага)?

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

Печальные выводы

Современные языки высокого уровня и современная разработка вообще — про одноразовость, сменяемость и временность.

Чтобы вы быстро (лет за 5-7) вошли в ИТ, быстро дошли до кондиции «все знаю — все могу» и дальше весь остаток стандартной карьеры программиста писали и поддерживали однообразный код.

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

Длительная разработка на таких языках подразумевает постоянную переделку и миграцию кода проекта на более новые версии всего — от инструментов до библиотек.

И ничего осмысленного для бизнеса в этом нет.

По-сути в весь современный инструментарий для разработки заложены те же самые бомбы «искуственного устаревания» что и в современном автопроме.

Вам просто не дадут спокойно использовать софт скажем 10 летней давности — софт будет просить обновлений, будет показывать страшные баннеры об устаревании и невозможности поддержки:

Называть такое прогрессом язык не поворачивается.

Что с этим всем делать

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

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

Остается только с этим жить и как-то сосуществовать, правильно оценивая риски и возможные сложности, но точно не гнаться сломя голову за каждой декоративной инновацией, добавленной лишь вчера в самый популярный язык.