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 процентов.

Либо запустится либо нет.

Так и живем.

Старая школа

А теперь для сравнения «старая школа» — небольшой пример кода на Си, взятый из ядра 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 и идем писать на сях.

«Back to roots» и все такое.

К сожалению не все так просто и есть тайная правда, которую от вас скрывают. Называется:

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

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

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

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

К слову веб-приложение на первом скриншоте выше — продолжило работать после отработки исключения, а вот 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 — выбор скажем прямо не для всех.

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