Речь пойдет о тайной, сугубо анонимной организации, следы которой начал замечать еще в 2018-ом, работая в Яндексе. О целях и мотивах организации можно только догадываться: некоторые считают это кибер-луддизмом, другие — техно-анархизмом. Ясно одно: организация существует, ее члены уничтожают кодовые базы десятилетиями, и говорить об этом не принято.

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

В серии статей будет 3 части:

  1. Тактика работы с кодовой базой (эта статья);

  2. Тактика работы с архитектурой;

  3. Тактика работы с коллективом.

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

Если же данная статья окажется последней — значит, до меня добрались.

План статьи

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

Содержание:

  • Конструирование ловушек в кодовой базе;

  • Когнитивное истощение или диалектика говнокода;

  • Подрыв производительности.

Примеры кода даны на Kotlin и за небольшим исключением будут понятны разработчикам «мейнстримовых» языков, вроде Java, Go, C++, TypeScript, Swift, Dart, Python.

Я не претендую на полноту изложения, но постарался изложить то, что доставило больше всего боли.

Конструирование ловушек в кодовой базе

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

Ловушка через «destructuring»

Чем как ни «дестракчерингом» — читай разрушением — пользоваться, когда нужно разрушить кодовую базу? Это и концептуально, и двусмысленно, и невинно одновременно. Давайте рассмотрим пример: создается data class c полями, порядок которых в будущем захочется поменять.

data class User(
    val age: Int,
    val id: Ing, // как будто бы id должен быть первым?
    val name: String,
    val surname: String,
)

И где-то в другом файле пишется:

val (_, id) = getUser()

Если какой-то разработчик поменяет поля age и id местами в декларации класса, что в общем-то безобидно, компилятор ничего не заметит и не подскажет о проблеме. В худшем случае какое-то время вместо id будет использоваться возраст.

Как это решать?

Можно завести отдельный класс для Age, тогда изменение порядка полей приведет к ошибке компиляции (Age не пройдет туда, где ожидается Int):±

value class UserAge(val age: Int)

Можно не использовать destructuring вовсе.

Ловушка через мета-программирование

Когда нужно добавить какую-то логику, мы идем в ожидаемое место. Бэкендеры за бизнес-логикой пойдут в handler и/или service, мобильные разработчики - в Interactor или UseCase. В redux-архитектуре бизнес-логика находится в функции reduce, а в TEA - update. Если речь про логику отрисовки деталей View, ее бы искали в реализации кастомной вьюхи. В общем, всегда ясно, куда идти.

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

Лучший пример, с которым сталкивался, — использование AspectJ в мобильном приложении. Не очень хочу называть имена, но намекну, что статей про AOP в Яндексе не так уж и много. Суть в том, что где-то в мета-файлах, куда никто никогда не заглядывает, помещаются инструкции, привязанные к текущей формации кода в проекте. Пишется что-то такое: найди класс с именем “X“ и вставь после метода “У“ в месте выхода из функции следующий код.

случайный пример AspectJ из интернета.
случайный пример AspectJ из интернета.

Работать с кодовой базой, зараженной Аспектами, становится больно, ведь изменение некоторых функций может сказаться непредсказуемым образом. Приходится держать в памяти код аспектов: на какие строчки и какие методы они завязаны, что и где добавляют. Безумно хорошо аспекты «синергируют» с наследованием, когда неявная логика прячется за уровень наследования.

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

Продают технологию под соусом поддержания чистоты: «код не будет замусорен аналитикой, логами, security-проверками». Кажется логично, что мета-задачи размещаются в мета-файлы, но на мой вкус, проблем больше:

  • Неявная и невидимая (в случае наследования) логика.

  • Дополнительный шаг компиляции.

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

Судя по всему, к этому пришел не только я, вот как выглядит частота гугления AspectJ с 2004 года:

Сколько компаний удалось уничтожить, прежде чем технология потеряла актуальность?
Сколько компаний удалось уничтожить, прежде чем технология потеряла актуальность?

