Следующие десять советов являются выдержкой из Perl Best Practices, новой книги Дэмиана Конвея о программированию на Perl и о разработке в целом.
Наиболее важный аспект любого модуля — не то как он реализует заложенные в него возможности, но прежде всего то, насколько удобно эти возможности использовать. Если API модуля слишком неудобен, или слишком сложен, или слишком обширен, или слишком фрагментирован или просто используемые в нём имена плохо выбраны — разработчики будут избегать его использование. Вместо этого они будут писать собственный код. Таким образом, плохо спроектированный модуль на самом деле уменьшает общее удобство работы над системой.
Разработка интерфейсов модулей требует как опыта, так и творческих способностей. Пожалуй наиболее простой способ решить каким должен быть интерфейс, это "поиграться" с ним: написать примеры кода, который будет использовать этот модуль, до написания самого модуля. Эти "ненастоящие" примеры не пропадут, когда Вы закончите разработку модуля. Вы можете просто переделать эти примеры в демо-программы, примеры для документации или использовать в качестве тестов для модуля.
Ключевой принцип в том, чтобы писать код так, словно модуль уже существует, и подразумевая, что модуль имеет такой интерфейс, который Вам наиболее удобен.
Когда Вы уже получите представление об интерфейсе, который Вы хотите реализовать, переделайте Ваши "игрушечные" примеры в настоящие тесты (см. совет #2). Теперь всего лишь "дело техники" заставить этот модуль работать так, как того ожидают Ваши примеры и тесты.
Конечно, иногда может быть попросту невозможно реализовать интерфейс модуля так, как Вы того желаете. В этом случае, попытки реализовать интерфейс помогут Вам определить то, какие аспекты Вашего API непрактичны и труднореализуемы и найти приемлемую альтернативу.
Пожалуй, наиболее универсальный принцип программирования — вначале писать комплект тестов.
Комплект тестов — это исполняемая спецификация какого-либо компонента программного обеспечения, автоматически контролирующая его поведение. Если у Вас есть комплект тестов, Вы можете — в любой точке процесса разработки — проверить, что код работает так, как вы того ожидаете. Если у Вас есть комплект тестов, Вы можете — после любых изменений во время цикла поддержки — удостовериться, что код по прежнему работает как полагается.
Вначале пишите тесты. Напишите их как только Вы определитесь с программным интерфейсом (см. совет #1). Пишите их до того, как начали реализацию Вашего приложения или модуля. До тех пор, пока у Вас нет тестов, у Вас нет чёткой спецификации функционала Вашего ПО, и тем более нет возможности узнать соответствует ли поведение ПО этой спецификации.
Написание тестов всегда кажется рутиной, причём рутиной в данном случае бессмысленной: у Вас ещё нет ничего, что можно тестировать - зачем писать эти тесты? Однако большая часть разработчиков будет -- почти "на автомате" -- писать вспомогательное ПО для тестирования их новых модулей для конкретных случаев (ad hoc):
> cat try_inflections.pl
# Тест для моего нового супер-модуля Английской морфологии...
use Lingua::EN::Inflect qw( inflect );
# Формы множественного числа (стандартные и не очень)...
my %plural_of = (
'house' => 'houses',
'mouse' => 'mice',
'box' => 'boxes',
'ox' => 'oxen',
'goose' => 'geese',
'mongoose' => 'mongooses',
'law' => 'laws',
'mother-in-law' => 'mothers-in-law',
);
# Для каждого из слов, вывести вычисленный и ожидаемый результаты...
for my $word ( keys %plural_of ) {
my $expected = $plural_of{$word};
my $computed = inflect( "PL_N($word)" );
print "Для $word:\n",
"\tОжидается: $expected\n",
"\tВычислено: $computed\n";
}
Специализированное вспомогательное ПО на самом деле написать сложнее чем набор тестов, поскольку Вы должны задумываться форматировании вывода, о представлении результатов в виде, удобном для анализа. Вспомогательное ПО также сложнее использовать чем набор тестов, поскольку каждый раз Вам необходимо анализировать выводимый результат "на глаз". Также такой способ контроля подвержен ошибкам. Наше зрение не оптимизировано для выявления небольших отличий в больших объёмах практически идентичного текста.
Вместо написания "на коленке" вспомогательной программы для тестирования,
проще написать простые тесты используя стандартный модуль
Test::Simple.
Вместо операторов print
для распечатки результатов, Вы просто вызываете функцию ok()
, передавая ей в качестве первого
аргумента булево значение или логическое выражение, проверяющее правильность вычислений, а в качестве второго аргумента описание того, что Вы тестируете:
> cat inflections.t
use Lingua::EN::Inflect qw( inflect);
use Test::Simple qw( no_plan);
my %plural_of = (
'mouse' => 'mice',
'house' => 'houses',
'ox' => 'oxen',
'box' => 'boxes',
'goose' => 'geese',
'mongoose' => 'mongooses',
'law' => 'laws',
'mother-in-law' => 'mothers-in-law',
);
for my $word ( keys %plural_of ) {
my $expected = $plural_of{$word};
my $computed = inflect( "PL_N($word)" );
ok( $computed eq $expected, "$word -> $expected" );
}
Имейте в виду, что этот код загружает Test::Simple
с аргументом
qw( no_plan )
.
Обычно используется аргумент tests => count
,
обозначающий как много тестов ожидается. Но нашем случае
тесты генерируются во время выполнения на основании данных таблицы %plural_of
, так что окончательное число тестов будет зависеть от количества
записей в таблице. Указание фиксированного числа тестов полезно в том случае,
если Вы точно знаете число выполняемых тестов в момент компиляции, поскольку
модуль может быть подвергнут "мета-тестированию": проверке того,
что успешно выполнены все тесты.
Тестовая программа, использующая Test::Simple
, более лаконична и читабельна, чем наша предыдущая вспомогательная программа, а вывод гораздо более компактный и информативный:
> perl inflections.t
ok 1 - house -> houses
ok 2 - law -> laws
not ok 3 - mongoose -> mongooses
# Failed test (inflections.t at line 21)
ok 4 - goose -> geese
ok 5 - ox -> oxen
not ok 6 - mother-in-law -> mothers-in-law
# Failed test (inflections.t at line 21)
ok 7 - mouse -> mice
ok 8 - box -> boxes
1..8
# Похоже, 2 теста из 8-ми провалились.
Что более важно, эта версия программы требует гораздо меньше усилий
для проверки результатов тестов. Вам нужно просто просканировать взглядом
левый край текстового вывода программы на предмет наличия слова not
.
Возможно, Вы предпочтёте использовать модуль Test::More вместо Test::Simple
. Вы этом случае Вы сможете указывать отдельно полученные и ожидаемые значения, используя функцию is()
вместо ok()
:
use Lingua::EN::Inflect qw( inflect );
use Test::More qw( no_plan ); # Теперь используем более продвинутый инструмент тестирования
my %plural_of = (
'mouse' => 'mice',
'house' => 'houses',
'ox' => 'oxen',
'box' => 'boxes',
'goose' => 'geese',
'mongoose' => 'mongooses',
'law' => 'laws',
'mother-in-law' => 'mothers-in-law',
);
for my $word ( keys %plural_of ) {
my $expected = $plural_of{$word};
my $computed = inflect( "PL_N($word)" );
# Проверить вычисленные и ожидаемые словоформы на равенство...
is( $computed, $expected, "$word -> $expected" );
}
Кроме того, что Вам теперь не нужно самому сравнивать значения
с помощью eq
, этот способ также позволяет получить
более детальные сообщения об ошибках:
> perl inflections.t
ok 1 - house -> houses
ok 2 - law -> laws
not ok 3 - mongoose -> mongooses
# Failed test (inflections.t at line 20)
# got: 'mongeese'
# expected: 'mongooses'
ok 4 - goose -> geese
ok 5 - ox -> oxen
not ok 6 - mother-in-law -> mothers-in-law
# Failed test (inflections.t at line 20)
# got: 'mothers-in-laws'
# expected: 'mothers-in-law'
ok 7 - mouse -> mice
ok 8 - box -> boxes
1..8
# Похоже, 2 теста из 8-ми провалились.
С Perl 5.8 поставляется документация
Test::Tutorial --
введение в Test::Simple
и Test::More
.
Одна из причин, по которой написание документации часто не доставляет никакого удовольствия, это "эффект пустой страницы". Многие программисты просто не знают с чего начать и что сказать.
Похоже наиболее простой путь написания документации в более приятной манере (и, следовательно, способ, с большей вероятностью приводящий к результату) - это обойти этот пустой экран, предоставив шаблон, который разработчики могут взять за основу, скопировать в свой код.
Для модуля шаблон документации может выглядеть следующим образом:
=head1 NAME
<Module::Name> - <One-line description of module's purpose>
=head1 VERSION
The initial template usually just has:
This documentation refers to <Module::Name> version 0.0.1.
=head1 SYNOPSIS
use <Module::Name>;
# Brief but working code example(s) here showing the most common usage(s)
# This section will be as far as many users bother reading, so make it as
# educational and exemplary as possible.
=head1 DESCRIPTION
A full description of the module and its features.
May include numerous subsections (i.e., =head2, =head3, etc.).
=head1 SUBROUTINES/METHODS
A separate section listing the public components of the module's interface.
These normally consist of either subroutines that may be exported, or methods
that may be called on objects belonging to the classes that the module
provides.
Name the section accordingly.
In an object-oriented module, this section should begin with a sentence (of the
form "An object of this class represents ...") to give the reader a high-level
context to help them understand the methods that are subsequently described.
=head1 DIAGNOSTICS
A list of every error and warning message that the module can generate (even
the ones that will "never happen"), with a full explanation of each problem,
one or more likely causes, and any suggested remedies.
=head1 CONFIGURATION AND ENVIRONMENT
A full explanation of any configuration system(s) used by the module, including
the names and locations of any configuration files, and the meaning of any
environment variables or properties that can be set. These descriptions must
also include details of any configuration language used.
=head1 DEPENDENCIES
A list of all of the other modules that this module relies upon, including any
restrictions on versions, and an indication of whether these required modules
are part of the standard Perl distribution, part of the module's distribution,
or must be installed separately.
=head1 INCOMPATIBILITIES
A list of any modules that this module cannot be used in conjunction with.
This may be due to name conflicts in the interface, or competition for system
or program resources, or due to internal limitations of Perl (for example, many
modules that use source code filters are mutually incompatible).
=head1 BUGS AND LIMITATIONS
A list of known problems with the module, together with some indication of
whether they are likely to be fixed in an upcoming release.
Also, a list of restrictions on the features the module does provide: data types
that cannot be handled, performance issues and the circumstances in which they
may arise, practical limitations on the size of data sets, special cases that
are not (yet) handled, etc.
The initial template usually just has:
There are no known bugs in this module.
Please report problems to <Maintainer name(s)> (<contact address>)
Patches are welcome.
=head1 AUTHOR
<Author name(s)> (<contact address>)
=head1 LICENSE AND COPYRIGHT
Copyright (c) <year> <copyright holder> (<contact address>).
All rights reserved.
followed by whatever license you wish to release it under.
For Perl code that is often just:
This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L<perlartistic>. This program is
distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Конечно, специфические особенности, который предоставляет Ваш шаблон, могут отличаться от показанных здесь. Вы формируете Ваш собственный шаблон исходя из Вашей собственной практики программирования. Наиболее вероятно, что будет варьироваться лицензия и сообщение об авторских правах, но также у Вас могут быть специфические соглашения об именовании версий, грамматике диагностических сообщений или указании авторства.
Поддержание контроля над созданием и модификацией исходного кода чрезвычайно важно для надёжной командной разработки. И не только для исходного кода: вы можете управлять версиями Вашей документации, файлов с данными, шаблонов документов, make-файлов, листов стилей, журналов изменений (changelogs) и любых других ресурсов, требующихся для Вашей системы.
Также как Вы не будете использовать редактор? не поддерживающий команду Undo или текстовый процессор, который не может объединять документы, вы не должны использовать набор файлов, который Вы не можете "откатить" на их предыдущие версии, или среду разработки. которая не может интегрировать работу нескольких программистов.
Программисты совершают ошибки и, порой эти ошибки будут катастрофическими. Они (программисты) могут переформатировать диск, содержащий последнюю версию Вашего кода. Или неверно вызвать макрос текстовом редакторе, который запишет нули в исходный текст Вашего главного модуля. Или два разработчика могут одновременно редактировать один и тот же файл и половина их изменений будет потеряно. Системы управления версиями могут предотвратить эти типы проблем.
Кроме того, иногда наилучший путь выйти из тупика -- просто "выбросить"
все вчерашние изменения, вернуться к предыдущей работающей версии
и начать всё заново.
Или, если действовать менее "круто", можно посмотреть построчный diff
между Вашим текущим кодом и кодом последней работающей версии из Вашего репозитория,
найти последние "улучшения" и выяснить, какие из них приводят к проблемам.
Системы управления версиями, такие как RCS, CVS, Subversion, Monotone, darcs
,
Perforce, GNU arch или BitKeeper помогут защитить от бедствий и обеспечить
возможность "отката", если что-то пойдёт совсем не так.
Различные системы имеют разные сильные стороны и ограничения,
многие из которых построены на совершенно различных принципах.
Хорошая идея -- попробовать несколько систем и найти ту,
которая лучше всего подходит Вам.
Рекомендую почитать
Pragmatic Version Control Using Subversion,
by Mike Mason (Pragmatic Bookshelf, 2005) и
Essential CVS,
by Jennifer Vesperman (O'Reilly, 2003).
Интерфейс командной строки имеет тенденцию усложняться со временем, вбирая в себя новые опции по мере того, как Вы добавляете новые возможности в приложение. К сожалению, редко эти интерфейсы разрабатываются с учётом их развития, развитие их не контролируется. Таким образом новые флаги, опции и аргументы, принимаемые приложением добавляются для конкретного случая (ad hoc) и не согласованы между собой.
Это также означает, что флаги, опции и аргументы разных приложений, написанных Вами, не похожи друг на друга. Результатом неизбежно будет набор программ, каждая из которых управляется особым и не похожим ни на что образом. Например:
> orchestrate source.txt -to interim.orc
> remonstrate +interim.rem -interim.orc
> fenestrate --src=interim.rem --dest=final.wdw
Invalid input format
> fenestrate --help
Unknown option: --help.
Type 'fenestrate -hmo' for help
Здесь утилита orchestrate
ожидает имя входного файла
в качестве первого аргумента, а с помощью флага -to
указывается
выходной файл.
Другой инструмент из того же набора, remonstrate
в отличие от предыдущей программы, использует опции -infile
и +outfile
.
А программа fenestrate
, похоже, требует "длинные опции" в GNU-стиле:
--src=infile
и --dest=outfile
, кроме, очевидно,
странно обозначенного флага для получения помощи. В конце концов,
это просто беспорядок!
Когда Вы предоставляете комплект программ, все они должны иметь сходный интерфейс -- предоставлять те же флаги, опции и те же стандартные возможности. Это позволяет пользователям Ваших программ извлечь преимущество из уже имеющихся знаний, вместо того, чтобы постоянно задавать Вам вопросы.
Эти три программы должны работать, например, так:
> orchestrate -i source.txt -o dest.orc
> remonstrate -i source.orc -o dest.rem
> fenestrate -i source.rem -o dest.wdw
Input file ('source.rem') not a valid Remora file
(type "fenestrate --help" for help)
> fenestrate --help
fenestrate - convert Remora .rem files to Windows .wdw format
Usage: fenestrate [-i <infile>] [-o <outfile>] [-cstq] [-h|-v]
Options:
-i <infile> Specify input source [default: STDIN]
-o <outfile> Specify output destination [default: STDOUT]
-c Attempt to produce a more compact representation
-h Use horizontal (landscape) layout
-v Use vertical (portrait) layout
-s Be strict regarding input
-t Be extra tolerant regarding input
-q Run silent
--version Вывести информацию о версии
--usage Вывести краткую инструкцию по запуску
--help Получить эту справку
--man Вывести подробное руководство
Здесь каждое приложение, которому требуется входной и выходной файл,
использует для этого одинаковые флаги.
Пользователь, который хочет использовать утилиту substrate
utility
(для конвертации .wdw-файла в процедуру) вероятно сможет угадать
корректный синтаксис требуемой команды:
> substrate -i dest.wdw -o dest.sub
Те, кто не сможет этого угадать, вероятно могут догадаться использовать команду
> substrate --help
для получения помощи.
Существенная часть работы по проектированию интерфейсов состоит в том, чтобы придать единообразие различным компонентам этого интерфейса. Вот некоторые соглашения, которые могут помочь Вам в проектировании последовательного и предсказуемого интерфейса:
Требуйте наличие флага перед каждым элементом данных командной строки, исключая, возможно, имена файлов
Пользователи не настроены запоминать, что Ваше приложение требует указания "входного файла, выходного файла, размера блока, вида операции, стратегии обработки ошибок", причём указания всех этих данных в строгом порядке:
> lustrate sample_data proc_data 1000 normalize log
Они хотят иметь возможность явно указать то, что они хотят, причём в удобном для них порядке::
> lustrate sample_data proc_data -op=normalize -b1000 --fallback=log
Предоставьте также флаги для каждого имени файла, особенно если файлы это могут использоваться программой для различных целей.
Пользователи также могут быть не в восторге от того, что им нужно помнить в каком порядке указываются различные имена файлов. Так что позвольте им также добавлять метки для этих аргументов и указывать их в том порядке, в котором они предпочитают:
> lustrate -i sample_data -op normalize -b1000 --fallback log -o proc_data
Используйте одиночный дефиса (-
) в качестве префикса
для флагов, заданных в сокращённой форме, до трёх символов включительно
(-v
, -i
, -rw
, -in
,
-out
).
Опытные пользователи ценят сокращённые флаги, т.к. нужно меньше набирать на клавиатуре. Не используйте двойной дефис для этих сокращённых флагов.
Используйте двойной дефис (--
) в качестве префикса для длинных
наименований флагов
(--verbose
, --interactive
, --readwrite
,
--input
, --output
).
Флаги, являющиеся полными словами улучшают читаемость команд (например, в shell-скриптах). Двойные дефисы также помогают отличить длинные имен флагов от расположенных рядом имён файлов.
Если флаг ожидает ассоциированное с ним значение, предоставьте опциональный
знак =
между именем флага и его значением.
Одни люди предпочитают визуально ассоциировать значение с предшествующим ему флагом::
> lustrate -i=sample_data -op=normalize -b=1000 --fallback=log -o=proc_data
Другие нет:
> lustrate -i sample_data -op normalize -b1000 --fallback log -o proc_data
А некоторые могут предпочитать смешивать эти два стиля:
> lustrate -i sample_data -o proc_data -op=normalize -b=1000 --fallback=log
Предоставьте им возможность выбора.
Предусмотрите возможность "компоновки" нескольких однобуквенных флагов в один (используя один дефис).
Иногда раздражает печатать дефисы для целой серии флагов:
> lustrate -i sample_data -v -l -x
Разрешите опытным пользователям также писать:
> lustrate -i sample_data -vlx
предоставляйте многобуквенную версию для каждого однобуквенного флага.
Сокращённые формы флагов хороши для опытных пользователей, но они могут вызывать затруднения у новичков: тяжело запомнить и ещё труднее понять. Не вынуждайте людей напрягаться. Дайте им более понятную многословную альтернативу для сокращённого флага; полные слова легче запомнить и легче понять, т.к. они сами себя документируют в shell-скриптах.
Всегда допускайте -
в качестве специального имени файла.
Широко используемое соглашение состоит в том, чтобы указывать дефис (-
)
на месте имени входного файла, что означает "считать со стандартного ввода",
и дефис на месте имени выходного файла, что означает "вывести на стандартный вывод".
Всегда допускайте --
в качестве ограничителя перед именами файлов.
Другая широко используемое соглашение состоим в том, что использование двойного дефиса
(--
) в командной строке означает конец указание любых флагов / опций
и означает, что все оставшиеся аргументы являются именами файлов, даже
если некоторые из них выглядят как флаги.
perltidy
Форматирование. Отступы. Стиль. Взаимное расположение элементов кода. Как бы Вы это не называли, это является одним из аспектов программирования, вызывающих наибольшие споры. По поводу форматирования кода мир пережил больше кровавых распрей, чем по поводу чего либо ещё, касающегося программирования.
Какова лучшая практика здесь? Использовать ли классический стиль Kernighan и Ritchie? Или BSD-стиль форматирования? Или адаптировать схему форматирования, применяемую в проекте GNU? Или следовать принципам кодирования, принятым в Slashcode?
Конечно, нет! Каждый знает, что <вставьте Ваш любимый стиль здесь> является Единственным Правильным Стилем, единственным верным выбором, как завещал великий <вставьте имя Вашего наиболее почитаемого Божества Программирования> с незапамятных времён! Любой другой выбор очевидно абсурден, явно еретический и само-собой-разумеется является инспирацией Сил Тьмы!
Именно в этом и проблема. Когда Вы принимаете решение о стиле форматирования, непросто сделать рациональный выбор и тут начинаются рационализованные привычки.
Адаптация согласованно-спроектированного подхода к форматированию кода и последовательное применение этого подхода в процессе работы является составной частью фундамента лучших практик программирования. Хорошее форматирование может улучшить читабельность программы, помочь обнаружить ошибки внутри неё, и сделать структуру кода более лёгкой для постижения. Форматирование имеет значение.
Однако, большинство стилей кодирования, включая четыре, упомянутые ранее, служат этим целям одинаково хорошо. В то время как форматирование кода само по себе имеет огромное значение, конкретный выбор в пользу той или иной схемы не имеет значения вообще! Всё что имеет значение, это то, что Вы следуете единому, согласованному стилю; такому стилю, который подходит для всей Вашей команды. Важно, чтобы все приняли этот стиль и строго следовали ему во время всего процесса разработки.
В долгосрочной перспективе наилучшим является обучиться самому и обучить свою команду писать код в последовательном, рациональном и читабельном стиле. Однако, время и приверженность команды, необходимые для этого, не всегда доступны. В таких случаях разумный компромисс состоит в том, чтобы предписать использование стандартного инструмента, который должен применяться перед тем как код будет отправлен в систему контроля версий, отправлен на ревизию или иным образом показан другим людям.
Прекрасный инструмент авто-форматирования для Perl: perltidy
.
Он предоставляет расширенный диапазон пользовательских настроек для
установки отступов, позиционирования разделителей блоков, выравнивание
по типу колонок и позиционирование комментариев.
Используя perltidy
, Вы можете сконвертировать код подобный этому:
if($sigil eq '$'){
if($subsigil eq '?'){
$sym_table{substr($var_name,2)}=delete $sym_table{locate_orig_var($var)};
$internal_count++;$has_internal{$var_name}++
} else {
${$var_ref} =
q{$sym_table{$var_name}}; $external_count++; $has_external{$var_name}++;
}} elsif ($sigil eq '@'&&$subsigil eq '?') {
@{$sym_table{$var_name}} = grep
{defined $_} @{$sym_table{$var_name}};
} elsif ($sigil eq '%' && $subsigil eq '?') {
delete $sym_table{$var_name}{$EMPTY_STR}; } else
{
${$var_ref}
=
q{$sym_table{$var_name}}
}
в нечто читабельное:
if ( $sigil eq '$' ) {
if ( $subsigil eq '?' ) {
$sym_table{ substr( $var_name, 2 ) }
= delete $sym_table{ locate_orig_var($var) };
$internal_count++;
$has_internal{$var_name}++;
}
else {
${$var_ref} = q{$sym_table{$var_name}};
$external_count++;
$has_external{$var_name}++;
}
}
elsif ( $sigil eq '@' && $subsigil eq '?' ) {
@{ $sym_table{$var_name} }
= grep {defined $_} @{ $sym_table{$var_name} };
}
elsif ( $sigil eq '%' && $subsigil eq '?' ) {
delete $sym_table{$var_name}{$EMPTY_STR};
}
else {
${$var_ref} = q{$sym_table{$var_name}};
}
Указание всем использовать единый инструмент для форматирования
их кода также является простым способом ухода от бесконечных возражений,
желчности и догматов, которые всегда окружают обсуждение стиля кодирования.
Если perltidy
делает за них всю работу, то для разработчиков
ничего не будет стоить приспособиться к новым принципам.
Они смогут просто настроить макрос редактора который будет "выпрямлять"
их код когда это будет им необходимо.
Параграф — это набор операторов, выполняющих одну задачу: в литературе это последовательность предложений, выражающих одну идею; в программировании — серия инструкций, реализующих один шаг алгоритма.
Разделяйте каждый кусок кода на фрагменты, решающие одну задачу, с помощью вставки одной пустой строки между этими фрагментами. Для дальнейшего улучшения сопровождабельности кода, добавляйте вначале каждого параграфа однострочный комментарий, объясняющий, что делает эта последовательность операторов. Типа такого:
# Обработать массив, который был распознан...
sub addarray_internal {
my ($var_name, $needs_quotemeta) = @_;
# Запомнить оригинал...
$raw .= $var_name;
# Добавить экранирование спецсимволов, если необходимо...
my $quotemeta = $needs_quotemeta ? q{map {quotemeta $_} } : $EMPTY_STR;
# Перевести элементы переменной в строку, соединяя их с помощью "|"...
my $perl5pat = qq{(??{join q{|}, $quotemeta \@{$var_name}})};
# Добавить отладочный код, если необходимо...
my $type = $quotemeta ? 'literal' : 'pattern';
debug_now("Adding $var_name (as $type)");
add_debug_mesg("Trying $var_name (as $type)");
return $perl5pat;
}
Параграфы полезны, поскольку человек может одновременно сфокусироваться Параграфы — единственный способ объединять небольшие количества связанной информации таким образом, что результирующий "кусок" может поместиться в единственный слот ограниченной по объёму кратковременной памяти читателя. Параграфы позволяют физической структуре разбиения кода на фрагменты отражать и подчёркивать его логическую структуру.
Добавление комментариев вначале каждого параграфа ещё более улучшает эффект от разбиения на фрагменты, резюмируя назначение каждого фрагмента (заметьте, назначение, а не поведение). Комментарии к параграфам нужны для объяснения того, почему этот код находится здесь и для чего он нужен, а не для дословного пересказа на естественном языке соответствующих операторов и вычислительных конструкций.
Заметьте, однако, что содержимое параграфов имеет здесь второстепенное значение. Важны именно вертикальные отступы, отделяющие параграфы друг от друга. Без них читабельность кода резко снижается, даже при сохранении комментариев:
sub addarray_internal {
my ($var_name, $needs_quotemeta) = @_;
# Запомнить оригинал...
$raw .= $var_name;
# Добавить экранирование спецсимволов, если необходимо...
my $quotemeta = $needs_quotemeta ? q{map {quotemeta $_} } : $EMPTY_STR;
# Перевести элементы переменной в строку, соединяя их с помощью "|"...
my $perl5pat = qq{(??{join q{|}, $quotemeta \@{$var_name}})};
# Добавить отладочный код, если необходимо...
my $type = $quotemeta ? 'literal' : 'pattern';
debug_now("Adding $var_name (as $type)");
add_debug_mesg("Trying $var_name (as $type)");
return $perl5pat;
}
Возврат специального значения, сигнализирующего ошибку или установка специального
флага — это очень широко используемая техника обработки ошибок.
Вообще говоря, это принцип сигнализирования об ошибках практически всех
встроенных функции Perl. Например, встроенные функции eval
,
exec
, flock
, open
, print
,
stat
, и system
— все возвращают специальные
значения в случае ошибки.
Некоторые при этом также устанавливают флаг.
К сожалению, это не всегда один и тот же флаг.
С неутешительными подробностями можно ознакомиться на странице
perlfunc
.
Кроме проблем последовательности, оповещение об ошибках при помощи флагов и возвращаемых значений имеет ещё один серьёзный порок: разработчики могут тихо игнорировать флаги и возвращаемые значения. И игнорирование их не требует абсолютно никаких усилий со стороны программиста. НА самом деле в void-контексте, игнорирование возвращаемых значений — это поведение Perl-программ по умолчанию. Игнорирование флага ошибки также элементарно просто — Вы просто не проверяете соответствующую переменную.
Кроме того, игнорирование возвращаемого значения в void-контексте происходит незаметно, ведь нет никак синтаксических зацепок, позволяющих это контролировать. Нет возможности посмотреть на программу и сразу увидеть "вот здесь возвращаемое значение игнорируется!". А это означает, что возвращаемое значение может быть запросто проигнорировано случайно.
Резюме: по умыслу или недосмотру программиста индикаторы ошибок часто игнорируются. Это не есть хороший подход к программированию.
Игнорирование индикаторов ошибок часто приводит к тому, что ошибки распространяют своё влияние в непредсказуемом направлении. Например:
# Найти и открыть файл с заданным именем,
# возвратить описатель файла или undef в случае неудачи....
sub locate_and_open {
my ($filename) = @_;
# Проверить директории поиска по порядку...
for my $dir (@DATA_DIRS) {
my $path = "$dir/$filename";
# Если файл существует в дикектории, откроем его и возвратим описатель...
if (-r $path) {
open my $fh, '<', $path;
return $fh;
}
}
# Ошибка если все возможные локации файла проверены и файл не найден...
return;
}
# Загрузить содержимое файла до первого маркера <DATA/> ...
sub load_header_from {
my ($fh) = @_;
# Использовать тег DATA в качестве конца "строки"...
local $/ = '<DATA/>';
# Прочитать до конца "строки"...
return <$fh>;
}
# и позже...
for my $filename (@source_files) {
my $fh = locate_and_open($filename);
my $head = load_header_from($fh);
print $head;
}
Функция locate_and_open()
предполагает что вызов
open
успешно отработал, немедленно возвращая описатель файла
($fh
), какой бы ни был возвращаемый результат open
.
Предположительно тот, кто вызывает locate_and_open()
проверит
возвращаемое ею значение на предмет корректного описателя файла.
Однако, этот кто-то не делает такой проверки.
Вместо тестирования на неуспех, основной цикл for
принимает "ошибочное" значение и немедленно его использует
в следующих операндах.
В результате этого вызов loader_header_from()
передаёт
это неверное значение дальше.В результате функция, которая пытается
использовать это ошибочное значение, вызывает крах программы.:
readline() on unopened filehandle at demo.pl line 28.
Код подобный этому — где сообщение об ошибке указывает на совершенно другую часть программы, не туда, где на самом деле произошла ошибка — довольно тяжело отлаживать.
Конечно, Вы можете возразить, что вина непосредственно лежит на том,
кто писал тот цикл и не проверял возвращаемое значение locate_and_open()
.
В узком смысле это чистая правда, но глубинная вина всё-таки лежит на том,
кто написал locate_and_open()
, или по крайней мере на том, кто
полагал, что вызывающая сторона будет всегда проверять возвращаемое значение
этой функции.
Люди просто не любят делать это. Скалы почти не когда не падают на небо, так что люди делают вывод что они никогда этого и не сделают, и перестают смотреть за ними. Пожары редко уничтожают их дома, поэтому люди скоро забывают, что такое может произойти и перестают тестировать детекторы дыма каждый месяц. Таким же образом программисты неизбежно сокращают фразу "почти никогда не сломается" до "никогда не сломается" и просто перестают делать проверки.
Вот почему так мало людей заботиться о проверке для
операторов print
:
if (!print 'Введите Ваше имя: ') {
print {*STDLOG} warning => 'Терминал отвалился!'
}
Такова натура человека: "доверяй и не проверяй".
Причина того, что возвращение индикатора ошибки не является лучшей практикой — в природе человека. Ошибки являются (как это предполагается) необычными случаями и маркеры ошибок почти никогда не будут возвращаться. Эти нудные и несуразные проверки почти никогда не делают ничего полезного, так что ими просто тихо пренебрегают. В конце концов, если опустить эти проверки, почти всё работает так же хорошо. Так что гораздо проще не париться с ними. В особенности когда их игнорирование является поведением по умолчанию (вспомните void-контекст)!
Не возвращайте специальные значения, когда что-то идёт не так;
вместо этого выбрасывайте исключения. Огромное преимущество исключений в том,
что они как бы выворачивают наизнанку обычное поведение по-умолчанию,
немедленно обращая внимание на необработанные ошибки.
С другой стороны, игнорирование исключений требует намеренных и видимых
усилий: вы должны предусмотреть явный блок eval
для их перехвата.
Функция locate_and_open()
будет намного
понятнее и надёжнее, если в случае ошибок мы выбрасываем исключения:
# Find and open a file by name, returning the filehandle
# or throwing an exception on failure...
sub locate_and_open {
my ($filename) = @_;
# Проверить директории поиска по порядку...
for my $dir (@DATA_DIRS) {
my $path = "$dir/$filename";
# Если файл существует в дикектории, откроем его и возвратим описатель...
if (-r $path) {
open my $fh, '<', $path
or croak( "Located $filename at $path, but could not open");
return $fh;
}
}
# Ошибка если все возможные локации файла проверены и файл не найден...
croak( "Could not locate $filename" );
}
# and later...
for my $filename (@source_files) {
my $fh = locate_and_open($filename);
my $head = load_header_from($fh);
print $head;
}
Заметьте, что основной цикл for
вообще не изменился.
Разработчик, использующий locate_and_open()
всё так же предполагает,
что всё отработает без сбоев.
Теперь этому есть обоснование, поскольку если действительно что-то пойдёт
не так, выброшенное исключение автоматически завершит цикл.
Исключения — это более удачный выбор даже если даже Вы настолько дисциплинированы, что проверяете каждое выходное значение на предмет наличия ошибки:
SOURCE_FILE:
for my $filename (@source_files) {
my $fh = locate_and_open($filename);
next SOURCE_FILE if !defined $fh;
my $head = load_header_from($fh);
next SOURCE_FILE if !defined $head;
print $head;
}
Постоянные проверки возвращаемых значений вносят посторонний шум в Ваш код
в виде проверочных операторов, часто сильно ухудшая читабельность.
Исключения, наоборот, дают возможность реализовать алгоритм вообще без вкраплений
кода для проверки ошибок.
Мы можете вынести обработку ошибок за пределы алгоритма и передать
эти функции обрамляющему eval
или вообще обойтись без этой
обработки:
for my $filename (@directory_path) {
# Просто игнорировать файлы, которые не загружаются...
eval {
my $fh = locate_and_open($filename);
my $head = load_header_from($fh);
print $head;
}
}
Первый шаг в любом процессе отладки -- это выделить наименьший кусок кода, который демонстрирует ошибку. Если The first step in any debugging process is to isolate the incorrect behavior of the system, by producing the shortest demonstration of it that you reasonably can. If you're lucky, this may even have been done for you:
To: [email protected]
From: [email protected]
Subject: Ошибка в модуле inflect
Здравствуйте,
Я использую модуль Lingua::EN::Inflect для приведения к нормальной форме
терминов в приложении для анализа данных, которое я разрабатываю.
И, похоже, я нашёл баг в этом модуле. Вот пример, который его демонстрирует:
use Lingua::EN::Inflect qw( PL_N );
print PL_N('man'), "\n"; # Выводит "men", как и ожидалось
print PL_N('woman'), "\n"; # Выводит неверное значение - "womans"
Как только Вы сварганили короткий пример, демонстрирующий ошибку, превратите его в серию тестов, типа:
use Lingua::EN::Inflect qw( PL_N );
use Test::More qw( no_plan );
is(PL_N('man') , 'men', 'man -> men' );
is(PL_N('woman'), 'women', 'woman -> women' );
Не пытайтесь сразу пофиксить баг. Сначала добавьте необходимые тесты в Ваш набор тестов. Если у Вас уже есть набор тестов, Вы просто добавляете пару записей в Вашу табличку:
my %plural_of = (
'mouse' => 'mice',
'house' => 'houses',
'ox' => 'oxen',
'box' => 'boxes',
'goose' => 'geese',
'mongoose' => 'mongooses',
'law' => 'laws',
'mother-in-law' => 'mothers-in-law',
# Сашин баг, зарепорченный 27 Aug 2004...
'man' => 'men',
'woman' => 'women',
);
Фишка вот в чём: если исходный набор тестов не сигнализирует об этой ошибке, значит этот набор тестов неполноценный. Он просто не выполняет свою работу (по нахождению ошибок) адекватно. Добавьте туда тесты, которые не будут пройдены:
> perl inflections.t
ok 1 - house -> houses
ok 2 - law -> laws
ok 3 - man -> men
ok 4 - mongoose -> mongooses
ok 5 - goose -> geese
ok 6 - ox -> oxen
not ok 7 - woman -> women
# Failed test (inflections.t at line 20)
# got: 'womans'
# expected: 'women'
ok 8 - mother-in-law -> mothers-in-law
ok 9 - mouse -> mice
ok 10 - box -> boxes
1..10
# Похоже, 1 тест из 10 провален.
Как только Ваш набор тестов корректно обнаруживает проблему, Вы исправляете баг и теперь Вы сможете точно сказать что исправили его, поскольку программа снова корректно проходит все тесты.
Этот подход к отладке наиболее эффективен когда набор тестов покрывает весь спектр случаев, при которых проявляется проблема. При добавлении тестов для ошибки, не ограничивайтесь просто добавлением одного простого теста. Убедитесь, что Вы включили также различные варианты:
my %plural_of = (
'mouse' => 'mice',
'house' => 'houses',
'ox' => 'oxen',
'box' => 'boxes',
'goose' => 'geese',
'mongoose' => 'mongooses',
'law' => 'laws',
'mother-in-law' => 'mothers-in-law',
# Sascha's bug, reported 27 August 2004...
'man' => 'men',
'woman' => 'women',
'human' => 'humans',
'man-at-arms' => 'men-at-arms',
'lan' => 'lans',
'mane' => 'manes',
'moan' => 'moans',
);
Чем тщательнее Вы тестируете программу на предмет наличия ошибок, тем более безошибочной будет программа.
Если Вам нужна функция для удаления дублирующихся элементов массива, вполне естественно, что однострочная версия наподобие этой:
sub uniq { return keys %{ { map {$_ => 1} @_ } } }
будет более эффективна, чем два оператора:
sub uniq {
my %seen;
return grep {!$seen{$_}++} @_;
}
До тех пор пока Вы не будете глубоко знакомы с внутренностями Perl-интерпретатора (а в этом случае Вы наверняка уже будете решать вопросы посложнее), интуиция по поводу относительного быстродействия двух конструкций является ничем иным как неосознанным выбором.
Единственный способ узнать какая из двух альтернатив быстрее — замерить каждую из них. Со стандартным модулем Benchmark это просто:
# Короткий список не-совсе-уникальных значений...
our @data = qw( do re me fa so la ti do );
# Различные кандидаты...
sub unique_via_anon {
return keys %{ { map {$_ => 1} @_ } };
}
sub unique_via_grep {
my %seen;
return grep { !$seen{$_}++ } @_;
}
sub unique_via_slice {
my %uniq;
@uniq{ @_ } = ();
return keys %uniq;
}
# Сравнить, используя текущий набор данных из @data...
sub compare {
my ($title) = @_;
print "\n[$title]\n";
# Создать сравнительную таблицу различных замеров времени,
# при том, чтобы каждый запуск продолжался минимум 10 секунд...
use Benchmark qw( cmpthese );
cmpthese -10, {
anon => 'my @uniq = unique_via_anon(@data)',
grep => 'my @uniq = unique_via_grep(@data)',
slice => 'my @uniq = unique_via_slice(@data)',
};
return;
}
compare('8 элементов, 10% повторяющихся');
# Зве копии исходных данных...
@data = (@data) x 2;
compare('16 элементов, 56% повторяющихся');
# Сто копий исходных данных...
@data = (@data) x 50;
compare('800 элементов, 99% повторяющихся');
Процедура cmpthese()
принимает в качестве аргумента число
и далее ссылку на хэш с тестами. Число обозначает либо точное число запусков
каждого теста (если это число положительное), либо количество секунд
процессорного времени, в течение которого нужно гонять каждый тест (если
число отрицательное). Обычные используемые значения — примерно 10'000
повторений или 10 CPU-секунд, но модуль предупредит Вас если тест будет слишком
коротким для получения точного замера.
Ключи хэша с тестами представляют собой имена тестов, а их
значения ‐ код соответствующих тестов. Эти значения могут быть как
строками (которые будут выполнены с помощью eval
) или ссылками
на функции (которые будут вызваны напрямую).
Код для замера быстродействия, приведённый выше, выдаст что-то типа:
[8 элементов, 10% повторяющихся]
Rate anon grep slice
anon 28234/s -- -24% -47%
grep 37294/s 32% -- -30%
slice 53013/s 88% 42% --
[16 элементов, 56% повторяющихся]
Rate anon grep slice
anon 21283/s -- -28% -51%
grep 29500/s 39% -- -32%
slice 43535/s 105% 48% --
[800 элементов, 99% повторяющихся]
Rate anon grep slice
anon 536/s -- -65% -89%
grep 1516/s 183% -- -69%
slice 4855/s 806% 220% --
Каждая из выведенных таблиц содержит отдельную строку для каждого
из поименованных тестов. В первой колонке -- абсолютная скорость каждого
кандидата, в повторениях в секунду, а остальные колонки позволяют
Вам сравнить полученный результат с остальными двумя тестами.
Например последний тест показывает, что grep
по сравнению с anon
выполняется в 1.83 раза быстрее (это 185 процента).
Также grep
был на 69 процентов медленнее (на -69 процентов быстрее) чем
slic
.
В целом, все три теста показывают, что решения, основанные на slice
неизменно быстрее других на этом наборе данных на этой конкретной машине.
Также становится ясно, что с увеличением размера массива, slice
всё больше выигрывает у конкурентов.
Однако эти два заключения были сделаны на основании всего трёх наборов данных (а именно, на основании трёх запусков замера быстродействия). Для того, чтобы получить более показательное сравнение этих трёх методов, Вы также должны протестировать другие возможности, такие как длинный список неповторяющихся значений или короткий список, состоящий только из повторений.
Лучше всего попробуйте тесты на реальных данных, которые Вам нужно будет обрабатывать.
Например, если данные являются отсортированным списком из четверти миллиона слов, с минимальными повторениями, а после обработки список должен оставаться отсортированным, то вот тест для такого случая:
our @data = slurp '/usr/share/biglongwordlist.txt';
use Benchmark qw( cmpthese );
cmpthese 10, {
# Note: the non-grepped solutions need a post-uniqification re-sort
anon => 'my @uniq = sort(unique_via_anon(@data))',
grep => 'my @uniq = unique_via_grep(@data)',
slice => 'my @uniq = sort(unique_via_slice(@data))',
};
Неудивительно, что решение, основанное на grep
здесь на высоте:
s/iter anon slice grep
anon 4.28 -- -3% -46%
slice 4.15 3% -- -44%
grep 2.30 86% 80% --
Причём решение с grep
оставляет список отсортированным.
Всё это даёт основания полагать, что превосходство решения, основанного на
slice
— это частный случай и это решение подрывается
растущей стоимостью манипуляций с хэшами в случаях, когда хэш вырастает
до слишком больших размеров.
Последний пример показывает, что выводы, сделанные Вами на основании тестирования быстродействия с какими-либо наборами данных, распространяются главным образом именно на этот конкретный вид данных.
Источник статьи