Erlang. Рекомендации к формлению кода

в 7:11, , рубрики: erlang, Erlang/OTP, Песочница, Программирование, стилистика, метки: , ,

Не так давно, в команду пришлось приглашать нового программиста и знакомить его с Erlang. Для ускорения процесса обучения я решил перевести уже давно лежавший у меня материал Erlang Programming Rules and Conventions. Чем в принципе и хочу поделиться сами. Надеюсь что он будет полезен тем, кто собирается изучать или уже использует этот замечательный язык. Сразу скажу, что перевод вольный, так что не критикуйте сильно.

1. Цели

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

2. Структура и терминология Erlang

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

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

Потоки могут следить за работой друг друга посредством связывания (команда link). Когда поток заканчивает свое выполнение или падает автоматически посылается сигнал ко всем связанным потокам.
Реакцией по-умолчанию на этот сигнал для потока является немедленное прекращение выполнения операция.
Но это поведение может быть изменено установкой флага trap_exit, при котором сигналы преобразуются в сообщения и могут быть обработаны программой.

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

Побочный эффект обычно проявляется если функция:

  • Отправляет сообщения
  • Принимает сообщения
  • Вызывает exit
  • вызывает BIF который меняет окружение потока (такие как get/1, put/2, erase/1, process_flag/2)

3 Принципы проектирования

3.1 Экспортируйте как можно меньше функций из модуля

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

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

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

3.2 Пытайтесь уменьшить зависимости между модулями

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

Запомните, что в идеале вызовы модулей должны представлять собой дерево а не граф с циклами. (от себя. Для этого в инструменте для рефакторинга Wrangler есть удобный механизм, основанный на GraphViz, который может отрисовать граф вызова функций)

В итоге код вызова должен быть представлени примерно так:
image
А не:
image

3.3 Помещайте в библиотеки только часто используемый код.

В библиотеке (модуле) должен быть набор только связанных функций.

Задача разработика состоит в том, чтобы создаваемые библиотеки содержали функции одного и того же типа.

Таким образом к примеру библиотека lists, которая содержит код по работе со списками — пример хорошей архитектуры, а lists_and_method, которая содержит как код работы со списками, так и по математическим алгоритмам — плохой.

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

3.4 Изолируйте «хаки» и «грязный код» в отдельные модули

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

К примеру «грязный код»:

  • использует словарь процесса
  • использует erlang:process_info/1 для не совсем адекватных целей :)
  • делает что-то, что вы изначально не предполагали делать, но пришлось

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

3.5 Не думайте о том как вызывающая функция будет использовать результат

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

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

do_something(Args) ->
  case check_args(Args) of
    ok ->
      {ok, do_it(Args)};
    {error, What} ->
      String = format_the_error(What),
      io:format("* error:~sn", [String]), %% Don’t do this
      error
end.

Лучше сделать так:

do_something(Args) ->
  case check_args(Args) of
    ok ->
      {ok, do_it(Args)};
    {error, What} ->
      {error, What}
end.

error_report({error, What}) ->
  format_the_error(What).

В первом случае ошибка всегда выводится на консоль, во втором же — просто возвращается описание ошибки. Разработчик приложения может сам решить что делать с ней — проигнорировать или привести к печатному виду с помощью error_report/1. В любом случае за ним остается право выбора, что делать при возникновении ошибки.

3.6 Выделяйте общие шаблоны и поведения из логики кода

Если в коде какое-то поведение начинает повторяться, попытайтесь выделить его в отдельную функцию или модуль. Так его будет намного легче поддерживать, чем несколько копий разбросанных по программе.
Не используйте «Copy & Paste», используйте функции!

3.7 Проектирование «сверху вниз»

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

3.8 Не надо оптимизировать код

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

3.9 Пишите предсказуемый код

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

3.10 Пытайтесь избежать побочных эффектов

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

3.11 Не разрешайте внутренним данным покидать модуль

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

-module(queue).
-export([add/2, fetch/1]).
add(Item, Q) ->
  lists:append(Q, [Item]).
fetch([H|T]) ->
  {ok, H, T};
fetch([]) ->
  empty.

Она описывает очередь как список. И используется примерно так:

NewQ = [], % Не делайте так
Queue1 = queue:add(joe, NewQ),
Queue2 = queue:add(mike, Queue1), ....

Из этого вытекает несколько проблем:

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

Намного лучше писать так:

-module(queue).
-export([new/0, add/2, fetch/1]).
new() ->
  [].
add(Item, Q) ->
  lists:append(Q, [Item]).
fetch([H|T]) ->
  {ok, H, T};
fetch([]) ->
  empty.