Другой пример из другого языка программирования: макросы в Clojure. Макросы позволяют добавлять все что угодно в язык. Я хорошо знаком с core языка, писал на Clojure год за деньги и еще года 3 в свободное время. Но когда попадается кодовая база, усыпанная большим количеством макросов, все покрывается туманом. Выше я перечислял проблемы использования аспектов, все те же минусы актуальны и здесь (кроме, пожалуй, невидимой логики).

Ловушка через неявную связность

Пишем в базу (в кеш, в переменную) в одном месте, читаем в другом — вместо того, чтобы передать данные явно (чисто).

suspend fun addUserToGroup(context: Context) {
    // много логики тут
    db.addUser(context) // данные о пользователе берутся из контекста
    notifyUsers(context)
    // еще логика
}

suspend fun notifyUsers(context) {
    val users = db.getUsers(context: Context)
    ws.notifyUsers(users)
}

Первая цель саботажа — дождаться, пока кто-то поменяет строчки местами:

suspend fun addUserToGroup(context: Context) {
    // ...
    notifyUsers(context)
    // логика сломается, и нужному юзеру никогда не придет notification
    db.addUser(context)
}

Вторая цель саботажа — создать гонку (race condition). Пользователю (из контекста) должна прийти нотификация, а для этого функции addUser и getUser должны быть в одной транзакции.

Обеих проблем можно было бы избежать, сделав функцию notifyUsers чистой:

suspend fun addUserToGroup(users: List<Users>) {
    // ...
    val users = transactionResult { con ->
        db.addUser(con, context)
        db.getUsers(context.groupId)
    }
    notifyUsers(users)
    // ...
}

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

Ловушка через нарушение гайдлайнов и ожиданий

Еще один прием — играть на ожиданиях. Пример в Kotlin: скрывать бизнес-логику и ветвление внутри функции apply. Функция сама по себе предназначена для конфигурации объекта, и разработчик, видя ее, не подумает, что туда попадет бизнес-логика, не относящаяся к конфигурации объекта.

fun handleProducts(request: Json) {
    val products = parseJson(request).apply {
        onProductRequestReceive(this)
    }    
    service.handle(products)
    retspond(HttpStatus.OK)
}

Разработчик пойдет искать и дописывать логику в service.handle(product) и может продублировать код или внести багу. Да пусть хотя бы повозится какое-то время с «магическим» поведением — уже неплохо с точки зрения хитреца, установившего ловушку.

Когнитивное истощение или диалектика говнокода

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

Когнитивное истощение: отрицание отрицания

Вместо того, чтобы писать прямое условие, всегда можно написать инверсное:

val enabled = isEnabled()
if (!enabled) {
    // some other logic    
} else {
    // some logic    
}

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

val disabled = isDisabled()
if (!disabled) {
    // some logic    
} else {
    // some other logic    
}

Тут уже нужно сделать две дополнительных мыслительных операции: одну на отрицание “!“, вторую на мысленный перевод disabled в enabled. Кроме того, созданием метода с сигнатурой fun isDisabled() гарантируется, что во всей кодовой базе будут создаваться диалектические отрицания отрицания.

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

Когнитивное истощение: неинтуитивные необязательные функции

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

infix fun Boolean.implies(other: Boolean): Boolean = !this || other

И использование:

val isRaining = false
val carryingUmbrella by lazy { getUmbrella() != null }

val ok = isRaining implies carryingUmbrella 

Для кого-то логическая импликация — очевидная вещь. Можно давить на необразованность или синдром самозванца ревьюера, чтобы защитить PR. Цель же такого кода — заставить других разработчиков при встрече с implies идти смотреть определение функции, потому что даже тот, кто знаком с концепцией, должен будет убедиться, что функцию реализована ожидаемо.

А теперь самое сладкое: только представьте, насколько хорошо можно комбинировать импликацию с отрицанием отрицания!

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

val shouldIGoHome = !noRaining implies !noUmbrella

И посмотрите, насколько проще читается:

val shouldIGoHome = if (isRaining) {
    !carryingUmbrella()
} else {
    false
}

