Русский
Русский
English
Статистика
Реклама

Codestyle

Прочти меня код, который не выбесит соседа

16.03.2021 12:09:05 | Автор: admin


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

Я расскажу о подходах, которые мы используем в Яндекс.Такси для написания читаемого кода на C++, Python, JavaScript и других языках.

Обычный рабочий процесс


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

Выглядит функция sample как-то так:

std::string sample(int d, std::string (*cb)(int, std::string)) {  // Getting new descriptors from d with a timeout  auto result = get(d, 1000);  if (result != -13) {    // Descriptor is fine, processing with non bulk options    auto data = process(result, true, false, true);    // Passing processing result to the callback    return cb(data.second.first, data.second.second);  }  // Notifying callback on error  return cb(result, {});}

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

Нам повезёт, мы сможем исправить функцию, не разобравшись до конца в её работе.
Начинаем читать первый комментарий, где написано: получаем какие-то новые дескрипторы из d с заданным timeout, d дескриптор. То есть часть разбросанных по коду int на самом деле не int они относятся к отдельному классу данных.

Решаем заменить часть int на отдельный тип данных Descriptor в надежде, что компилятор сам найдёт ошибки и нам не придётся дальше отлаживать код. Зовём автора кода, просим его подсказать, где дескрипторы, а где числа. Он сейчас работает над другим проектом, но после долгих уговоров неохотно помогает и быстро ретируется:

enum class Descriptor : int {};std::string sample(Descriptor d, std::string (*cb)(Descriptor, std::string)) {  // Getting new descriptors from d with a timeout  auto result = get(d, 1000);  if (result != Descriptor(-13)) {    // Descriptor is fine, processing with non bulk options    auto data = process(result, true, false, true);  // <== ERROR    // Passing processing result to the callback    return cb(data.second.first, data.second.second);  // <== ERROR  }  // Notifying callback on error  return cb(result, {});}

И тут понеслось:

  • Компилятор нашёл сразу две ошибки. Это очень подозрительно, может, мы не так типы расставили?
  • А что вообще такое data.second.first и data.second.second? В комментариях не написано.

Делать нечего, придётся прочитать весь код и комментарии, чтобы понять, как исправить ошибки.

Боль


Поначалу казалось, что много комментариев это хорошо. Однако при отладке всё выглядит иначе. Код написан на двух языках: на английском и C++. Когда мы пытаемся понять, что происходит в коде, и отладить его, нужно прочитать английский, перевести его в голове на русский. Затем прочитать код на C++, его тоже перевести в голове на русский и сравнить эти два перевода. Убедиться, что смысл комментария совпадает с тем, что написано в коде, а если код делает что-то другое, то, возможно, там и кроется ошибка.

Ещё неприятно, что по коду разбросаны волшебные константы. Вот, например, -13 что это такое? Почему -13? Комментарии не помогают. Волшебные константы только смущают и делают разбор функции сложнее. Но это цветочки, сейчас пойдут ягодки.

Попробуйте угадать, что значат булевые флажки true, false, true в функции process? В комментариях о них ни слова. Чтобы разобраться, нужно пойти в header file, где объявлена функция process:

std::pair<Descriptor, std::pair<int, std::string>> process(bool, Descriptor, bool, bool);

И там мы увидим, что у булевых переменных нет осмысленных имён.

Чтобы понять, что происходит, нужно перейти в соседний файл, прочитать код функции process и разобраться в нём. Возможно, походить по соседним функциям и почитать их. На исследование смежных файлов тратится уйма времени, что мешает осознанию функции sample и портит настроение.

Наконец, data.second.first и data.second.second. Чтобы выяснить их назначение, нужно отмотать назад туда, где мы получаем переменную data. Пойти в место, где объявлена функция process, увидеть, что комментариев нет, а process возвращает пару от пары. Пойти в исходники, узнать, что обозначают переменные int и string, и на всё это снова уходит очень много нашего времени.

Ещё одна маленькая боль код обработки ошибок перемешан с основной логикой. Это мешает ориентироваться. Обработка ошибок функции sample находится внизу, а в середине, внутри цикла if, с большими отступами находится happy path.

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

Выжимка проблем


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

  • Комментариев всё ещё недостаточно:
    Приходится читать код смежных функций.
    Есть магические константы.

  • Код обработки ошибок и основной логики перемешаны:
    Большие блоки кода с большими отступами.

Читаемый код


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

Поехали:

std::string Sample(Descriptor listener,                              std::string (*cb)(Descriptor, std::string)){  UASSERT_MSG(cb, "Callback must be a non-zero function pointer");  const auto new_descriptor = Accept(listener, kAcceptTimeout);  if (new_descriptor == kBadDescriptor) {    return cb(kBadDescriptor, {});  }  auto data = Process(Options::kSync, new_descriptor);  return cb(data.descriptor, data.payload);}

В первой же строчке проверяем входные параметры. Это мини-подсказка/документация по тому, какие данные ожидаются на входе функции.

Следующая правка: вместо функции с непонятным именем Get появляется Accept, широко известная в кругах сетевых программистов. Затем страшную константу 1000 превращаем в именованную константу с осмысленным читаемым именем.

Теперь строка прекрасно читается без дополнительных комментариев: из listener мы принимаем новый дескриптор, на эту операцию даётся kAcceptTimeout.

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

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

Специфика C++
Маленький бонус большинство компиляторов в C++ считают одиночные if без блока else холодным путём.

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

В итоге мы незначительно (а то и вовсе незаметно) ускорили приложение. Пустячок, но приятно.

Дальше. Функция process преобразовалась. Вместо true, false, true теперь есть перечисление возможных опций для process. Код можно прочитать глазами. Сразу видно, что из дескриптора мы процессим какие-то данные в синхронном режиме и получаем их в переменную data.

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

В результате код стал почти в два раза короче. Он стал понятнее. Больше не нужно ходить в соседние функции и файлы. Читать такой код куда приятнее.

Приёмы


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

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

1) Compute(payload, 1023) нечитаемо. Что такое 1023?
Используйте именованные константы.
Compute(payload, kComputeTimeout)

Альтернативным решением может быть явное использование имён параметров. Например, Python позволяет писать:

Compute(payload=payload, timeout=1023);

Ну и C++20 не отстаёт:

Compute({.payload=payload, .timeout=1023});

Идеальный результат получается, если пользоваться сразу двумя приёмами:

Compute(payload=payload, timeout=MAX_TEST_RUN_TIME);

2) Compute(payload, false) нечитаемо. Что такое false?
Используйте перечисления или именованные константы вместо bool.
У bool не всегда понятна семантика. Введение перечисления даже из двух значений явно описывает смысл конструкции.