Теперь использование будет выглядеть так:

NewQ = queue:new(),
Queue1 = queue:add(joe, NewQ),
Queue2 = queue:add(mike, Queue1), …

Этот код лишен недостатков описанных выше.

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

Len = length(Queue) % не пишите так

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

-module(queue).
-export([new/0, add/2, fetch/1, len/1]).
new() -> [].
add(Item, Q) ->
  lists:append(Q, [Item]).
fetch([H|T]) ->
  {ok, H, T};
fetch([]) ->
  empty.
len(Q) ->
  length(Q).

Теперь пользователь сможет взамен своему коду вызывать queue:len(Queue).
В данном коде мы «абстрагировались» от деталей реализации типа данных очередь. Т.е. другими словами очередь теперь — «абстрактный тип данных».
К чему вся эта морока? На практике, получился легкий механизм для замены внутреннего представления, и теперь можно поменять реализацию своего модуля не боясь нарушить обратную совместимость с пользовательским кодом. Например для лучшей реализации код переписан так:

-module(queue).
-export([new/0, add/2, fetch/1, len/1]).
new() ->
  {[],[]}.
add(Item, {X,Y}) -> % Вставка ускорена
  {[Item|X], Y}.
fetch({X, [H|T]}) ->
  {ok, H, {X,T}};