Когнитивное истощение: go to

Казалось бы, все современные языки отказались от go to, и после выхода статьи Дейкстры «Go To Statement Considered Harmful» (кстати, тут же могу порекомендовать и статью Go statement considered harmful) только самые ярые представители техно-анархизма сопротивлялись и продолжали апологию go to. Движение было сильно, и сейчас вы можете найти злосчастный стейтмент и в JS, и в C++, хотя большинство гайдлайнов его запрещают.

Что нам более интересно, все популярные языки неявно поддерживают go to.

Для демонстрации проблемы упростим логику приложения до простого стека выполнения программы:

+ Поток выполнения
    + Логика первого уровня
        + Логика второго уровня
            + Логика третьего уровня
            - Завершение логики третьего уровня
        - Завершение логики второго уровня
    - Завершение логики первого уровня
- Завершение потока выполнения

На примере бэкенда это могло бы выглядеть так:

+ Цикл обработки запросов
    + Middleware start
        + Handler function start
            + Service function start
            - Service function end
        - Handler function end
    - Middleware end 
- Закрытие сервиса

Теперь давайте представим, что нужно доработать бизнес-логику: в случаях, когда в запросе присутствует флаг deferred («отложенный»), мы не будем запускать основную логику, а сделаем вместо этого deferredLogic и вернем 202, а не 200. Изначально (упрощенный) код мог выглядеть как-то так (прошу, не обращайте внимание на «нейминг»):

class SomeService {
    // some code here

    fun someFunction(request: SomeRequest): Result {
        // more logic here
        return doLogicWithRequest()
    }

    sealed class Result {
        class Done(): Result()
        // more results
    }
}

// где-то на уровен хэндлера есть код с parrent matching по SomeService.Result
// Когда Result.Done, возвращается 200.

Чтобы реализовать логику, можно было бы дописать ветвление в ожидаемом месте:

+ Цикл обработки запросов
    + Middleware start
        + Handler function start
            + Service function start (вся новая логика тут)
            - Service function end
        - Handler function end
    - Middleware end 
- Закрытие сервиса
class SomeService {

    fun someFunction(request: SomeRequest): Result {
        return if (request.deferred) {
            doDeferredLogicWithRequest(request)
        } else {
            doLogicWithRequest(request)
        }
    }

    fun doDeferredLogicWithRequest(request: SomeRequest): Result.Deferred {
        // тут какай-то логика по отложенному запросу
    }

    sealed class Result {
        class Done(): SomeResponse() 
        class Deferred(): SomeResponse() 
        // more results
    }
}

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

+ Цикл обработки запросов
    + Middleware start
        + Handler function start
            + Service function start (часть логики тут)
                + DAO function start (часть логики тут)
                - DAO function end
            - Service function end
        - Handler function end
    - Middleware end (часть логики тут)
- Закрытие сервиса
class SomeService {
  
    fun someFunction(request: SomeRequest): Result {
        // обратите внимание, ничего не намекает, что 
        // поток выполнения может прерваться, и не видно, где он возобновится
        if (request.debug) {
            dao.processDeferred(request) // <- хотя тут кидается ошибка!
        }
        return doLogicWithRequest(request)
    }

    // тут тоже ничего не намекает на новые варианты ответов
    sealed class Result {
        class Done(): Result() 
        // ...
    }
}

class SomeDAO {
    fun processDeferred(request: SomeRequest) {
        // что-то пишем в базу
        throw SomeDefferedException() 
    }
}

И где-то на просторах «Middleware»:

catch(e: SomeDefferedException) {
    call.respond(202)
}

Обратите внимание, насколько это прекрасно: если ревьюер (который как правило занят другими задачами и торопится) посмотрит на каждый отдельный кусочек кода, все будет выглядеть приемлемо. Ну вызвали какой-то метод в сервисе. Добавили какой-то другой метод. Мало ли, понадобилось кинуть исключение. Добавилась какая-то обработка в Middleware.

Теперь представьте, что практически в каждом «хэндлере» присутствует неявная логика. Написание нового кода в предсказуемом месте может прерываться непредсказуемым образом через go to (throw). При вынесении кода на другой поток ошибка вообще потеряется.

Всегда ли исключения - плохо? Если речь о нормальном потоке выполнения, а не об ошибке, то да. Никогда не стоит реализовывать логику через исключения. Могу предположить, что адепты уничтожения корпораций вдохновляются книгами, вроде Effective Java, делая противоположное.

Use exceptions only for exceptional conditions. c. Effective Java

Когнитивное истощение: слепой try-catch

Если android-разработчик встретит код из листинга ниже, он сразу обнаружит, что Exception не обработается:

fun function() {
    // ...
    try {
        this.post {
            if (badCondition) throw BadException() 
        }
    } catch(e: BadException) {
        // какая-то обработка ошибок
    }
    // ...
}

Так как post ставит блок кода в очередь, которая обработается вне рамок try-catch. Чтобы реализовать «слепой try-catch», нужно значительно расширить его «скоуп». И возможно, в одном из мест выкинуть exception вне post, чтобы ублажить ревьюера.

Гляньте пример, все станет ясно:

fun function() {
    try {
        // какой-то код здесь
        if (badCondition) throw BadException() 
        // какой-то код здесь
        newFunction()         // <- вся хитрость тут!
    } catch(e: BadException) {
        // какая-то обработка ошибок
    }
    // ...
}

fun newFunction() {
        // ...
        this.post { // <- и вот он post, чтобы BadException не обработалось
            if (anotherCondition) throw BadException() 
        }
}

Если на ревью спросят, зачем такой широкий «скоуп» try-catch, всегда можно сказать, что Exception кидается несколько раз, зачем дублировать код? Надо DRY! Надо ли? в части про архитектуру в следующей статье об этом будет свой раздел.

Когнитивное истощение: скрытый источник данных

Обычно данные в базе данных появляются или в результате выполнения кода проекта (например, добавление юзера при регистрации), или в результате миграций. Можно включить третий источник — добавление данных от триггеров в базе. В любом случае, практически всегда можно разобраться: найти код, ответственный за добавление.

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

Почему так лучше не делать? Скрипт о добавлении данных может затеряться, а на тестовых стендах может произойти чистка (запланированная или случайная). Могут добавиться новые стенды.

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

Когнитивное истощение: неидеоматичный код

С каким бы стеком вы ни работали, всегда ясно, как писать «правильно» — то есть как писать так,

  • как принято,

  • как пишут все,

  • как разработчики языков и фреймворков подразумевали написание кода.

Техно-анархисты зашли очень далеко и даже пишут книги с рекомендациями писать неидеоматично (Data-oriented programming). Автор рекомендует в статически типизированных языках, вроде Java и C#, использовать мапы вместо классов.

Вдохновившись этой книгой, пробовал писать в функциональном стиле flutter-приложение на Dart.

Dart is an object-oriented, class-based, garbage-collected language with C-style syntax. c. Wikipedia

Кто-то может возразить, что Dart поддерживает и функции высшего порядка, и иммутабельность, и стримы, и библиотеки с персистентными коллекциями есть. Но этого не хватает, и мне кажется, что попытка писать полностью функционально тормозит разработку на Dart раза в 3-4 в сравнении с дефолтным подходом (субъективно).

Также не стоит писать FP на Go или OOP на Clojure. Я зайду дальше и скажу, что на Clojure не стоит писать как на Haskell, используя моноиды и прочую теорию категорий. Clojure предлагает отличный набор инструментов для sequence abstraction, все остальное только мешает читать и поддерживать код.

Подрыв производительности

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

Цитата неизвестного автора.

Маскировка проблем производительности через циклы

Прежде чем начать, сделаем небольшое отступление. Уверен, что каждый — даже самый маленький — понимает, что запрос к базе, развернутой на другой машине, значительно медленнее, чем запрос к хэшмапе (словарю) в том же потоке. Давайте оценим, насколько.