Compute(payload, Device::kGPU)

Именованные аргументы в этом месте не всегда спасают:

Compute(payload=payload, is_cpu=False);

Всё ещё непонятно, что False заставляет считать на GPU.

3) Compute(data.second.first, data.second.second) или Compute(data[1][0], data[1][1]) что вообще тут происходит?
Используйте типы с информативными именами полей, избегайте кортежей.
Compute(data.node_id, data.chunk_id)

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

Попробуйте угадать, какой смысл у возвращаемых int и std::string в коде.

std::tuple<int, std::string> Receive();

int это дескриптор устройства? Код возврата?

А вот так всё становится кристально ясно:

struct Response {    int pending_bytes;    std::string payload;};Response Receive();

4) void Accept(int , int); что это за два числа?
Заводите отдельные типы данных для разных по смыслу вещей.
void Accept(Descriptor, std::chrono::milliseconds)

Или для Python:

def Accept(listener: Descriptor, timeout: datetime.timedelta) -> None:

На самом деле это совет не столько про читаемость кода, сколько про отлов ошибок. Многие (но далеко не все) современные языки программирования позволяют статически проверять типы и узнавать об ошибках ещё до запуска приложения или тестов. В C++ эта функциональность доступна из коробки, в Python нужно пользоваться линтерами и typing.

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

5) void Compute(Data data) функция есть в модуле или заголовке, но должны ли мы ей пользоваться?
Используйте особый namespace или именование для служебных вещей.
namespace detail { void Compute(Data data); }

Или для Python:

def _compute(data: Data) -> None:

С namespace detail/impl или с особым наименованием пользователь поймёт, что функцию использовать не нужно.

6) d, cb, mut, Get что это?
Придумывайте информативные имена переменных, классов и функций.
descriptor, callback, mutator, GetDestination

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

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

Так вот, через неделю или месяц будет сложно вспомнить, что такое d или cd, что делает метод Get (или это вообще класс Get?). Что он возвращает?

Информативные имена вам очень помогут. При чтении будет сразу видно, где descriptor, callback и mutator, а где функция под именем GetDestination() возвращает какой-то Destination.

7) connection = address.Connect(timeout) отладчик завёл нас в эту строчку кода. Что там за переменные, откуда они и куда мы их присваиваем?
Закрепите разные стили за переменными класса, аргументами функций и константами.
Если закрепить отдельные стили за разными типами переменных, код читается лучше. В большинстве распространённых code style именно так и делают:

connection_ = address.Connect(kTimeout);

Мы сразу видим, что переменная address локальная, что мы пытаемся соединиться с kTimeout, который является константой. Результат соединения присваиваем переменной класса connection_. Поменяли буквально пару символов, а код стал понятнее.

Для Python стоит дополнительно придерживаться правила, что приватные поля начинаются с нижнего подчёркивания:

self._connection = address.Connect(TIMEOUT);

8) connection_ = address.Connect(timeout / attempts) есть ли тут ошибка?
Используйте assert, чтобы проверить, правильно ли используют ваш код.
Если количество attempts будет равно нулю, то нативное приложение, скорее всего, рухнет. Где-то возникнет stack trace или корка. С помощью дополнительных телодвижений можно добраться до stack trace и понять, что падение произошло именно в этой строчке.

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

Однако если внутри Connect не будет проверки, всё станет сильно-сильно сложнее. Приложение не упадёт, но будет работать неправильно, не так, как мы ожидаем.

Коду явно не хватает проверок:

ASSERT(attempts > 0);

assert timeout > 0

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

Assert не только позволяет быстро находить ошибки, но и добавляет читаемости. Выражение assert timeout > 0 прямо говорит, что код ниже будет работать неправильно с отрицательными числами и 0.

8.1) connection_ = address.Connect(timeout / attempts) есть ли тут ошибка?
НЕ используйте assert для проверки пользовательского ввода.
Будет невежливо (и опасно!), если ваша библиотека уронит весь сервер потому, что кто-то на сайте ввёл неправильное число. Здесь стоит использовать другие механизмы для сообщения об ошибках:

if (attempts <= 0) throw NegativeAttempts();

if (attempts <= 0) raise NegativeAttempts();

Как отличить неправильное использование функции программистом от неправильного ввода?

Вот несколько примеров:

  • функция init() должна быть вызвана перед первым использованием функции foo() assert,
  • мьютекс не должен дважды захватываться из одного и того же потока assert,
  • баланс на карте должен быть положительным НЕ assert,
  • стоимость поездки не должна превышать миллиона доллларов НЕ assert.

