- PVSM.RU - https://www.pvsm.ru -
Не так давно, в команду пришлось приглашать нового программиста и знакомить его с Erlang. Для ускорения процесса обучения я решил перевести уже давно лежавший у меня материал Erlang Programming Rules and Conventions [1]. Чем в принципе и хочу поделиться сами. Надеюсь что он будет полезен тем, кто собирается изучать или уже использует этот замечательный язык. Сразу скажу, что перевод вольный, так что не критикуйте сильно.
В этой статье перечислены некоторые аспекты, которые должны быть учтены при разработке ПО с использованием Erlang.
Все подсистемы Erlang разбиты на модули. модули в свою очередь состоят из функций и атрибутов.
Функции могут быть как внутренними, так и экспортированными для применения в сторонних модулях.
Атрибуты начинается с '-' и помещаются в начало модуля.
Все работы в архитектуре Erlang осуществляются потоками. Поток в свою очередь — это набор инструкций, который может использовать вызовы функций различных модулей. Потоки могут взаимодействовать между собой посредством посылки сообщений. Также, при получении, они могут определять какие сообщения необходимо обработать в первую очередь. Остальные же сообщения остаются в очереди до тех пор пока в них
не возникнет необходимость.
Потоки могут следить за работой друг друга посредством связывания (команда link). Когда поток заканчивает свое выполнение или падает автоматически посылается сигнал ко всем связанным потокам.
Реакцией по-умолчанию на этот сигнал для потока является немедленное прекращение выполнения операция.
Но это поведение может быть изменено установкой флага trap_exit, при котором сигналы преобразуются в сообщения и могут быть обработаны программой.
Чистые функции — это функции значение которых зависит только от входных данных и не зависит от контекста вызова. Этим поведением они похожи на обычные математические. О тех же, которые не являются чистыми говорят, что это функции с побочными эффектами.
Побочный эффект обычно проявляется если функция:
Модули являются базовой структурной единицей в Erlang. Они могут содержать огромное количество функций, но только те, что включены в список экспорта будут доступны сторонним модулям.
Сложность модуля напрямую зависит от количества экспортируемых функций. Согласитесь, ведь легче разобраться с модулем который экспортирует пару функций, чем тот, который экспортирует дюжину.
Ведь пользователю требуется разобраться только теми, что экспортированны.
Кроме того, тому, кто сопровождает код модуля можно не опасаясь менять внутренние функции, ведь интерфейс обращения к модулю остается неизменным.
Модуль, который обращается ко множеству других, намного труднее поддерживать, потому что, если меняется
интерфейс, то необходимо менять код везде, где использовался этот модуль.
Запомните, что в идеале вызовы модулей должны представлять собой дерево а не граф с циклами. (от себя. Для этого в инструменте для рефакторинга Wrangler [2] есть удобный механизм, основанный на GraphViz, который может отрисовать граф вызова функций)
В итоге код вызова должен быть представлени примерно так:
А не:
В библиотеке (модуле) должен быть набор только связанных функций.
Задача разработика состоит в том, чтобы создаваемые библиотеки содержали функции одного и того же типа.
Таким образом к примеру библиотека lists, которая содержит код по работе со списками — пример хорошей архитектуры, а lists_and_method, которая содержит как код работы со списками, так и по математическим алгоритмам — плохой.
Лучше всего, когда все функции в библиотеке не содержат побочных эффектов. Тогда ее можно с легкостью использовать повторно.
Часто решение проблемы требует совместного использования «чистого» и «грязного» кода.
Разделяйте такие участки в разные модули.
К примеру «грязный код»:
Старайтесь писать как можно больше чистого кода, а «грязный», во-первых используйте как можно меньше, во-вторых документируйте все возможные побочные эффекты и проблемы использования.
Не думайте о том как вызывающая функция будет использовать результат выполнения вашей.
К примеру, предположим, вы вызываете функцию 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. В любом случае за ним остается право выбора, что делать при возникновении ошибки.
Если в коде какое-то поведение начинает повторяться, попытайтесь выделить его в отдельную функцию или модуль. Так его будет намного легче поддерживать, чем несколько копий разбросанных по программе.
Не используйте «Copy & Paste», используйте функции!
Разрабатывайте программу по принципу сверху вниз. Это позволит постепенно детализировать код, до тех пор пока вы не дойдете примитивных функций. Код, написанный таким образом не зависит от представления данных, т. к. представление еще не известно когда вы ведете разработку на более высоком уровне.
Не оптимизируйте код на начальной стадии разработки. Сначала заставьте его правильно работать, а уже потом, если появится необходимость, займитесь оптимизацией.
Результат выполнения функций не должен стать сюрпризом для пользователя. Функции, которые выполняют сходные действия должны выглядеть одинаково или хотя бы быть похожи. Пользователь должен иметь возможность предугадать что может вернуть функция. Если результат выполнения не оправдывает ожиданий, то значит либо название функции не соответствует ее назначению, либо сам код функции работает не верно.
В Erlang есть несколько примитивов с побочными эффектами. Функции, которые их используют не так просто повторно использовать, т. к. они они зависят от окружения и программисту необходимо контролировать состояние потока перед вызовом.
Пишите как можно больше кода без побочных эффектов.
Четко документируйте все возможные проблемы при использовании не чистых функций. Это сильно облегачает написание тестов и поддержку.
Легче всего продемонстрировать на примере. Предположим есть функция управления очередью:
-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).
Детерминированные программы всегда ведут себя одинаково не зависимо от того сколько раз их запускали. Не детерминированные могут вести себя по разному при каждом запуске. Первый вариант намного легче для отладки и позволяет отследить и избежать множество ошибок, так что старайтесь придерживаться его.
Например, программа должна запустить 5 разных потоков, а затем проверить, что они запустились, притом порядок запуска не важен.
Можно запустить все 5 сразу, а потом проверить, но лучше запускать их по очереди, каждый раз проверяя запустился ли поток и только после этого запускать следующий.
Часто программисты не доверяют входным данным для части системы, которую они разрабатывают. Большая часть кода не должна заботиться о корректности передаваемых ей данных. Проверка должна происходить только тогда, когда данные передаются извне.
Например:
%% 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. Так и должно быть! Тот кто вызывает эту функцию должен контролировать то, что ей передать.
Оборудование должно быть изолировано от системы с помощью драйвера. Задача драйвера представить аппаратуру в системе так, как будто это просто Erlang поток, который так же как и простой поток принимает и отсылает сообщения и реагирует на ошибки.
Предположим, что есть код, который открывает файл, что-от с ним делает и потом закрывает.
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) %% Не делайте так
Не смешивыйте нормальный код с кодом обработки ошибок. Вы должны программировать нормальное поведение. Если что-то пойдет не так, процесс должен тут же завершиться с ошибкой. Не пытайтесь исправить ошибку и продолжить. Обработкой ошибок должен занимать другой процесс.
Одним их этапов разработки является определение того какая часть системы всегда должна функционировать без ошибок, а в какой ошибки допустимы.
К примеру, в операционных системах ядро должно функционировать без ошибок, в то время как обычные приложения могут упасть с ошибкой, но это не отразится на функционировании системы в целом.
Та часть которая должна всегда работать корректно мы называем «ядро ошибок». Эта часть, как правило, потсоянно сохраняет свое промежуточное состояние в БД или на диск.
Конечно надо понимать, что поток вызывает функции из других модулей, но здесь речь идет о том, что главный цикл потока должен быть в одном модуле, а не разбит на несколько. Иначе будет тяжело контролировать ход выполнения. Это так же не означает, что не стоит использовать generic библиотеки. Они как раз и предназначены для помощи в организации потока.
Также не стоит забывать, что код, общий для нескольких потоков, должен быть реализован в своем совершенно отдельном модуле.
Потоки — это базовые структурные элементы системы. Но не используйте их и отправку сообщения, когда возможно просто вызвать функцию.
Потоки должны быть зарегистрированы под тем же именем, что название модуля, который содержит их код. Это значительно облегчит поддержку. Регистрируйте только те потоки, который планируется использовать длительное время.
Использовать потоки или решать задачу последовательно зависит напрямую от проблемы, которую мы стараемся решить. Но желательно руководствоваться одним главным правилом:
«Используйте один поток для каждого параллельного процесса в реальном мире».
Если соблюдать это правило, то логику программы будет легко понять.
Потоки могут выполнять разные роли в системе. Например, в архитектуре клиент-сервер поток может быть и клиентом и сервером, но лучше разделить две роли на два потока.
Другие роли:
В большинстве случаев использовать gen_server для создания серверов является самым лучшим решением, т. к. это сильно облегчит структуру системы. Это верно и для обработки протоколов передачи данных.
У всех сообщений должны быть тэги. Тэгированными сообщениями легче управлять, т. к. не важен их порядок в блоке 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.
Каждый сервер воизбежание переполнения очереди должен обрабатывать все приходящие к нему сообщения к примеру так:
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.
Все сервера обязаны быть реализованы с использованием хвостовой рекурсии, дабы избежать переполнения памяти.
Не стоит писать так:
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. % А вот это хвостовая рекурсия
Используйте функции для доступа к серверу взамен прямой отправки сообщений всегда, когда это возможно.
Протокол обмена сообщениями это внутренняя информация и она не должна быть доступна другим модулям.
Пример:
-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>...
Будьте предельно осторожны при использовании after при получении сообщений. Убедитесь, что вы предусмотрели ситуацию когда сообщение придет уже после таймаута. (см. п. 5.8).
Как можно меньше потоков должны выставлять trap_exit флаг. Но использовать его или нет, это все же зависит от назначения модуля.
Тип данных record это тэгированный кортеж во внутреннем представлении Erlang. Впервые он появился в Erlang 4.3. Record очень похож на struct в C или record в Pascal.
Если record планируется использовать в нескольких модулях, то его определение следует поместить в заголовочный файл.
Record лучше всего использовать для обеспечения совместимости модулей при передаче данных между ними.
Используйте селекторы и конструкторы для управления 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. % Не стоит так делать
Не стоит писать так:
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.
Не используйте catch и throw в случае если вы не уверены в том, что вы делаете.
Catch и throw могут быть полезны при обработке внешних входных данных, или данных которые требуют сложной обработки (например оьработка текста компилятором).
Не используйте 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) выводит в описании тоько аргументы вызовов функции, а не состояние словаря потока на тот момент.
Код с импорт тяжелее читать. Лучше чтобы все определения функций модуля были в одном файле.
Следует различать то, зачем экспортируются функции:
Группируйте -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]).
Под сильно вложенным понимается код, в котором много блоков case / if / receive вложено в другие блоки case / if / receive. Считается плохим тоном наличие более двух уровней вложенности. Такой код тяжело читается. Лучше разбивать такой код на мелкие функции.
Модуль не должен содержать более 400 строк кода. Лучше разбивать такие модули на несколько маленьких.
Функция не должна быть длиннее 15-20 строк. Не пытайтесь решить проблему написанием всего в одну строчку.
Число символов в строке должно быть не больше 80 ( чтобы поместиться на A4 ).
В Erlang строки, объявленные после переноса, автоматически склеиваются.
Например:
io:format("Name: ~s, Age: ~w, Phone: ~w ~n"
"Dictionary: ~w.~n", [Name, Age, Phone, Dict])
Выбрать правильное(значимое) имя для переменной — достаточно трудная задача.
Если имя переменно состоит из нескольких слов лучше их разделять либо через '_' либо использовать CamelCase [3]
Не используйте '_' для игнорирования переменной в функции, лучше пишите название переменнной, начинающееся с '_', например _Name.
Название функции должно четко отражать её назначение. Результат выполнения должен быть предсказуем исходя из названия. Следует использовать стандартные имена для стандартных функций (такие как start,stop,init,main_loop …).
Функции из разных модулей, но с одинаковым назначением должны называться одинаково (например, Module:module_info()).
Неправильный выбор имени функции — самая распространенная ошибка.
Некоторые соглашения в именах функций намного облегчают этот выбор. К примеру префикс 'is_' говорит от том, что результатом выполнения будет true или false.
is_...() -> true | false
check_...() -> {ok, ...} | {error, ...}
В Erlang плоская модель именования модулей, т.е. нет пакетов как, например, в Java (но, если быть точным, есть, но их применение на практике пока приносит больше проблем, чем пользы).
Обычно для обозначения того, что модули как-то связаны используют одинаковые префиксы. Например ISDN реализован так:
isdn_init
isdn_partb
isdn_…
Единообразный стиль позволяет разработчикам легче понимать код друг друга.
Все люди привыкли писать код по разному.
К примеру кто-то определяет элементы кортежа через пробел
{12, 23, 45}
кто-то без
{12,23,45}
Как только вы выработаете для себя единый стиль — придерживайтесь его.
Необходимо всегда правильно задавать атрибуты модуля в заголовке. Описывать откуда возникла идея этого модуля, если код появился в результате работы на другим модулем, описать это.
Никогда не воруйте код. Воровство — это копирование кода без указания первоисточника.
Примеры атрибутов:
-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').
Если код реализует какой-то стандарт, обязательно оставляйте ссылку на документацию (к примеру RFC).
Все ошибки должны быть понятно описаны в отдельном документе.
В коде в местах где возможна логическая вызывайте error_logger:
error_logger:error_msg(Format, {Descriptor, Arg1, Arg2, ....})
И убедитесь, что {Descriptor, Arg1, Arg2, ....} описаны в документе «Описание ошибок».
Используйте тэгированные кортежи в качестве сообщений пересылаемых потоками.
Использование record в сообщениях гарантирует совместимость между различными модулями.
Документируйте все эти типы данных документе «Описание сообщений».
Комментарии не должны быть многословны, но должны быть достаточны для понимания вашего кода. Всегда поддерживайте их в актуальном состоянии.
Комментарии касательно модулей должны быть без отступов и начинаться с %%%.
Комментарии касательно функций должны быть без отступов и начинаться с %%.
Комментарии к коду должны быть выровнены также как и он, и начинаться с %. Такие комментарии должны находится над кодом или на одной линии с ним. На одной линии более предпочтительно (если помещается).
%% 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,
…
Важно документировать:
Пример:
%%----------------------------------------------------------------------
%% 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(Option, Pid) when pid(Pid) ->
…
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 = []}).
Каждый файл должен начинаться информации о копирайте. Например:
%%%---------------------------------------------------------------------
%%% 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.
%%%---------------------------------------------------------------------
В каждом файле должна быть история версий, в которой показано кто сделал какие изменения и с какой целью.
%%%---------------------------------------------------------------------
%%% 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
%%%---------------------------------------------------------------------
Каждый файл должен начинаться с короткого описания назначения модуля и списка экспортируемых функций.
%%%---------------------------------------------------------------------
%%% 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
%%%---------------------------------------------------------------------
Если вам известны какие-либо проблемы с использование кода или что-то не завершено, описывайте их здесь. Это поможет в дальнейшем поддержке вашего кода.
Удаляйте старый код и оставляйте описание в системе контроля версий. CSV поможет вам.
Все более менее сложные проекты должны использовать контроль версий (Git, SVN и т.п.)
Написание функций на несколько страниц
Написание функций с множеством вложенных case / if / receive
Написание нетэгированных функций
Имена функций не соответствуют и функциональному назначению
Плохие имена переменных
использование потоков, когда в это нет необходимости
Неправильный выбор структур данных (плохое представление)
Плохое комментирование или отсутствие комментариев
Код без отступов
Использование put / get
Плохой контроль очереди сообщений
В этом блоке описана документация которая необходима для проекта для го поддержки и администрирования.
Одна глава на модуль. Она должна содержать описание каждого модуля и описание всех экспортируемых им функций:
Описание сообщений, используемых в системе, кроме тех, которые используются только внутри модулей
Описание всех зарегистрированных потоков и интерфейсов к ним.
Описание всех динамически создаваемых потоков и интерфейсов к ним.
Описание всех возможных ошибок системы.
Автор: egobrain
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/6029
Ссылки в тексте:
[1] Erlang Programming Rules and Conventions: http://www.erlang.se/doc/programming_rules.shtml#HDR42
[2] Wrangler: https://github.com/RefactoringTools/wrangler
[3] CamelCase: http://ru.wikipedia.org/wiki/CamelCase
Нажмите здесь для печати.