Данные получены из Графаны рабочего сервиса, в момент, когда я идентифицировал код агента хаоса. В конкретном примере поход в базу обходился в 2-4 миллисекунды.
Время запроса к мапе — 10-60 наносекунд. 10 из статьи «HashMap performance improvements in Java 8», 60 из личных замеров на своем стареньком маке.

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

Если обращение к мапе — это 1 минута, то поход в базу на другой машине — это примерно полгода.

Очень сложно «задедосить» сервис снаружи, но невероятно просто — изнутри. Надо только добавить запрос к базе в нескольких вложенных циклах. Вы спросите: как? ведь придется проходить code review! Я встречал неопытных анархистов, которые прямо вот так и писали:

for ...
    for ...
        for ...
            for ...
                db.pool.execute...

И этот код жил и хоронил сервер. Не преувеличиваю ни на цикл.

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

Первый файл — модельки данных:

// Названия продуктов в заказе
data class Order(val items: List<String>)

// какая-то информация по рекламе
data class AdInfo(val adds: List<String>)

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

fun processOrder(data: Order) {
    // Тут какой-то отвлекающий код, чтобы усыпить внимание.
    
    // Анархист надеется, что цикл тут будет пропущен:
    repeat(data.items.size, ::process) 
    
    // Еще какой-то код, отвлекающий внимание.
}

fun process(index: Int) {
    val item = data.items[index]
    val (detailInfo, success) = getProductDetails(item)
    when(success) {
        true -> processDetail(detailInfo)

        // специально бросается general Exception,
        // чтобы ревьюер мог зацепиться и проглядел реальную проблему
        else -> throw Exception() 
    }
}

fun getProductDetails(item: String): Pair<List<String>, Boolean> = TODO()
fun getAdInfo(key: String): List<String> = TODO()

Третий файл — попытка спрятать цикл через создание интератора. Даже при беглом просмотре кода вы увидите for, но если закамуфлировать итератор за typealias, можно и проглядеть:

fun processDetail(details: List) {
    // какой-то еще код ...
    val detailProcessor = details.prepare()
    detailProcessor.process()
    // какой-то еще код ...
}

// Еще лучше было бы, если вынести код ниже в отдельный файл,
// чтобы спрятать информацию о листах и итераторах
typealias DetailsProcessor = ListIterator

fun List.prepare(): DetailsProcessor {
    return listIterator() 
} 

fun ListIterator.process() {
    while(this.hasNext()) {
        val adInfo = getAdInfo(this.next())
        adInfo.process()
    }
}

Обратите внимание, process не намекает на работу с базой, в отличие, например, от save. А ниже рекурсия опять скрывает цикл. Тут важно постоянно использовать разные конструкции: repeat, forEach, for, while, (0..size), onEach, рекурсию, do while — это создает дополнительную когнитивную нагрузку.

fun AdInfo.process() {
    // тут какой-то код ...
    if (addInfo.adds.count() == 0) return
    val info = adInfo.adds[0]
    saveAds(info)
    this.copy(adds = adInfo.adds.subList(1))
        .process()
}

fun saveAds(add: String): Boolean {
    db.pool.execute {
        // sql inside 3 for loops
    }
}

Ок, скажете вы, этот код абсурдный и никакое ревью не пройдет. Можно докопаться практически до каждой строчки. Дело в том, что кода будет больше, значительно больше. Там, где речь не идет о главном для анархиста (внутренняя DDoS-атака), код будет написан хорошо. По всему остальному любой, даже неопытный разработчик сможет защититься.

— Убери, пожалуйста, typealias, он прячет итератор и цикл.
— Спасибо за предложение, но я считаю, что так лучше читается, сразу по коду видно, где не просто итератор, а именно DetailsProcessor. Plain old domain specific design.

— Помести всю логику в одну функцию или хотя бы в один файл.
— А это у меня SRP (Solid), «high cohesion low coupling». Я предпочитаю не связывать логику обработки заказов и рекламы. Вдруг потом это будут разные сервисы, не стоит связывать их.