fetch({[], []) ->
  empty;
fetch({X, []) ->
  % Выполняем эти тяжелые вычисления только иногда
  fetch({[],lists:reverse(X)}).
len({X,Y}) ->
  length(X) + length(Y).

3.12 Делайте код детерминированным настолько, на сколько это возможно

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

3.13 Не надо постоянно проверять корректность входных данных

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

%% Args: Option is all|normal
get_server_usage_info(Option, AsciiPid) ->
  Pid = list_to_pid(AsciiPid),
  case Option of
    all -> get_all_info(Pid);
    normal -> get_normal_info(Pid)
  end.

Функция завершит выполнение ошибкой, если Option это не normall или all. Так и должно быть! Тот кто вызывает эту функцию должен контролировать то, что ей передать.

3.14 Изолируйте логику работы с аппаратным средствами в драйвер

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

3.15 Отменяйте действия в той же функции, что и делаете

Предположим, что есть код, который открывает файл, что-от с ним делает и потом закрывает.

do_something_with(File) ->
	case file:open(File, read) of,
		{ok, Stream} ->
			doit(Stream),
			file:close(Stream) % Правильное решение
		Error -> Error
	end.

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

do_something_with(File) ->
  case file:open(File, read) of,
    {ok, Stream} ->
      doit(Stream)
    Error -> Error
  end.

doit(Stream) ->
  ....,
  func234(...,Stream,...).
  ...

func234(..., Stream, ...) ->
  ...,
  file:close(Stream) %% Не делайте так

4 Обработка ошибок

4.1 Отделяйте обработку от нормального кода

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

4.2 Выделяйте «ядро ошибок»

Одним их этапов разработки является определение того какая часть системы всегда должна функционировать без ошибок, а в какой ошибки допустимы.
К примеру, в операционных системах ядро должно функционировать без ошибок, в то время как обычные приложения могут упасть с ошибкой, но это не отразится на функционировании системы в целом.
Та часть которая должна всегда работать корректно мы называем «ядро ошибок». Эта часть, как правило, потсоянно сохраняет свое промежуточное состояние в БД или на диск.

5 Потоки, серверы и сообщения

5.1 Реализуйте весь код потока в одном модуле

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

5.2 Используйте потоки для построения системы

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

5.3 Зарегистрированные потоки

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

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

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

5.5 У каждого потока должна быть только одна «роль»

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

  • супервизор (supervisor) — наблюдает за потоками и перезапускает их после падения;
  • рабочий (worker) — обычный исполнительный поток, в котором могут возникать ошибки;
  • доверенный поток (или системный, trusted worker) — поток, в котором не должно возникать ошибок.
5.6 Используйте generic функции для создания серверов и работы с протоколами всегда, когда это возможно

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

5.7 Добавляйте тэги к сообщениям

У всех сообщений должны быть тэги. Тэгированными сообщениями легче управлять, т. к. не важен их порядок в блоке receive, и легче добавлять поддержку новых сообщений.
Не стоит писать такой код:

loop(State) ->
	receive
	...
	{Mod, Funcs, Args} -> % Не делайте так
		apply(Mod, Funcs, Args},
	loop(State);
	...
end.

Если возникнет необходимость добавить обработку сообщения {get_status_info, From, Option} и его обработку разместить под первым блоком, то возникнет конфликт и новый код не выполниться никогда.
Если сообщения синхронные, то результат тоже должен быть помечен тэгом, но отличным от запроса. Например: при запросе с тэгом get_status_info, должен придти ответ status_info. Соблюдение этого правила существенно облегчает отладку.
Вот пример правильного кода:

loop(State) ->
receive
	...
	{execute, Mod, Funcs, Args} -> % Используются тэгированные сообщени
		apply(Mod, Funcs, Args},
		loop(State);
	{get_status_info, From, Option} ->
		From ! {status_info, get_status_info(Option, State)}, % Тэгированный ответ
	loop(State);
	...
end.

5.8 Очищайте очередь от неизвестных вашему коду сообщений

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

main_loop() ->
	receive
	{msg1, Msg1} ->
		...,
		main_loop();
	{msg2, Msg2} ->
		...,
		main_loop();
	Other -> % Очистка очереди
		error_logger:error_msg(
		"Error: Process ~w got unknown msg ~w~n.",[self(), Other]),
		main_loop()
end.

5.9 Используйте хвостовую рекурсию при написании серверов

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

loop() ->
	receive
		{msg1, Msg1} ->
			...,
			loop();
		stop ->
			true;
	Other ->
		error_logger:log({error, {process_got_other, self(), Other}}),
		loop()
	end,
	io:format("Server going down").  % не делайте так
					 % Это не хвостовая рекурсия!

Лучше так:

loop() ->
	receive
		{msg1, Msg1} ->
			...,
			loop();
		stop ->
			io:format("Server going down");
		Other ->
			error_logger:log({error, {process_got_other, self(), Other}}),
			loop()
	end. % А вот это хвостовая рекурсия

5.10 Создавайте интерфейс доступа к своему серверу

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

-module(fileserver).
-export([start/0, stop/0, open_file/1, ...]).
open_file(FileName) ->
	fileserver ! {open_file_request, FileName},
	receive
		{open_file_response, Result} -> Result
	end.
...<code>...

5.11 Таймауты

Будьте предельно осторожны при использовании after при получении сообщений. Убедитесь, что вы предусмотрели ситуацию когда сообщение придет уже после таймаута. (см. п. 5.8).

5.12 Перехват сигнала выхода

Как можно меньше потоков должны выставлять trap_exit флаг. Но использовать его или нет, это все же зависит от назначения модуля.

6 Рекомендации к коду Erlang

6.1 Используйте record для хранения структур

Тип данных record это тэгированный кортеж во внутреннем представлении Erlang. Впервые он появился в Erlang 4.3. Record очень похож на struct в C или record в Pascal.
Если record планируется использовать в нескольких модулях, то его определение следует поместить в заголовочный файл.
Record лучше всего использовать для обеспечения совместимости модулей при передаче данных между ними.

6.2 Используйте селекторы и конструкторы

Используйте селекторы и конструкторы для управления record. Никогда не используйте record как кортэж.
Пример:

demo() ->
	P = #person{name = "Joe", age = 29},
	#person{name = Name1} = P,% matching или...
	Name2 = P#person.name. % селектор

Не стоит использовать record так:

demo() ->
	P = #person{name = "Joe", age = 29},
	{person, Name, _Age, _Phone, _Misc} = P. % Не стоит так делать

6.3 Возвращайте тэгированный результат функции

Не стоит писать так:

keysearch(Key, [{Key, Value}|_Tail]) ->
	Value; %% Не возвращайте нетэгированное значение!
keysearch(Key, [{_WrongKey, _WrongValue} | Tail]) ->
	keysearch(Key, Tail);
keysearch(Key, []) ->
	false.

При таком решении {Key, Value} не может содержать false в качестве значения. Вот правильный код:

keysearch(Key, [{Key, Value}|_Tail]) ->
	{value, Value}; %% Все исправлено.
keysearch(Key, [{_WrongKey, _WrongValue} | Tail]) ->
	keysearch(Key, Tail);
keysearch(Key, []) ->
	false.

6.4 Используйте с осторожностью catch и throw

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

6.5 Используйте словарь процесса с предельной осторожностью

Не используйте get и put в случае если вы не уверены в том, что вы делаете.
Функции которые используют put и get могут быть достаточно просто переписаны добавлением еще одного аргумента.
Не стоит писать так:

tokenize([H|T]) ->
	...;
tokenize([]) ->
	case get_characters_from_device(get(device)) of % Не используйте get/1!
		eof -> [];
		{value, Chars} ->
			tokenize(Chars)
	end.

Лучше переписать:

tokenize(_Device, [H|T]) ->
	...;
	tokenize(Device, []) ->
		case get_characters_from_device(Device) of % Так лучше
		eof -> [];
		{value, Chars} ->
			tokenize(Device, Chars)
	end.

Использование put и get делает функцию не детерминированной. В этом случае значительно осложняется отладка, т.к. результат выполнения функции зависит не только от входных данных но и от словаря процесса. Тем более Erlang при ошибке (к примеру bad_match) выводит в описании тоько аргументы вызовов функции, а не состояние словаря потока на тот момент.

6.6 Не используйте import

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

6.7 Экспортирование функций

Следует различать то, зачем экспортируются функции:

  1. Для предоставления возможности внешнего обращения к ним
  2. Для предоставления интерфейса пользователю
  3. Для вызовов через spawn или apply изнутри самого модуля.

Группируйте -export и комментируйте причину экспорта.

%% интерфейса пользователю
-export([help/0, start/0, stop/0, info/1]).

%%  для внешнего обращения
-export([make_pid/1, make_pid/3]).
-export([process_abbrevs/0, print_info/5]).

%% для внутреннего использования
-export([init/1, info_log_impl/1]).

7. Рекомендации для стилистического и лексического оформления кода

7.1 Не пишите сильно вложенный код

Под сильно вложенным понимается код, в котором много блоков case / if / receive вложено в другие блоки case / if / receive. Считается плохим тоном наличие более двух уровней вложенности. Такой код тяжело читается. Лучше разбивать такой код на мелкие функции.

7.2 Не пишите слишком большие модули

Модуль не должен содержать более 400 строк кода. Лучше разбивать такие модули на несколько маленьких.

7.3 Не пишите слишком длинные функции

Функция не должна быть длиннее 15-20 строк. Не пытайтесь решить проблему написанием всего в одну строчку.

7.4 Не стоит писать много символов в одной строке

Число символов в строке должно быть не больше 80 ( чтобы поместиться на A4 ).
В Erlang строки, объявленные после переноса, автоматически склеиваются.
Например:

io:format("Name: ~s, Age: ~w, Phone: ~w ~n"
	  "Dictionary: ~w.~n", [Name, Age, Phone, Dict])

7.5 Именовение переменных

Выбрать правильное(значимое) имя для переменной — достаточно трудная задача.
Если имя переменно состоит из нескольких слов лучше их разделять либо через '_' либо использовать CamelCase
Не используйте '_' для игнорирования переменной в функции, лучше пишите название переменнной, начинающееся с '_', например _Name.

7.6 Именование функций

Название функции должно четко отражать её назначение. Результат выполнения должен быть предсказуем исходя из названия. Следует использовать стандартные имена для стандартных функций (такие как start,stop,init,main_loop …).
Функции из разных модулей, но с одинаковым назначением должны называться одинаково (например, Module:module_info()).
Неправильный выбор имени функции — самая распространенная ошибка.
Некоторые соглашения в именах функций намного облегчают этот выбор. К примеру префикс 'is_' говорит от том, что результатом выполнения будет true или false.

is_...() -> true | false
check_...() -> {ok, ...} | {error, ...}

7.7 Именование модулей

В Erlang плоская модель именования модулей, т.е. нет пакетов как, например, в Java (но, если быть точным, есть, но их применение на практике пока приносит больше проблем, чем пользы).
Обычно для обозначения того, что модули как-то связаны используют одинаковые префиксы. Например ISDN реализован так:
isdn_init
isdn_partb
isdn_…

7.8 Оформляйте код единообразно

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

{12, 23, 45}

кто-то без

{12,23,45}

Как только вы выработаете для себя единый стиль — придерживайтесь его.

8 Документирование кода

8.1 Атрибуты кода

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

-revision('Revision: 1.14 ').
-created('Date: 1995/01/01 11:21:11 ').
-created_by('eklas@erlang’).
-modified('Date: 1995/01/05 13:04:07 ').
-modified_by('mbj@erlang').

8.2 Оставляйте ссылки на спецификацию

Если код реализует какой-то стандарт, обязательно оставляйте ссылку на документацию (к примеру RFC).

8.3 Документируйте все ошибки

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

error_logger:error_msg(Format, {DescriptorArg1Arg2, ....})

И убедитесь, что {Descriptor, Arg1, Arg2, ....} описаны в документе «Описание ошибок».

8.4 Документируйте все типы данных, которые передаются в сообщениях

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

8.5 Комментируйте код

Комментарии не должны быть многословны, но должны быть достаточны для понимания вашего кода. Всегда поддерживайте их в актуальном состоянии.
Комментарии касательно модулей должны быть без отступов и начинаться с %%%.
Комментарии касательно функций должны быть без отступов и начинаться с %%.
Комментарии к коду должны быть выровнены также как и он, и начинаться с %. Такие комментарии должны находится над кодом или на одной линии с ним. На одной линии более предпочтительно (если помещается).

%% Comment about function
some_useful_functions(UsefulArgugument->
    another_functions(UsefulArgugument),  % Comment at end of line
    % Comment about complicated_stmnt at the same level of indentation
    complicated_stmnt,

8.6 Комментируйте каждую функцию

Важно документировать:

  • цели этой функции
  • допустимую область входных данных
  • область выходных данных
  • Если функция реализует какой-то алгоритм-опишите его
  • Возможные падения этой функции, наличие вызовов exit/1 и throw/1, возможные ошибки.
  • Побочные эфекты

Пример:

%%----------------------------------------------------------------------
%% Function: get_server_statistics/2
%% Purpose: Get various information from a process.
%% Args: Option is normal|all.
%% Returns: A list of {Key, Value}
%% or {error, Reason} (if the process is dead)
%%----------------------------------------------------------------------
get_server_statistics(OptionPidwhen pid(Pid->

8.7 Структуры данных

Record должен быть определен вместе с описанием. Например:

%% File: my_data_structures.h
%%---------------------------------------------------------------------
%% Data Type: person
%% where:
%% name: A string (default is undefined).
%% age: An integer (default is undefined).
%% phone: A list of integers (default is []).
%% dict: A dictionary containing various information about the person.
%% A {Key, Value} list (default is the empty list).
%%----------------------------------------------------------------------
-record(person, {name, age, phone = [], dict = []}).

8.8 Заголовок файла, копирайт

Каждый файл должен начинаться информации о копирайте. Например:

%%%---------------------------------------------------------------------
%%% Copyright Ericsson Telecom AB 1996
%%%
%%% All rights reserved. No part of this computer programs(s) may be
%%% used, reproduced,stored in any retrieval system, or transmitted,
%%% in any form or by any means, electronic, mechanical, photocopying,
%%% recording, or otherwise without prior written permission of
%%% Ericsson Telecom AB.
%%%---------------------------------------------------------------------

8.9 Заголовок файла, история версий

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

%%%---------------------------------------------------------------------
%%% Revision History
%%%---------------------------------------------------------------------
%%% Rev PA1 Date 960230 Author Fred Bloggs (ETXXXXX)
%%% Intitial pre release. Functions for adding and deleting foobars
%%% are incomplete
%%%---------------------------------------------------------------------
%%% Rev A Date 960230 Author Johanna Johansson (ETXYYY)
%%% Added functions for adding and deleting foobars and changed
%%% data structures of foobars to allow for the needs of the Baz
%%% signalling system
%%%---------------------------------------------------------------------

8.10 Заголовок файла, описание

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

%%%---------------------------------------------------------------------
%%% Description module foobar_data_manipulation
%%%---------------------------------------------------------------------
%%% Foobars are the basic elements in the Baz signalling system. The
%%% functions below are for manipulating that data of foobars and for
%%% etc etc etc
%%%---------------------------------------------------------------------
%%% Exports
%%%---------------------------------------------------------------------
%%% create_foobar(Parent, Type)
%%% returns a new foobar object
%%% etc etc etc
%%%---------------------------------------------------------------------

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

8.11 Не комментируйте старый код — удаляйте его

Удаляйте старый код и оставляйте описание в системе контроля версий. CSV поможет вам.

8.12 Используйте системы контроля версий

Все более менее сложные проекты должны использовать контроль версий (Git, SVN и т.п.)

9 Частые ошибки

Написание функций на несколько страниц
Написание функций с множеством вложенных case / if / receive
Написание нетэгированных функций
Имена функций не соответствуют и функциональному назначению
Плохие имена переменных
использование потоков, когда в это нет необходимости
Неправильный выбор структур данных (плохое представление)
Плохое комментирование или отсутствие комментариев
Код без отступов
Использование put / get
Плохой контроль очереди сообщений

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

В этом блоке описана документация которая необходима для проекта для го поддержки и администрирования.

10.1 Описание модулей

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

  • назначение
  • описание входных данных
  • описание выходных данных
  • возможные проблемы использования и вызовы exit/1.

10.2 Описание сообщени

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

10.3 Описание потоков

Описание всех зарегистрированных потоков и интерфейсов к ним.
Описание всех динамически создаваемых потоков и интерфейсов к ним.

10.4 Описание ошибок

Описание всех возможных ошибок системы.

Автор: egobrain

Поделиться

* - обязательные к заполнению поля