| Десять практических рекомендаций разработчикам на Perl |
|
|
| Автор Westry | |
| 04.02.2008 г. | |
|
Следующие десять советов являются выдержкой из Perl Best Practices, новой книги Дэмиана Конвея о программировании на Perl и о разработке в целом.
1. Вначале разработайте интерфейс модулей Наиболее важный аспект любого модуля — не то как он реализует заложенные в него возможности, но прежде всего то, насколько удобно эти возможности использовать. Если API модуля слишком неудобен, или слишком сложен, или слишком обширен, или слишком фрагментирован или просто используемые в нём имена плохо выбраны — разработчики будут избегать его использование. Вместо этого они будут писать собственный код. Таким образом, плохо спроектированный модуль на самом деле уменьшает общее удобство работы над системой. Разработка интерфейсов модулей требует как опыта, так и творческих способностей. Пожалуй наиболее простой способ решить каким должен быть интерфейс, это "поиграться" с ним: написать примеры кода, который будет использовать этот модуль, до написания самого модуля. Эти "ненастоящие" примеры не пропадут, когда Вы закончите разработку модуля. Вы можете просто переделать эти примеры в демо-программы, примеры для документации или использовать в качестве тестов для модуля. Ключевой принцип в том, чтобы писать код так, словно модуль уже существует, и подразумевая, что модуль имеет такой интерфейс, который Вам наиболее удобен. Когда Вы уже получите представление об интерфейсе, который Вы хотите реализовать, переделайте Ваши "игрушечные" примеры в настоящие тесты (см. совет #2). Теперь всего лишь "дело техники" заставить этот модуль работать так, как того ожидают Ваши примеры и тесты. Конечно, иногда может быть попросту невозможно реализовать интерфейс модуля так, как Вы того желаете. В этом случае, попытки реализовать интерфейс помогут Вам определить то, какие аспекты Вашего API непрактичны и труднореализуемы и найти приемлемую альтернативу. 2. Вначале пишите тесты, затем кодПожалуй, наиболее универсальный принцип программирования — вначале писать комплект тестов. Комплект тестов — это исполняемая спецификация какого-либо компонента программного обеспечения, автоматически контролирующая его поведение. Если у Вас есть комплект тестов, Вы можете — в любой точке процесса разработки — проверить, что код работает так, как вы того ожидаете. Если у Вас есть комплект тестов, Вы можете — после любых изменений во время цикла поддержки — удостовериться, что код по прежнему работает как полагается. Вначале пишите тесты. Напишите их как только Вы определитесь с программным интерфейсом (см. совет #1). Пишите их до того, как начали реализацию Вашего приложения или модуля. До тех пор, пока у Вас нет тестов, у Вас нет чёткой спецификации функционала Вашего ПО, и тем более нет возможности узнать соответствует ли поведение ПО этой спецификации. Написание тестов всегда кажется рутиной, причём рутиной в данном случае бессмысленной: у Вас ещё нет ничего, что можно тестировать - зачем писать эти тесты? Однако большая часть разработчиков будет -- почти "на автомате" -- писать вспомогательное ПО для тестирования их новых модулей для конкретных случаев (ad hoc): > cat try_inflections.pl Специализированное вспомогательное ПО на самом деле написать сложнее чем набор тестов, поскольку Вы должны задумываться форматировании вывода, о представлении результатов в виде, удобном для анализа. Вспомогательное ПО также сложнее использовать чем набор тестов, поскольку каждый раз Вам необходимо анализировать выводимый результат "на глаз". Также такой способ контроля подвержен ошибкам. Наше зрение не оптимизировано для выявления небольших отличий в больших объёмах практически идентичного текста. Вместо написания "на коленке" вспомогательной программы для тестирования, проще написать простые тесты используя стандартный модуль Test::Simple. Вместо операторов print для распечатки результатов, Вы просто вызываете функцию ok(), передавая ей в качестве первого аргумента булево значение или логическое выражение, проверяющее правильность вычислений, а в качестве второго аргумента описание того, что Вы тестируете: > cat inflections.t Имейте в виду, что этот код загружает Test::Simple с аргументом qw( no_plan ). Обычно используется аргумент tests => count, обозначающий как много тестов ожидается. Но нашем случае тесты генерируются во время выполнения на основании данных таблицы %plural_of, так что окончательное число тестов будет зависеть от количества записей в таблице. Указание фиксированного числа тестов полезно в том случае, если Вы точно знаете число выполняемых тестов в момент компиляции, поскольку модуль может быть подвергнут "мета-тестированию": проверке того, что успешно выполнены все тесты. Тестовая программа, использующая Test::Simple, более лаконична и читабельна, чем наша предыдущая вспомогательная программа, а вывод гораздо более компактный и информативный: > perl inflections.t Что более важно, эта версия программы требует гораздо меньше усилий для проверки результатов тестов. Вам нужно просто просканировать взглядом левый край текстового вывода программы на предмет наличия слова not. Возможно, Вы предпочтёте использовать модуль Test::More вместо Test::Simple. Вы этом случае Вы сможете указывать отдельно полученные и ожидаемые значения, используя функцию is() вместо ok(): use Lingua::EN::Inflect qw( inflect ); Кроме того, что Вам теперь не нужно самому сравнивать значения с помощью eq, этот способ также позволяет получить более детальные сообщения об ошибках: > perl inflections.t С Perl 5.8 поставляется документация Test::Tutorial -- введение в Test::Simple и Test::More. 3. Создавайте POD-документацию для модулей и приложенийОдна из причин, по которой написание документации часто не доставляет никакого удовольствия, это "эффект пустой страницы". Многие программисты просто не знают с чего начать и что сказать. Похоже наиболее простой путь написания документации в более приятной манере (и, следовательно, способ, с большей вероятностью приводящий к результату) - это обойти этот пустой экран, предоставив шаблон, который разработчики могут взять за основу, скопировать в свой код. Для модуля шаблон документации может выглядеть следующим образом: =head1 NAME Конечно, специфические особенности, который предоставляет Ваш шаблон, могут отличаться от показанных здесь. Вы формируете Ваш собственный шаблон исходя из Вашей собственной практики программирования. Наиболее вероятно, что будет варьироваться лицензия и сообщение об авторских правах, но также у Вас могут быть специфические соглашения об именовании версий, грамматике диагностических сообщений или указании авторства. 4. Используйте систему управления версиямиПоддержание контроля над созданием и модификацией исходного кода чрезвычайно важно для надёжной командной разработки. И не только для исходного кода: вы можете управлять версиями Вашей документации, файлов с данными, шаблонов документов, 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). 5. Создавайте продуманные интерфейсы командной строкиИнтерфейс командной строки имеет тенденцию усложняться со временем, вбирая в себя новые опции по мере того, как Вы добавляете новые возможности в приложение. К сожалению, редко эти интерфейсы разрабатываются с учётом их развития, развитие их не контролируется. Таким образом новые флаги, опции и аргументы, принимаемые приложением добавляются для конкретного случая (ad hoc) и не согласованы между собой. Это также означает, что флаги, опции и аргументы разных приложений, написанных Вами, не похожи друг на друга. Результатом неизбежно будет набор программ, каждая из которых управляется особым и не похожим ни на что образом. Например: > orchestrate source.txt -to interim.orc Здесь утилита orchestrate ожидает имя входного файла в качестве первого аргумента, а с помощью флага -to указывается выходной файл. Другой инструмент из того же набора, remonstrate в отличие от предыдущей программы, использует опции -infile и +outfile. А программа fenestrate, похоже, требует "длинные опции" в GNU-стиле: --src=infile и --dest=outfile, кроме, очевидно, странно обозначенного флага для получения помощи. В конце концов, это просто беспорядок! Когда Вы предоставляете комплект программ, все они должны иметь сходный интерфейс -- предоставлять те же флаги, опции и те же стандартные возможности. Это позволяет пользователям Ваших программ извлечь преимущество из уже имеющихся знаний, вместо того, чтобы постоянно задавать Вам вопросы. Эти три программы должны работать, например, так: > orchestrate -i source.txt -o dest.orc Здесь каждое приложение, которому требуется входной и выходной файл, использует для этого одинаковые флаги. Пользователь, который хочет использовать утилиту substrate utility (для конвертации .wdw-файла в процедуру) вероятно сможет угадать корректный синтаксис требуемой команды: > substrate -i dest.wdw -o dest.sub Те, кто не сможет этого угадать, вероятно могут догадаться использовать команду > substrate --help для получения помощи. Существенная часть работы по проектированию интерфейсов состоит в том, чтобы придать единообразие различным компонентам этого интерфейса. Вот некоторые соглашения, которые могут помочь Вам в проектировании последовательного и предсказуемого интерфейса:
6. Придите к единому мнению на счёт стиля форматирования исходного кода и автоматизируйте стилизацию, используя perltidyФорматирование. Отступы. Стиль. Взаимное расположение элементов кода. Как бы Вы это не называли, это является одним из аспектов программирования, вызывающих наибольшие споры. По поводу форматирования кода мир пережил больше кровавых распрей, чем по поводу чего либо ещё, касающегося программирования. Какова лучшая практика здесь? Использовать ли классический стиль Kernighan и Ritchie? Или BSD-стиль форматирования? Или адаптировать схему форматирования, применяемую в проекте GNU? Или следовать принципам кодирования, принятым в Slashcode? Конечно, нет! Каждый знает, что <вставьте Ваш любимый стиль здесь> является Единственным Правильным Стилем, единственным верным выбором, как завещал великий <вставьте имя Вашего наиболее почитаемого Божества Программирования> с незапамятных времён! Любой другой выбор очевидно абсурден, явно еретический и само-собой-разумеется является инспирацией Сил Тьмы! Именно в этом и проблема. Когда Вы принимаете решение о стиле форматирования, непросто сделать рациональный выбор и тут начинаются рационализованные привычки. Адаптация согласованно-спроектированного подхода к форматированию кода и последовательное применение этого подхода в процессе работы является составной частью фундамента лучших практик программирования. Хорошее форматирование может улучшить читабельность программы, помочь обнаружить ошибки внутри неё, и сделать структуру кода более лёгкой для постижения. Форматирование имеет значение. Однако, большинство стилей кодирования, включая четыре, упомянутые ранее, служат этим целям одинаково хорошо. В то время как форматирование кода само по себе имеет огромное значение, конкретный выбор в пользу той или иной схемы не имеет значения вообще! Всё что имеет значение, это то, что Вы следуете единому, согласованному стилю; такому стилю, который подходит для всей Вашей команды. Важно, чтобы все приняли этот стиль и строго следовали ему во время всего процесса разработки. В долгосрочной перспективе наилучшим является обучиться самому и обучить свою команду писать код в последовательном, рациональном и читабельном стиле. Однако, время и приверженность команды, необходимые для этого, не всегда доступны. В таких случаях разумный компромисс состоит в том, чтобы предписать использование стандартного инструмента, который должен применяться перед тем как код будет отправлен в систему контроля версий, отправлен на ревизию или иным образом показан другим людям. Прекрасный инструмент авто-форматирования для Perl: perltidy. Он предоставляет расширенный диапазон пользовательских настроек для установки отступов, позиционирования разделителей блоков, выравнивание по типу колонок и позиционирование комментариев. Используя perltidy, Вы можете сконвертировать код подобный этому: if($sigil eq '$'){ в нечто читабельное: if ( $sigil eq '$' ) { Указание всем использовать единый инструмент для форматирования их кода также является простым способом ухода от бесконечных возражений, желчности и догматов, которые всегда окружают обсуждение стиля кодирования. Если perltidy делает за них всю работу, то для разработчиков ничего не будет стоить приспособиться к новым принципам. Они смогут просто настроить макрос редактора который будет "выпрямлять" их код когда это будет им необходимо. 7. Разбивайте код на параграфы, снабжённые комментариямиПараграф — это набор операторов, выполняющих одну задачу: в литературе это последовательность предложений, выражающих одну идею; в программировании — серия инструкций, реализующих один шаг алгоритма. Разделяйте каждый кусок кода на фрагменты, решающие одну задачу, с помощью вставки одной пустой строки между этими фрагментами. Для дальнейшего улучшения сопровождабельности кода, добавляйте вначале каждого параграфа однострочный комментарий, объясняющий, что делает эта последовательность операторов. Типа такого: # Обработать массив, который был распознан... Параграфы полезны, поскольку человек может одновременно сфокусироваться Параграфы — единственный способ объединять небольшие количества связанной информации таким образом, что результирующий "кусок" может поместиться в единственный слот ограниченной по объёму кратковременной памяти читателя. Параграфы позволяют физической структуре разбиения кода на фрагменты отражать и подчёркивать его логическую структуру. Добавление комментариев вначале каждого параграфа ещё более улучшает эффект от разбиения на фрагменты, резюмируя назначение каждого фрагмента (заметьте, назначение, а не поведение). Комментарии к параграфам нужны для объяснения того, почему этот код находится здесь и для чего он нужен, а не для дословного пересказа на естественном языке соответствующих операторов и вычислительных конструкций. Заметьте, однако, что содержимое параграфов имеет здесь второстепенное значение. Важны именно вертикальные отступы, отделяющие параграфы друг от друга. Без них читабельность кода резко снижается, даже при сохранении комментариев: sub addarray_internal { 8. Выбрасывайте исключение вместо возврата специальных значений или установки флаговВозврат специального значения, сигнализирующего ошибку или установка специального флага — это очень широко используемая техника обработки ошибок. Вообще говоря, это принцип сигнализирования об ошибках практически всех встроенных функции Perl. Например, встроенные функции eval, exec, flock, open, print, stat, и system — все возвращают специальные значения в случае ошибки. Некоторые при этом также устанавливают флаг. К сожалению, это не всегда один и тот же флаг. С неутешительными подробностями можно ознакомиться на странице perlfunc. Кроме проблем последовательности, оповещение об ошибках при помощи флагов и возвращаемых значений имеет ещё один серьёзный порок: разработчики могут тихо игнорировать флаги и возвращаемые значения. И игнорирование их не требует абсолютно никаких усилий со стороны программиста. НА самом деле в void-контексте, игнорирование возвращаемых значений — это поведение Perl-программ по умолчанию. Игнорирование флага ошибки также элементарно просто — Вы просто не проверяете соответствующую переменную. Кроме того, игнорирование возвращаемого значения в void-контексте происходит незаметно, ведь нет никак синтаксических зацепок, позволяющих это контролировать. Нет возможности посмотреть на программу и сразу увидеть "вот здесь возвращаемое значение игнорируется!". А это означает, что возвращаемое значение может быть запросто проигнорировано случайно. Резюме: по умыслу или недосмотру программиста индикаторы ошибок часто игнорируются. Это не есть хороший подход к программированию. Игнорирование индикаторов ошибок часто приводит к тому, что ошибки распространяют своё влияние в непредсказуемом направлении. Например: # Найти и открыть файл с заданным именем, Функция 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 'Введите Ваше имя: ') { Такова натура человека: "доверяй и не проверяй". Причина того, что возвращение индикатора ошибки не является лучшей практикой — в природе человека. Ошибки являются (как это предполагается) необычными случаями и маркеры ошибок почти никогда не будут возвращаться. Эти нудные и несуразные проверки почти никогда не делают ничего полезного, так что ими просто тихо пренебрегают. В конце концов, если опустить эти проверки, почти всё работает так же хорошо. Так что гораздо проще не париться с ними. В особенности когда их игнорирование является поведением по умолчанию (вспомните void-контекст)! Не возвращайте специальные значения, когда что-то идёт не так; вместо этого выбрасывайте исключения. Огромное преимущество исключений в том, что они как бы выворачивают наизнанку обычное поведение по-умолчанию, немедленно обращая внимание на необработанные ошибки. С другой стороны, игнорирование исключений требует намеренных и видимых усилий: вы должны предусмотреть явный блок eval для их перехвата. Функция locate_and_open() будет намного понятнее и надёжнее, если в случае ошибок мы выбрасываем исключения: # Find and open a file by name, returning the filehandle Заметьте, что основной цикл for вообще не изменился. Разработчик, использующий locate_and_open() всё так же предполагает, что всё отработает без сбоев. Теперь этому есть обоснование, поскольку если действительно что-то пойдёт не так, выброшенное исключение автоматически завершит цикл. Исключения — это более удачный выбор даже если даже Вы настолько дисциплинированы, что проверяете каждое выходное значение на предмет наличия ошибки: SOURCE_FILE: Постоянные проверки возвращаемых значений вносят посторонний шум в Ваш код в виде проверочных операторов, часто сильно ухудшая читабельность. Исключения, наоборот, дают возможность реализовать алгоритм вообще без вкраплений кода для проверки ошибок. Мы можете вынести обработку ошибок за пределы алгоритма и передать эти функции обрамляющему eval или вообще обойтись без этой обработки: for my $filename (@directory_path) { 9. Добавляйте новые тесты перед тем, как начнёте отладкуПервый шаг в любом процессе отладки -- это выделить наименьший кусок кода, который демонстрирует ошибку. Иногда вам Везёт, и это делают за Вас: To: Этот e-mail защищен от спам-ботов. Для его просмотра в вашем браузере должна быть включена поддержка Java-script Как только Вы сварганили короткий пример, демонстрирующий ошибку, превратите его в серию тестов, типа: use Lingua::EN::Inflect qw( PL_N ); Не пытайтесь сразу пофиксить баг. Сначала добавьте необходимые тесты в Ваш набор тестов. Если у Вас уже есть набор тестов, Вы просто добавляете пару записей в Вашу табличку: my %plural_of = ( Фишка вот в чём: если исходный набор тестов не сигнализирует об этой ошибке, значит этот набор тестов неполноценный. Он просто не выполняет свою работу (по нахождению ошибок) адекватно. Добавьте туда тесты, которые не будут пройдены: > perl inflections.t Как только Ваш набор тестов корректно обнаруживает проблему, Вы исправляете баг и теперь Вы сможете точно сказать что исправили его, поскольку программа снова корректно проходит все тесты. Этот подход к отладке наиболее эффективен когда набор тестов покрывает весь спектр случаев, при которых проявляется проблема. При добавлении тестов для ошибки, не ограничивайтесь просто добавлением одного простого теста. Убедитесь, что Вы включили также различные варианты: my %plural_of = ( Чем тщательнее Вы тестируете программу на предмет наличия ошибок, тем более безошибочной будет программа. 10. Не оптимизируйте код -- замеряйте его производительность!Если Вам нужна функция для удаления дублирующихся элементов массива, вполне естественно, что однострочная версия наподобие этой: sub uniq { return keys %{ { map {$_ => 1} @_ } } } будет более эффективна, чем два оператора: sub uniq { До тех пор пока Вы не будете глубоко знакомы с внутренностями Perl-интерпретатора (а в этом случае Вы наверняка уже будете решать вопросы посложнее), интуиция по поводу относительного быстродействия двух конструкций является ничем иным как неосознанным выбором. Единственный способ узнать какая из двух альтернатив быстрее — замерить каждую из них. Со стандартным модулем Benchmark это просто: # Короткий список не-совсе-уникальных значений... Процедура cmpthese() принимает в качестве аргумента число и далее ссылку на хэш с тестами. Число обозначает либо точное число запусков каждого теста (если это число положительное), либо количество секунд процессорного времени, в течение которого нужно гонять каждый тест (если число отрицательное). Обычные используемые значения — примерно 10'000 повторений или 10 CPU-секунд, но модуль предупредит Вас если тест будет слишком коротким для получения точного замера. Ключи хэша с тестами представляют собой имена тестов, а их значения ‐ код соответствующих тестов. Эти значения могут быть как строками (которые будут выполнены с помощью eval) или ссылками на функции (которые будут вызваны напрямую). Код для замера быстродействия, приведённый выше, выдаст что-то типа: [8 элементов, 10% повторяющихся] Каждая из выведенных таблиц содержит отдельную строку для каждого из поименованных тестов. В первой колонке -- абсолютная скорость каждого кандидата, в повторениях в секунду, а остальные колонки позволяют Вам сравнить полученный результат с остальными двумя тестами. Например последний тест показывает, что grep по сравнению с anon выполняется в 1.83 раза быстрее (это 185 процента). Также grep был на 69 процентов медленнее (на -69 процентов быстрее) чем slic. В целом, все три теста показывают, что решения, основанные на slice неизменно быстрее других на этом наборе данных на этой конкретной машине. Также становится ясно, что с увеличением размера массива, slice всё больше выигрывает у конкурентов. Однако эти два заключения были сделаны на основании всего трёх наборов данных (а именно, на основании трёх запусков замера быстродействия). Для того, чтобы получить более показательное сравнение этих трёх методов, Вы также должны протестировать другие возможности, такие как длинный список неповторяющихся значений или короткий список, состоящий только из повторений. Лучше всего попробуйте тесты на реальных данных, которые Вам нужно будет обрабатывать. Например, если данные являются отсортированным списком из четверти миллиона слов, с минимальными повторениями, а после обработки список должен оставаться отсортированным, то вот тест для такого случая: our @data = slurp '/usr/share/biglongwordlist.txt'; Неудивительно, что решение, основанное на grep здесь на высоте: s/iter anon slice grep Причём решение с grep оставляет список отсортированным. Всё это даёт основания полагать, что превосходство решения, основанного на slice — это частный случай и это решение подрывается растущей стоимостью манипуляций с хэшами в случаях, когда хэш вырастает до слишком больших размеров. Последний пример показывает, что выводы, сделанные Вами на основании тестирования быстродействия с какими-либо наборами данных, распространяются главным образом именно на этот конкретный вид данных. Perl.com Compilation Copyright © 1998-2006 O'Reilly Media, Inc. Дэмиан Конвей (Damian Conway), Perl.com |
| « Пред. | След. » |
|---|