— Зачем используешь то range, то while, то итератор — это создает метальную нагрузку? Давай везде сделаем for.
— Да ладно, чего там сложного, разработчик должен знать базовые конструкции языка.

— Я вижу 3 вложенных цикла и поход в базу, сделай все без циклов, пожалуйста, они тут не нужны.
— Это преждевременная оптимизация. Давай, если начнутся проблемы, будем думать об оптимизациях. Фичу нужно срочно релизить, сейчас нет времени на рефакторинг.

Маскировка проблем производительности через триггеры

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

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

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

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

Экспертная маскировка проблем с производительностью

Статей про то, как можно оптимизировать Postgres, очень много, например, вышедшая в совсем недавно «Оптимизация SQL запросов».

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

Вместо заключения

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

Понять можно и анархистов, борющихся с гнётом крупных корпораций.

Если же вы узнали в некоторых примерах себя и не являетесь членом организации, о которой шла речь, прошу не обижаться. Все совершают ошибки, и я не исключение. Хочется порекомендовать юмористическую статью The Grug Brained Developer с примерами «как надо».

Данная статья — попытка посмотреть на код с точки зрения нанесения ущерба кодовой базе — то есть «как не надо». Надеюсь, кому-то поможет писать более простой понятный код.

Кто-то стучится в дверь. Открою, а потом допишу последн...

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


  1. lightmaann
    18.01.2025 14:26

    маскируют вредоносный код, прячут проблемы на code review, создают абстракции с экспоненциальным ростом сложности.

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


    1. arturdumchev Автор
      18.01.2025 14:26

      О, я даже помню, что написал своему бывшему коллеге из LinguaLeo, который тоже работал в Яндексе в прошлом. Диалог был на фейсбуке, который я удалил, скопировать не могу — расскажу, как помню.

      — А у тебя на проекте в Я тоже был говнокод?
      — Да, тот еще.
      — А где-либо ты работал, чтобы код был нормальный?
      — Нет.

      Я уже 10 лет в разработке. Такого, чтобы прийти на проект, которые поддерживается 3+ лет и чтобы кодовая база была хорошей, — не было ни разу. Смотрел на код друзей, работающих в других компаниях. Самое-самое ужасное, что видел — размер функции main в java-приложении на несколько десятков тысяч строк кода.


      1. Krawler
        18.01.2025 14:26

        Я думаю что никакого заговора нет, а это следствие естественной деградации кодовой базы в условиях недостатка времени (кто видел бизнес которому фича нужна не вчера, поднимите руки) и недостаточной аналитики (не обязательно прям работы СА, а просто малого времени на анализ в целом)


        1. arturdumchev Автор
          18.01.2025 14:26

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


  1. Sly_tom_cat
    18.01.2025 14:26

    А зачем искать злой умысел там, где все легко объяснить глупостью?

    Бизнес давит на разработчиков "давай, давай, быстрее" и вот разработчик находит самый примитивный путь решения и фигачит.
    Спросите, а где же тут глупость? Так там, откуда подгоняют.


    1. arturdumchev Автор
      18.01.2025 14:26

      Да, согласен.

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

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

      Иногда переходишь на другой стек и пишешь неидиоматично, просто потому что привык к другому.

      То что заговора нет — это понятно, мне интересно было в таком стиле написать статью.


      1. CrazyOpossum
        18.01.2025 14:26

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


        1. SilverHorse
          18.01.2025 14:26

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


      1. xSVPx
        18.01.2025 14:26

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

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

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

        Никто и нигде(почти) не оптимизирует код по метрике "насколько это просто поддерживать", так чего удивляться, что модификации приводят к бедам?


    1. WLMike
      18.01.2025 14:26

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


  1. sshikov
    18.01.2025 14:26

    Статей про то, как можно оптимизировать Postgres, очень много, например, вышедшая в совсем недавно «Оптимизация SQL запросов».

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

    Только не в этом случае. Я бы сказал что автор как раз из "этих", из вашей тайной организации.


  1. SergeiZababurin
    18.01.2025 14:26

    То что описанно в статье, это следствие огромного колличества школ. Там учат писать по книгам, но ученики не понимают для чего что то делается и в каких случаях можно что то дедать, а что то нельзя.

    Результат.

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

    У таких людей нет заинтересованности поддерживать код и развивать, но есть заинтересованность красиво его представить.

    Вот например статья, в которой кнопку делают через обсервер. (из пушки по воробьям )
    https://habr.com/ru/articles/874302/


    1. vDyDHp8
      18.01.2025 14:26

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

      Про Яндекс вообще странно такое слышать.


      1. SergeiZababurin
        18.01.2025 14:26

        На ревью такой код лучше проходит чем читаемый как правило, Это тоже в статье есть. Здесь не зависит от ревювера этот момент.

        — Убери, пожалуйста, typealias, он прячет итератор и цикл.
        — Спасибо за предложение, но я считаю, что так лучше читается, сразу по коду видно, где не просто итератор, а именно DetailsProcessor. Plain old domain specific design.

        — Помести всю логику в одну функцию или хотя бы в один файл.
        — А это у меня SRP (Solid), «high cohesion low coupling». Я предпочитаю не связывать логику обработки заказов и рекламы. Вдруг потом это будут разные сервисы, не стоит связывать их.

        — Зачем используешь то range, то while, то итератор — это создает метальную нагрузку? Давай везде сделаем for.
        — Да ладно, чего там сложного, разработчик должен знать базовые конструкции языка.

        — Я вижу 3 вложенных цикла и поход в базу, сделай все без циклов, пожалуйста, они тут не нужны.
        — Это преждевременная оптимизация. Давай, если начнутся проблемы, будем думать об оптимизациях. Фичу нужно срочно релизить, сейчас нет времени на рефакторинг.

        трейдофф со временем.


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


        Про все языки не знаю. Знаю что это точно про js. Особенно после появления культа реакта (который сам по себе является адом колбеков (http://callbackhell.ru/) возникает вопрос.

        Кому и зачем это надо ?


      1. arturdumchev Автор
        18.01.2025 14:26

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

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


    1. arturdumchev Автор
      18.01.2025 14:26

      Я думаю, тут очень много причин, почему код усложняют или пишут “грязно“.

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

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


  1. x012
    18.01.2025 14:26

    Члены этой тайного сообщества уже проникли в ркн или еще нет?


    1. arturdumchev Автор
      18.01.2025 14:26

      Судя по всему, да, недавно попадались новости, что они случайно Youtube разблокировали


      1. x012
        18.01.2025 14:26

        Интересненько...
        И как далеко сия организация разовьется?

        Кто следующий объект проникновения?
        Тайные общества мировых правительств?


        1. arturdumchev Автор
          18.01.2025 14:26

          К сожалению, я пока не располагаю данными. Будет больше данных — буду делиться.


  1. dimal
    18.01.2025 14:26

    Не дочитал, но осужд.... А нет, поддерживаю!


  1. Cerberuser
    18.01.2025 14:26

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


  1. worldaround
    18.01.2025 14:26

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

    И где-то в другом файле пишется:

    val (_, id) = getUser()

    Если ваш язык позволяет это скомпилировать, то ищите истинного злодея не в яндексе.

    Спойлер

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


    1. arturdumchev Автор
      18.01.2025 14:26

      Kotlin как раз strongly staticaly typed.

      Код превратится в:

      val (_, id: Int) = getUser()

      Тут проблема в том, что и age, и id — одного типа Int, а destructuring возвращает компоненты по порядку из указания в декларации data class.


      1. Spinoza0
        18.01.2025 14:26

        "val id: Ing" - опечатка


    1. Spinoza0
      18.01.2025 14:26

      Так это одна из фишек языка, только нужно с умом использовать ) Автор даже подсказал, что value class спасёт


  1. Alexandr140
    18.01.2025 14:26

    Работал в Сбедивайсах с беглыми из Яндекса. Подтверждаю

    Шизофреническое усложнение архитектуры проекта, в ревью - постоянные переименования переменных. Документации не было вообще никакой. При этом у всех этих светил была полная профнепригодность в практических задачах. Когда задавал вопросы - звонили и закатывали истерики. Страдали от них в основном исполнители, которых набирали со стороны и которые не задерживались. Было это до февраля 2022 года.


    1. arturdumchev Автор
      18.01.2025 14:26

      Здравствуйте, коллега, на каком проекте вы были? Я с 2020 под 2022 написал и поддерживал медиа-приложения на девайсах.


  1. vadimr
    18.01.2025 14:26

    Настоящие программисты не будут писать функцию для импликации, они используют операцию <=

    Но вообще-то в конкретном примере из статьи проще написать shouldGoHome = (isRaining && !carryingUmbrella), а в общем претензия к импликации неясна.

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

    А макросы – это вообще святое.


    1. arturdumchev Автор
      18.01.2025 14:26

      shouldGoHome = (isRaining && !carryingUmbrella)

      В черновой версии статьи был такой код. Я решил изменить на if, чтобы читалось чуть проще, но это субъективно, конечно. Были бы примеры кода на Lua, так бы и оставил.

      Тут тезис про то, что если то же самое через импликацию писать, читается сложнее. А претензии к импликации те же, что и у разработчиков большинства языков, которые не включают implies как ключевое слово в кор языка. Тема обсуждалась много раз.

      А макросы – это вообще святое.

      Ловите, один уже скомпрометировал себя!


      1. vadimr
        18.01.2025 14:26

        Так именно потому, что импликация – это просто-напросто <= на булевском типе, для неё и нет отдельного ключевого слова.


        1. arturdumchev Автор
          18.01.2025 14:26

          Но мы же не можем в java, kotlin, go, clojure, dart, lua, fennel применить `<= ` к двум boolean. И ключевого слова implies нет.


          1. vadimr
            18.01.2025 14:26

            Ну clojure вообще не из этой оперы, там есть условная функция, которая позволяет всё это сформулировать изящнее, да и можно написать макрос. Хотя в racket есть implies из коробки.

            А вот почему в языках типа java нельзя применять операции сравнения к примитивному булевскому типу, хотя это самое натуральное перечисление из двух значений, лично мне непонятно, и это выходит за рамки вопроса об импликации. А как вы в java будете сортировать по булевскому полю? Инкапсулируя его в объект и применяя compareTo? Хорошо ли это?


    1. arturdumchev Автор
      18.01.2025 14:26

      На самом деле, по поводу макросов — все зависит от области применения.

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

      Если речь о написании фреймворка, макросы имеют право на жизнь и бывают очень красивы. Пример — nest в ClojureDart, писал об этом в статье Зачем Clojure Flutter.


  1. redfox0
    18.01.2025 14:26

    Ячан опять протекает в наши интернеты.


  1. dom1n1k
    18.01.2025 14:26

    Я не шарю в Котлине, но вот это что-то странное:

    data class User(
        val age: Int,
        val id: Int,
        val name: String,
        val surname: String,
    )
    
    val (_, id) = getUser()

    Серьезно?! Язык неявно преобразует структуру вида ключ-значение в упорядоченный кортеж? Но это же дичь, фундаментальный смысл ключей в том, чтобы не зависеть от их порядка.

    Или что такое функция getUser, может там внутри какое-то преобразование, которое не такое уж неявное?


  1. kotlomoy
    18.01.2025 14:26

    Ловушка через «destructuring»

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


  1. Jijiki
    18.01.2025 14:26

    спасибо за статью, очень понравилось что есть примеры и описание, интересно было бы посмотреть вообще как правильно или как надо, я тоже заметил что в языках много тонкостей, например в си++ в return (vec3)4*sin(10)+cos(1); можно возвращать, по началу было непривычно, потом подумал вроде логично если оператор с регионом памяти может работать(а может всё проще может это число пойдёт во все компоненты )) - подводных камней предостаточно в кодинге почти везде

    Лиспоподобные языки тоже нравятся - хороший детокс от С/С++ и разгрузочная остановка )