Если не уверены не используйте assert.

Ещё один хороший приём использовать в отладочных сборках assert, а в релизе другой механизм информирования об ошибках. Пример с attempts идеально ложится на этот случай:

ASSERT(attempts > 0);if (attempts <= 0) throw NegativeAttempts();

9) v = foo(); bar(v); baz(v); assert(v); а функциям bar() и baz() точно хорошо?
Не тяните с обработкой ошибок, не несите их в блок else.
Как только получили значение сразу проверяйте и обрабатывайте ошибки, а дальше работайте как ни в чём не бывало.

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

Пример:

if (value != BAD) {    auto a = foo(value);    auto b = bar(value);    if (a + b < 0) {         return a * b;    } else {         return a / b + baz();    }}return 0;

Сравните:

if (value == BAD) {    return 0;}auto a = foo(value);auto b = bar(value);if (a + b < 0) {     return a * b;}return a / b + baz();

10) [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
Придерживайтесь вменяемой вложенности конструкций.
Если в вашем коде есть if, внутри которого for, внутри которого if, внутри которого while, это сложно читать. Стоит разнести какие-то внутренности на отдельные функции и дать функциям осмысленные имена. Так код станет красивее и приятнее для чтения.

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

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

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


Разумеется, это общие советы. У каждого языка есть своя специфика. В C++ важно писать понятные имена шаблонных параметров, в Python не стоит злоупотреблять тем, что переменные предоставляют возможности для общего модифицирующего владения.

Даже в рамках одной компании практики могут расходиться (например, в userver у нас пожёстче с наименованями и bool).

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

Руководство по стилю Kotlin для Android разработчиков (Часть I)

18.01.2021 16:06:50 | Автор: admin

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

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

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

Поэтому я решил разделить её на две части.

Обе части содержат описание стандартов кода на языке прораммирования Kotlin.

Что покрывают обе части:

  • Именование файлов, переменных, классов, свойств и т.д.

  • Структура исходного файла

  • Форматирование - строки, пробелы, скобки, специальные конструкции, переносы и др.

  • Документация

В первой части я затрону исходные файлы и форматирование (неполностью).

Ну что ж пора начинать!

Исходные файлы

Поговорим сначала об исходных файлах, о их структуре и других важных вещах.

Кодировка

Все исходные файлы должны иметь UTF-8 кодировку.

Именование

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

Если файл содержит несколько высокоуровневых определений (два класса и один enum к примеру) выбирается имя файла, которое описывает его содержимое:

// PhotoAdapter.ktclass PhotoAdapter(): RecyclerView.Adapter<PhotoViewHolder>() {// ...}// Utils.ktclass Utils {}fun Utils.generateNumbers(start: Int, end: Int, step: Int) {// ...}// Map.ktfun <T, O> Set<T>.map(func: (T) -> O): List<O> = // ...fun <T, O> List<T>.map(func: (T) -> O): List<O> = // ...

Структура

Kotlin файл .kt включает в себя:

  • Заголовок, в котором указана лицензия и авторские права (необязательно)

  • Аннотации, которые объявлены на уровне файла

  • package объявление

  • import выражения

  • высокоуровневые объявления (классы, интерфейсы, различные функции)

Заголовок должен быть объявлен выше остальных определений с использованием многострочных комментариев:

/* * Copyright 2021 MyCompany, Inc. * * */

Не используйте однострочные и KDoc комментарии:

/**  * Copyright 2021 MyCompany, Inc. * */// Copyright 2021 MyCompany, Inc.//

Аннотация @file, которая является use-site target должна быть помещена между заголовком и package объявлением:

/* * Copyright 2021 MyCompany, Inc. * */@file:JvmName("Foo")package com.example.android

Оператор package и importникогда не переносятся и всегда размещаются на одной строке:

package com.example.android.fragments  // переносы запрещеныimport android.view.LayoutInflater // так же и здесьimport android.view.View

Выражения import группируются для классов, функций и свойств в сортированные списки.

Импорты с подстановочным знаком не разрешены:

 import androidx.room.*  // так делать не нужно

Kotlin файл может содержать объявление одного или нескольких классов, функций, свойств или typealias выражений.

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

Нет явного ограничения на количество и порядок содержимого файла

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

Важен логический порядок, который может объяснить сам разработчик.

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

Для членов класса применимы те же правила, что и для высокоуровневых определений.

Специальные символы

В исходном коде используется только ASCII горизонтальный пробельный символ (0x20).

Это означает, что:

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

  • Tab символы не используются для отступов

Для любого символа, который имеет экранированную последовательность (\b, \r, \t, \\) используется эта последовательность, а не Unicode (например: \u000a).

Для оставшихся символов, которые не принадлежат ASCII, используется либо Unicode символ (), либо Unicode последовательность (\u221e).

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

// Лучшая практика: понятно без комментариевval symbol0 = ""// Плохо: нет причины не использовать символ вместо Unicode последовательностиval symbol1 = "\u221e" // // Плохо: читатель не сможет понять, что это за символ val symbol2 = "\u221e"// Хорошо: использование Unicode последовательности для непечатаемого символаreturn "\ufeff" + content// неразрывный пробел нулевой ширины

Форматирование

Ближе к коду!

Скобки

Скобки не требуются дляwhen и if которые помещаются на одной строке (оператор if не имеет else ветки):

if (str.isEmpty()) returnwhen (option) {    0 -> return    // }

В другом случае скобки обязательно требуются для if, for, when ветвлений и do и while выражений:

if (str.isEmpty())    return  // так делать нельзя!if (str.isEmpty()) {    return  // OK}

Скобки следуют стилю Кернигана и Ритчи для непустых блоков и блочных конструкций:

  • Нельзя делать разрыв строки перед открывающей скобкой

  • Разрыв строки после открывающей cкобки

  • Разрыв строки перед закрывающей скобкой

  • Разрыв строки после закрывающей скобкой только в том случае, если она заканчивает выражение или тело функции, конструктора, класса.

class MainActivity : AppCompatActivity() {    private lateinit var binding: ActivityMainBinding    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)        // ...    }}

Пустые блоки тоже должны быть в стиле K&R:

try {    val response = fetchDogs("https://api.dog.com/dogs")} catch (e: Exception) {} // неправильноtry {    val response = fetchDogs("https://api.dog.com/dogs")} catch (e: Exception) {} // OK

if/else выражение может быть без скобок, если помещается на одной строке:

val value = if (str.isEmpty()) 0 else 1  // OKval value = if (str.isEmpty())// неправильно0else1val value = if (str.isEmpty()) { // OK0} else {1}

С каждом новым блоком отступ увеличивается на 4 пробела. Когда блок закрывается отступ возвращается на предыдущий уровень (это применимо и для комментариев).

Переносы

Каждое выражение разделяется переносом на новую строку (; не используется)

Строка кода имеет ограничение в 100 символов.

Исключения:

  • Строки, которые невозможно перенести (например: длинный URL)

  • package и import выражения

  • Команды в документации, которые можно вставить в shell

Правила для переноса на новую строку:

  • Перенос после оператора или infix функции.

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

    • точка (., .?)

    • ссылка на член (::)

  • Имя метода или конструктура находится на одной строке с открывающей скобкой

  • Запятая (,) связана с элементом и не переносится

  • Стрелка (->) для lambda выражений связана с аргументами

Когда сигнатура функции не помещается, объявление параметров располагается на отдельных строчках (параметры должны иметь один отступ в 4 пробела):

fun makeSomething(  val param1: String,  val param2: String,  val param3: Int) {}

Когда функция содержит одно выражение можно сделать так:

override fun toString(): String {return "Hello, $name"}override fun toString() = "Hello, $name"

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

fun waitMe() = runBlocking {delay(1000)}

Когда инициализация свойства не помещается на одной строке можно сделать перенос после знака присваивания (=):

 val binding: ListItemBinding =  DataBindingUtil.inflate(inflater, R.layout.list_item, parent, false)

get и set функции должны быть на отдельной строке с обычным отступом (4 пробела):

 val items: LiveData<List<Item>> get() = _items

Read-only свойства могут иметь более краткий синтаксис:

val javaExtension: String get() = "java"

Пробелы

Пустая строка может быть:

  • Между членами классов: свойствами, функциями, конструкторами и другими

    • Пустая строка между двумя свойствами необязательна. Это нужно для создания логических групп (например для backing свойств)

  • Между выражениями для логического разделения

  • Перед первым членом функции или класса (необязательно)

Помимо требуемых правил для языка и литералов (строчных или символьных) одиночный ASCII пробел:

  • Разделяет зарезервированные слова, таких как: if, for или catch от круглой открывающей скобки:

// неправильноfor(i in 1..6) {}// OKfor (i in 1..6) {}
  • Разделяет любые зарезервированные слова, таких как else и catch от закрывающей фигурной скобки:

// Неправильно}else {}// OK} else {}
  • Ставиться перед любой открывающей фигурной скобкой:

// Неправильноif (items.isEmpty()){}// OKif (items.isEmpty()) {}
  • Ставиться между операндами:

// Неправильноval four = 2+2// OKval four = 2 + 2// Это относится и к оператору лямбда выражения (->)// Неправильноitems.map { item->item % 2 == 0 }// OKitems.map { item -> item % 2 == 0 }
  • Исключение: оператор ссылка на член (::), точка (.) или range (..)

// Неправильноval str = Any :: toString// OKval str = Any::toString// Неправильноitem . toString()// OKitem.toString()// Неправильноfor (i in 1 .. 6) {println(i)}// OKfor (i in 1..6) {println(i)}
  • Перед двоеточием (:) для указания расширения базового класса или интерфейса, а также в when выражении для generic типов:

// Неправильноclass Worker: Runnable// OKclass Worker : Runnable// Неправильноfun <T> min(a: T, b: T) where T: Comparable<T>  // OKfun <T> min(a: T, b: T) where T : Comparable<T>
  • После двоеточия (:) или запятой (,)

// Неправильноval items = listOf(1,2)// OKval items = listOf(1, 2)// Неправильноclass Worker :Runnable// OKclass Worker : Runnable
  • По обеим сторонам двойного слеша:

// Неправильноvar debugging = false//отключен по умолчанию// OKval debugging = false // отключен по умолчанию

Заключение

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

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

Полезные ссылки:

Ждите следующей части!

Подробнее..

Руководство по стилю Kotlin для Android разработчиков (Часть II)

21.01.2021 18:22:37 | Автор: admin

В принципе, я согласен с комментариями, что данная тема излишняя, так как существуют автоматические инструменты форматирования кода

И к тому же у каждого своё мнение о красоте и эстетичности, поэтому coding style носит субъективный характер.

Но я все таки решил закончить данную серию статей по Kotlin стилю, как и обещал.

Возможно кому-нибудь пригодится.

Ну что ж прошу под кат!

Именование

Идентификаторы переменных, функций, свойств, классов используют только ASCII буквы и цифры (обычные английские буквы + 10 цифр)

Специальные суффиксы или префиксы (например: _digit, power_) не используются (исключение: Backing свойства).

Имена функций

Начнем с именования функций.

Основное правило: имя функции должно быть написано в верблюжьем стиле (например: fetchDogs) и быть глаголом (makeRepost)

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

@Test fun get_emptyList() {  // ...}

Функции, которые аннотированы @Composable (Jetpack Compose смотрите здесь) носят имена, которые являются существительными, отформатированными в Pascal стиле:

@Composablefun MyDog(name: String) {    // }

Именование констант

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

Константы в Kotlin - это val свойства, которые не имеют кастомного оператора доступа get, а их контент является неизменяемым. Сюда относятся: неизменяемые коллекции и типы

(listOf(1, 2, 3)), а также скалярные типы, помеченные ключевым словом const:

// константы для скалярных типов должны быть объявлены с помощью constconst val FIVE = 5val MY_DOGS = listOf("Dina", "Red")val EMPTY_ARRAY = arrayOf()

Константы могут быть объявлены внутри конструкции object или как высокоуровневые определения (на уровне файла).

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

Именование свойств и переменных

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

val viewModel by viewModels<DogViewModel> { viewModelFactory }val firstName = "Anna"val dogs = listOf(Dog("Dina"), Dog("Red"))lateinit var binding: ListItemBindingfun fetchDogs(page: Int): List<Dog> {// ...}                                                                  

Когда вы используете backing свойство имя должно точно соответствовать реальному свойству только без нижнего подчеркивания:

    private val _dogs = MutableLiveData<List<Dog>>()    val dogs: LiveData<List<Dog>>        get() = _dogs

Имена generic типов должны соответствовать одному из стилей:

  • Одна заглавная буква (можно добавить одну цифру, например: T1, T2, R1)

  • Имя generic типа, который используется для класса может состоять из имени класса и дополнительного суффикса T (например: RequestT, ResponseT)

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

Именование классов и интерфейсов

Имена классов записываются в Pascal стиле (например: SleepingDog - каждое слово начинается с заглавной буквы).

Также они являются существительными или фразами (например: MyCar - состоит из двух слов)

Интерфейсы именуются по тем же правилам. Как дополнение они могут быть прилагательными (например: Cloneable, Readable, Writable):

class MainActivity(): AppCompatActivity() {  // ...}interface OnItemListener {fun onItemClick(id: Long)}interface Readable {}

Именование пакетов

Здесь ничего особеного: имена пакетов находятся в нижнем регистре и не содержат никаких нижних подчеркиваний:

// Неправильноpackage com.example.android.marsRealeState.overview// Неправильноpackage com.example.android.mars_reale_state.overview// OKpackage com.example.android.marsrealestate.overview

Специальные конструкции

Константы enum классов могут быть размещены на одной строке:

enum class NetworkStatus {   SUCCESS, FAILED, LOADING } 

Если вы решили поместить свои константы на отдельные строчки то следуйте рекомендации: разделяйте отдельные константы, которые содержат тело пустой строкой от других:

enum class NetworkStatus {  SUCCESS,  FAILED,    LOADING {  override fun toString() = "loading..."   }}

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

Что касается аннотаций, здесь правила предельно простые:

// аннотации размещаются на отдельных строчках @Singleton@Component(modules = [DatabaseModule::class])interface AppComponent {  // ...}// если аннотации не имеют параметров, то можно впихнуть их в одну строку@JvmField @Volatilevar disposable: Disposable? = null// если у вас одна простая аннотация то можно вот так:@Inject lateinit var viewModelFactory: DogViewModelFactory

Также возможен вариант более лаконичного синтаксиса для объявления переменных и функций:

// вместоoverride fun toString(): String = "My name is $name"// можноoverride fun toString() = "My name is $name"// вместоprivate val redDog: Dog = Dog("Red")// можноprivate val redDog = Dog("Red")

Документация

Базовое форматирование KDoc выглядит следующим образом:

/** * Здесь пишем документацию *  */fun fetchDogs(page: Int) {    // }

Если строка очень маленькая её можно уместить на одной строке:

/** Очень короткая строка моей документации */

Основные правила форматирования документации:

  • Используйте пустую строку, которая содержит только звездочку (*), чтобы разделять отдельные абзацы

  • Любые из стандартных блоков идут по порядку: @constructor,@receiver,@param,@property,@return,@throws,@seeи не могут быть пустыми

  • Каждый KDoc блок начинается со специального фрагмента, которые содержит основную информацию о классе или функции (например: "This function returns sum of digits"). Обычно это неполное предложение

Как минимум, KDoc документация используется для публичных методов, классов, полей, то есть public API.

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

// Полагаю, не требует объяснений.fun sum(a: Int, b: Int) = a + b

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

Исключение: KDoc документация не всегда присутствует в методе, который переопределяет метод из суперкласса.

Заключение

Эта статья завершает мой небольшой цикл.

Возможно я выбрал эту тему, потому что мне нечего делать, а возможно я просто хотел понять: стоит ли замарачиваться такими вещами, как форматирование кода, если есть встроенные инструменты Android Studio, которые сделают это лучше меня по всем правилам!

Я хотел бы попросить читателей о небольшой просьбе: написать комментарий "на какую тему вы бы хотели прочитать статью по Android разработке?"

Полезные ссылки:

  1. Официальное руководство по Kotlin стилю (на английском)

  2. Backing свойства

  3. Generic типы

  4. Jetpack Compose

  5. Документирование Kotlin кода (на английском)

  6. Дополнительная информация на Википедии

Подробнее..

Заметки о codestyle

10.02.2021 14:12:50 | Автор: admin

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

Второй возникающий вопрос: "Где научился так красиво писать?". Ответ на который будет к концу статьи.

Приведу три примера, с которыми сталкиваюсь чаще всего.

Пример 1

use App\Models\Payment;use Illuminate\Database\Eloquent\Model;class Foo extends Model {  public function user(){    return $this->hasOne('App\Models\User');  }  public function payments(){    return $this->hasMany(Payment::class);  }}

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

Начнём с того, что открывающиеся фигурные скобки лучше переносить на новую строку. Это позволит сделать код более приятным в чтении (Привет, PSR-12).

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

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

class Foo{    public function user()    {        //    }}

То у другого он может быть таким:

class Foo{        public function user()        {                //        }}

Или таким:

class Foo{  public function user()  {    //  }}

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

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

Итак, после ревью код будет выглядеть так:

use App\Models\Payment;use App\Models\User;use Illuminate\Database\Eloquent\Model;class Foo extends Model{    public function user()    {        return $this->hasOne(User::class);    }    public function payments()    {        return $this->hasMany(Payment::class);    }}

Пример 2

Недавно встретил такой участок кода (вставлю картинкой, чтобы не нарушить его вид):

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

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

Проведём код-ревью:

class NewsService{    public function sendNews(EmployerNews $employerNews)    {        $this->dispatch($employerNews, WorkPositionIdsJob::class);        $this->dispatch($employerNews, WorkPositionTypesJob::class);        $this->dispatch($employerNews, EmployerIdsJob::class);    }    protected function dispatch(EmployerNews $news, string $class): void    {        $job   = $this->job($news, $class);        $delay = $this->delay();        dispatch($job)->delay($delay);    }    protected function job(EmployerNews $news, string $job): AbstractJob    {        return new $job($news->getAttributes());    }    protected function delay(): Carbon    {        return Carbon::now()->addSecond();    }}

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

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

То же самое касается и примерно таких проблемных участков, как:

switch($value){    case 'foo':        //        break;    case 'bar':        //        break;}
if ($employerPriceCategory->default) {    return false;}$defaultCategory = EmployerPriceCategory::where('default', true)    ->first(['id']);Employer::query()    ->where('employer_price_category_id', $employerPriceCategory->id)    ->update([        'employer_price_category_id' => $defaultCategory->id,    ]);
$districts = $this->getDistricts();$this->output->writeln('districts: ' . sizeof($districts));$period = $this->getPeriod();foreach ($period as $start) {    $end = $start->copy()->endOfDay();    $this->output->writeln('date = ' . $start->format('Y-m-d'));    //}

Это же просто ужас! Как такое вообще можно читать? А писать?!

Давайте отрефакторим их!

switch($value){    case 'foo':        //        break;    case 'bar':        //        break;}
if ($employerPriceCategory->default) {    return false;}$defaultCategory = EmployerPriceCategory::query()    ->select(['id'])    ->where('default', true)    ->first();Employer::query()    ->where('employer_price_category_id', $employerPriceCategory->id)    ->update(['employer_price_category_id' => $defaultCategory->id]);

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

Пример 3

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

Логический блок - это участок кода состоящий из нескольких строк, объединённых между собой какой-либо общей чертой.

Для примера возьмём следующий участок:

$salaryFirstBaseEmployer = $employer->salaryBases->first()->salary_base;$salaryLastBaseEmployer = $employer->salaryBases->last()->salary_base;$begin = Carbon::parse('2020-05-01')->startOfMonth()->startOfDay();$end = $begin->clone()->endOfMonth()->endOfDay();$endMonth = $begin->clone()->endOfMonth();$startPeriod = new CarbonPeriod($begin, $end);$endPeriod = new CarbonPeriod($begin, $end);

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

$salaryFirstBaseEmployer = $employer->salaryBases->first()->salary_base;$salaryLastBaseEmployer  = $employer->salaryBases->last()->salary_base;$begin = Carbon::parse('2020-05-01')->startOfMonth()->startOfDay();$end      = $begin->clone()->endOfMonth()->endOfDay();$endMonth = $begin->clone()->endOfMonth();$startPeriod = new CarbonPeriod($begin, $end);$endPeriod   = new CarbonPeriod($begin, $end);

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

Заключение

Очень давно мне дали дельный совет:

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

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

Подробнее..

Системный подход к переменным в Ansible

07.08.2020 12:20:56 | Автор: admin

ansible devops codestyle


Hey! Меня зовут Денис Калюжный я работаю инженером в отделе автоматизации
процессов разработки. Каждый день новые сборки приложений раскатываются на сотнях
серверов кампании. И в этой статье я делюсь опытом использования Ansible для
этих целей.


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


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

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



Переменные в ролях


Роль это отдельный Объект системы деплоя. Как и любой объект системы, он
должен иметь интерфейс взаимодействия с остальной системой. Таким интерфейсом
являются переменные роли.
Возьмём, для примера, роль api, которая устанавливает Java приложение на
сервер. Какие переменные у неё могут быть?



Переменные роли можно разделить на 2 вида по типу:


1. Свойства    a) независимые от среды    б) зависимые от среды2. Связи    a) слушатели     б) запросы внутри системы    в) запросы в среду

Переменные свойства это переменные, которые определяют поведение роли.


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


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


С другой стороны, 1а, 2а, 2б это переменные, которые не зависят от среды
(железо, внешние ресурсы и т.д.) и могут быть заполнены дефолтными значениями в
defaults роли. Однако переменные типа 1.б и 2.в заполнить кроме как 'example'
значениями невозможно, так как они будут меняться от стенда к стенду в
зависимости от окружения.


Code style


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


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


  • Старайтесь не использовать словари для переменных. Ansible не позволяет
    удобно переопределять отдельные значения в словаре.
    Пример плохой переменной:


    myrole_user:    login: admin    password: admin
    

    Здесь login средонезависимая переменная, а password зависимая. Но
    поскольку они объединены в словарь, вам придётся задавать её полностью
    всегда. Что очень неудобно. Лучше так:


    myrole_user_login: adminmyrole_user_password: admin
    


Переменные в плейбуках деплоя


При составлении плейбука деплоя (далее плейбук), мы придерживаемся правила, что
он должен размещаться в отдельном репозитории. Так же, как и роли: каждая в своем
git репозитории. Это позволяет осозновать, что роли и плейбук это разные
независимые объекты системы деплоя, и изменения в одном объекте не должны влиять
на работу другой. Достигается это изменением дефолтных значений переменных.


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


mydeploy                        # Каталог деплоя deploy.yml                  # Плейбук деплоя group_vars                  # Каталог переменных плейбука  all.yml                 # Файл для переменных связи всей системы  myapi.yml               # Файл переменных свойств группы myapi inventories                 #     prod                    # Каталог окружения prod       prod.ini            # Инвентори файл       group_vars          # Каталог для переменных инвентори         myapi           #           vars.yml    # Средозависимые переменные группы myapi           vault.yml   # Секреты (всегда средозависимы) *

* Variables and Vaults


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


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


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


Переменные свойств для групп


Расширим нашу модель на рисунке 1, добавив 2 группы серверов с другим Java
приложением, но с разными настройками.



Представим, как будет выглядить плейбук в этом случае:


- hosts: myapi  roles:    - api- hosts: bbauth  roles:    - auth- hosts: ghauth  roles:    - auth

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


Code Style


  • Старайтесь вообще не использовать host_vars переменные, поскольку они не
    описывают систему, а только частный случай, что в перспективе приведёт к
    вопросам: "А почему этот хост отличается от остальных?", ответ на который не
    всегда легко найти.

Переменные связи


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


По началу была идея использовать монструозную конструкцию вида:
hostvars[groups['bbauth'][0]]['auth_bind_port'], но от неё сразу отказались
поскольку она имеет недостатки. Во-первых, громоздкость. Во-вторых, зависимость
от определенного хоста в группе. В-третьих, необходимо перед началом деплоя
собрать факты со всех хостов, если мы не хотим получить ошибку неопределённой
переменной.


В итоге решено было использовать переменные связи.


Переменные связи это переменные, которые принадлежат плейбуку, и нужны для
связи объектов системы.


Переменные связи заполняются в общих переменных системы group_vars/all/vars и
образуются путём выноса всех переменных слушателей из каждой группы, и
добавлением в начало переменной название группы откуда слушатель был вынесен.
Таким образом обеспечивается однотипность и непересекаемость имён.


Попробуем связать переменные из примера выше:



Представим, что у нас есть переменные, которые друг от друга зависят:


# roles/api/defaults:# Переменная запросаapi_auth1_address: "http://example.com:80"api_auth2_address: "http://example2.com:80"# roles/auth/defaults:# Переменная слушательauth_bind_port: "20000"

Вынесем в общие переменные group_vars/all/vars всех слушателей, и добавим в
название имя группы:


# group_vars/all/varsbbauth_auth_bind_port: "20000"ghauth_auth_bind_port: "30000"# group_vars/bbauth/varsauth_bind_port: "{{ bbauth_auth_bind_port }}"# group_vars/ghauth/varsauth_bind_port: "{{ ghauth_auth_bind_port }}"# group_vars/myapi/varsapi_auth1_address: "http://{{ bbauth_auth_service_name }}:{{ bbauth_auth_bind_port }}"api_auth2_address: "http://{{ ghauth_auth_service_name }}:{{ ghauth_auth_bind_port }}"

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


Code Style


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

Средозависимые файлы


В ролях могут использоваться файлы, которые отличаются от среды к среде.
Примером таких файлов можно назвать SSL-сертификаты. Хранить их в текстовом виде
в переменной не очень удобно. Зато удобно хранить путь до них внутри переменной.
Например, используем переменную api_ssl_key_file: "/path/to/file".


Поскольку очевидно, что сертификат ключа будет меняться от среды к среде, то это
средозависимая переменная, а значит она должна расположиться в файле
group_vars/myapi/vars инвентори переменных, и содержать значение 'для примера'.


Удобнее всего в этом случае положить файл ключа в репозиторий плейбука по пути
files/prod/certs/myapi.key, тогда значение переменной будет:
api_ssl_key_file: "prod/certs/myapi.key". Удобство же заключается в том, что
люди отвечающие за разворачивание системы на конкретном стенде, так же имеют
своё выделенное место в репозитории для хранения своих файлов. В то же время
остаётся возможность указать абсолютный путь до сертификата на сервере, на
случай если сертификаты поставляются другой системой.





Несколько стендов в одной среде


Часто возникает потребность развернуть несколько практически идентичных стендов
в одной среде с минимальными различиями. В этом случае мы делим средозависимые
переменные на те, что не меняются в рамках этой среды и те, что меняются. И
выносим последние непосредственно в сами инвентори файлы. После этой манипуляции
становится возможным создать ещё один инвентори прямо в каталоге окружения.
Он будет переиспользовать инвентори group_vars, а также иметь возможность
переопределить некоторые переменные непосредственно под себя.





Окончательная структура каталогов для проекта деплоя:


mydeploy                        # Каталог деплоя deploy.yml                  # Плейбук деплоя files                       # Каталог для файлов деплоя  prod                    # Католог для средозависимых файлов стенда prod   certs               #        myapi.key       #  test1                   # Каталог для средозависимых файлов стенда test1 group_vars                  # Каталог переменных плейбука  all.yml                 # Файл для переменных связи всей системы  myapi.yml               # Файл переменных свойств группы myapi  bbauth.yml              #   ghauth.yml              # inventories                 #     prod                    # Каталог окружения prod      group_vars          # Каталог для переменных инвентори       myapi           #        vars.yml    # Средозависимые переменные группы myapi        vault.yml   # Секреты (всегда средозависимы)       bbauth          #         vars.yml    #        vault.yml   #       ghauth          #           vars.yml    #           vault.yml   #      prod.ini            # Инвентори стенда prod     test                    # Каталог окружения test         group_vars          #          myapi           #           vars.yml    #           vault.yml   #          bbauth          #           vars.yml    #           vault.yml   #          ghauth          #              vars.yml    #              vault.yml   #         test1.ini           # Инвентори стенда test1 в среде test         test2.ini           # Инвентори стенда test2 в среде test

Подведение итога


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


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


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


Литература


  1. Документация

Автор


Калюжный Денис Александрович

Подробнее..

Какой могла бы быть Standard PHP Library

04.04.2021 20:17:04 | Автор: admin

Каждая статья с критикой PHP, следом за объяснением почему $ в обозначении переменной портит синтаксис, обязательно расскажет вам о том, как плохо организовано именование функций в ядре и расширениях. И если $ останется в стандарте языка и наших сердцах навсегда (хотя бы просто как напоминание о старом-добром PHP), то с именованием функций давно пора что-то делать.

TLDR;

Автор предлагает свои правила именования функций ядра PHP и распределение их по пространствам имен.

Standard PHP Library

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

Standard PHP Library (SPL) уже существует в качестве отдельного расширения. SPL предоставляет структуры данных, исключения, итераторы и многое другое, но к базовым функциям, например к функциям для работы со строками, никакого отношения не имеет. Они, на мой далёкий от устройства интерпретатора PHP взгляд, просто существуют где-то в ядре и являются как бы частью стандарта языка.

Я считаю, что базовые функции следует выделить в отдельное расширение и самым подходящим для него названием стало бы именно Standard PHP Library.

Проблемы

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

Наиболее заметные проблемы:

  1. одновременно существуют несколько подходов к именованию: с префиксом str_ (str_split), с префиксом str (strrev), с префиксом substr_ (substr_compare), без какого-либо префикса (trim);

  2. однородные функции имеют аргументы с разными именами, например, в strncmp два аргумента: str1 и str2, в strnatcasecmp: string1 и string2, а в substr_compare: haystack и needle;

  3. однородные функции имеют разный порядок следования однородных же аргументов все помнят историю про порядок аргументов в implode и explode;

  4. совершенно нечитаемые и непроизносимые имена функций: strtr, strncmp, strpbrk, strrchr и т.д.;

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

  6. зачастую функции возвращают false в случае ошибки или неверно заданных аргументов, например, strrpos;

  7. некоторые функции выглядят довольно экзотично и используются крайне редко: soundex, levenshtein и т.д.

Причины

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

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

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

Как бы это сделал я.
  1. PHP 8.2 создать и принять новый регламент об именовании функций и их аргументов;

  2. PHP 8.3 добавить новые функции согласно регламента, старые оставить в качестве псевдонимов, выпустить инструмент, который преобразует старые вызовы в новые;

  3. PHP 9 объявить старые функции deprecated, вынести новые и старые функции в отдельные расширения, при этом legacy расширение по умолчанию не будет идти в составе сборки,

  4. PHP 10 end of life для legacy расширения.

Учитывая, что между 7 и 8 версиями пролетело каких-то 5 лет, я думаю, что 10 лет экосистеме хватит на переход.

Что я предлагаю

Выделить функции из состава ядра в отдельные расширения

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

Добавить пространства имен для расширений

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

Я не буду выдумывать что-то новое и просто использую PSR-4. В качестве имени вендора очевидно следует использовать Php, а дальше добавить имя расширения. Таким образом, будущие функции стандартной библиотеки будут в пространстве имен Php\Spl.

Договориться о правилах именования функций и аргументов

Снова предлагаю существующие стандарты: частично использовать PSR-12 и частично правила именования функций от дядюшки Боба.

  1. Имена функций не должны содержать какого-либо явного префикса расширения, в котором определены. Иными словами, не должно быть никаких str_ или str, для этих целей служит пространство имен.

  2. Имена функций и аргументов должны быть заданы в lowerCamelCase.

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

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

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

Включить строгую проверку типов данных

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

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

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

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

  3. Для аргументов допустимо использовать union-типы и nullable-типы.

  4. Возвращаемое значение должно иметь один явный тип или быть null там, где это оправданно.

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

Практическая реализация

В качестве proof of concept я создал библиотеку, которая следует всем правилам, указанным выше, и предлагает новые имена функций для работы со строками https://github.com/marvin255/php-spl-proposal/blob/master/src/StringUtilities.php.

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

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

Пара примеров для ознакомления.

strnatcmp(test1, test2);// заменяется наStringUtilities::compareUsingNaturalOrder(test1, test2);
str_contains(test, t);// заменяется наStringUtilities::isContain(test, t);

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

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

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

Статья не претендует на какую-либо объективность. Более того, очевидно, что я не смог за пару вечеров исправить один из коренных недостатков PHP.

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

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

Подробнее..
Категории: Php , Codestyle

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru