17 March 2010

Правила программирования Delcam International plc

Website development
Автор топика Код, который приятно читать поделился своими правилами для написания хорошего кода, и в конце поинтересовался — «Какие ещё правила вы используете? Как сделать код читаемее?». У меня как раз под рукой есть документ из одной крупной английской конторы на которую я работал несколько лет назад (Delcam International plc), занимающейся разработкой CAD/CAM систем и в которой трудится несколько сотен, если не тысяч программистов из разных стран мира.

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

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


Набор правил и руководств по программированию на С++


в компании Delcam International plc






1 Введение
2 Основные правила и рекомендации
3 Соглашения об именах
4 Формат кода и форматирование текста
5 Комментарии
6 Операторы управления
7 Файлы
8 Препроцессор
9 Классы
10 Переменные
11 Функции
12 Конструкторы/Деструкторы/Операторы присваивания
13 Приведение типов
14 Выделение и удаление памяти
15 Дружественные классы и дружественные функции
16 Написание переносимого кода
Приложение A: Примеры форматирования кода






1 Введение
1. 1 Предмет


Этот документ представляет из себя набор правил и руководств для программирования на C++ в компании Delcam International plc. Наша цель как компании — писать корректные программы, легкие в поддержке (как самим автором так и другими программистами). Следовательно, цели этого стандарта сделать весь код Delcam:

  1. Легко поддерживаемым: легким для понимания; настолько ясным и простым, насколько возможно практически; последовательным по стилю. Надежным: свободным от ошибок; устойчивым к введению ошибок во время поддержки; легким при тестировании.
  2. Комбинированным: предназначенным для переноса на различные платформы и компиляторы; годным для повторного использования.



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

1. 2 Источники и цели

При подготовке этого стандарта использовались несколько книг и документов:


Ellemtel
Mats Henricson и Eric Nyquist, Programming
in C++, Rules and Recommendations
, (Ellemtel Telecommunication System
Laboratories)

ARM
Margaret Ellis и Bjarne Stroustrup, The
Annotated C++ Reference Manual, ( Addison Wesley)



Policy
Ed Lambourne, Delcam Software Development Policies,
(Internal memo: Delcam International Plc.)

cppstd
David Dunnington et al., The cppstd gripe file,
(Internal document: Delcam International Plc.)


Philosophy
Paul Davies, David Dunnington, Data Model programming
philosophy
, (Internal document: Delcam International Plc.)

Maguire
Steve Maguire, Writing Solid Code


Oualline
Steve Oualline, C Elements of Style, ( M & T
Publishing)

Hobbs

Steve Hobbs, Delcam Draft C Programming Standard,
(Delcam International Plc.)

Ellemtel — великолепнейший источник: почти все правила и рекомендации пересекаются с этим документом. Неоценимым источником был также cppstd gripe file, который дал большое количество информации. Благодарность всем тем, кто работал над cppstd, особенно Paul Davies — внесшему наибольший вклад.
В данном стандарте мало обучающей информации. Предполагается, что вы знакомы с основными конструкциями, синтаксисом и использованием C++. Вопросы проектирования и разработки программного обеспечения также выходят за рамки этого документа. Новичкам в языке рекомендуется обращаться к книгам за подобной информацией. Для начала, попробуйте The Complete C++ Primer by Keith Weiskamp and Bryan Flamig (Academic Press). Хорошая книга по разработке программного обеспечения — Object Oriented Software Construction by Bertrand Meyer.


1. 3 Правила и рекомендации

Несоответствие стандарту может быть двух видов: нарушение правила и не следование рекомендации. Характеристики правила:

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


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

Принуждение:
Комиссия, принимающая код, должна отвергнуть его, если он нарушает правила.


Управление:
Код может подвергаться автоматическим тестам и/или проверкам.

Характеристики рекомендации:

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


Исключения:
Основные исключения часто документируются вместе с отдельными рекомендациями. Если вы не уверены верная ли у вас причина, обсудите этот вопрос с администратором стандарта.

Принуждение:
Будьте готовы отстаивать нарушение рекомендации.


Управление:
Код может подвергаться автоматическим тестам и/или проверкам.


Если вы считаете, что правило или рекомендация неверны, лучше попытаться изменить их, чем игнорировать. Это утверждение подчеркивается Policy. 7.
Нет необходимости переформатировать или переписывать старый код. Приветствуется (но не обязывается) делать изменения, согласующиеся со стандартом, когда вам нужно модифицировать код во время поддержки. Можно сохранять способ форматирования, применяемый в старом коде его при расширении, но при внесении изменений соблюдайте правила и рекомендации, не связанные с форматированием.


1. 4 Организация

Авторы этого документа люди (несмотря на слухи, говорящие об обратном) и следовательно он не идеален. Документ задуман развиваемым и будет со временем улучшаться.
Чтобы способствовать 'эволюции' стандарта, управление версиями документа осуществляется системой 'gripe' с названием пункта 'cppstd' (C-Plus-Plus-STandarD). Наберите `gripe -doc' для изучения gripe и `gripe cppstd -help', чтобы получить детальную информацию по пункту cppstd. Замечания, вопросы, предложения и т. д. должны регистрироваться через gripe.
Правила и рекомендации заданы ключевыми словами, а не числами, чтобы не иметь дела с устаревающими числами.


  • Ключевое слово с предшествующим
    r- — правило.
  • Ключевое слово с предшествующим
    s- — рекомендация.


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



2 Основные правила и рекомендации

2. 1 Руководства по политике


r-gen-golden: Следуйте Золотому Правилу.
Каждый раз, когда нарушается правило, это должно быть ясно документировано. Нарушение должно быть санкционировано людьми, поддерживающими стандарт и он в результате будет изменен.


s-gen-dupl: Не дублируйте код.
Дублирование кода рассматривается как смертный грех. Выделяйте код, общий для двух функций, в отдельную
функцию. Используйте существующие библиотеки классов, когда это возможно. В частности, представляют интерес две стандартные библиотеки классов
Delcam: utils.dev, которая содержит программные средства, отсутствующие в стандарте языка C++ (например, абстрактный класс строк) и contain
er.dev, которая содержит основные абстрактные типы данных для хранения объектов.

s-gen-reuse: Разрабатывайте повторно используемый код.
Когда возможно, программное обеспечение должно быть разработано в контексте библиотек связанных инструментальных средств, с хорошо определенным, ясно написанным интерфейсом. Библиотеки должны быть связаны ясным, документированным, иерархическим способом.


s-gen-simple: Сохраняйте код простым.
Пишите код для 'среднего' программиста. 'Умный' код сложен для понимания, и следовательно меньше
подходит для повторного использования и труден в поддержке. Всегда склоняйтесь в сторону простоты и читабельности. В литературе это известно как kiss(«keep it simple, stupid») принцип.


s-gen-test: Пишите тестируемый код и тестируйте его.
Создайте основательную подсистему проверок и используйте их потом. Пишите и тестируйте маленькие куски кода. Чаще используйте отладчик, чтобы пройти шаг за шагом ваш код, недожидайтесь пока у вас появится ошибка. Исправляйте ошибки, сразу как они обнаружены, устраняйте причину, а не симптом.



2. 2 Основные руководства при написании
программ



s-gen-mc: Помещайте в одно место платформенно зависимый код.
Помещайте платформенно зависимый код в специальный файл. В Delcam это apsl.dev/apsl/machine.h.


s-gen-comp: Если код должен быть изменен в следствии ошибки в каком-то компиляторе, комментируйте
код, чтобы указать на это, приводя детали о компиляторе.

Сообщите об ошибке в компиляторе, используя 'gripe cppbug', чтобы мы могли могли принять меры к ее исправлению.

s-gen-opt: Оптимизируйте код только при необходимости.
Оптимизируйте код только если вы знаете, что вы имеете проблемы с производительностью. Если ваша программа слишком медленна, используйте groff++ или эквивалентное средство, чтобы определить точную причину проблемы, перед тем как начать оптимизацию.


s-gen-warn: Разрешите все предупреждения компилятора.
Активизируйте предупреждения компилятора, если это возможно, чтобы локализировать ошибки и уменьшить часть кода, которая должна быть переписана для каждой новой версии компилятора (потому что сегодняшние предупреждения часто становятся завтра ошибками).
Пример: используя компилятор на базе Cfront, компилируйте с флагом +w, чтобы устранить как можно больше
предупреждений.


s-gen-assert: Используйте assertion для проверки вашего кода.
Assertion описаны в Maguire. Реализация assertion в Delcam находится в utils.dev/utils/ut_assert.h и описана в d0170400.



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




3 Соглашения об именах


Выбор осмысленных имен — важная часть в написании легко читаемого кода. Последовательное использование соглашений об именах улучшает читабельность кода, позволяя человеку быстро разобраться в структуре программы. Другое требование — установить порядок, чтобы минимизировать риск возникновения конфликтов имен. Этот раздел описывает соглашения об именах, разработанных, чтобы способствовать читабельности, последовательности и поддерживать крупномасштабную разработку. Здесь и далее по документу, тип — это class, enum или typedef.

3. 1 Префиксы библиотек

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



r-nam-libpx: Назначьте префикс каждой библиотеке.
С каждой библиотекой должен быть ассоциирован уникальный для этой библиотеки двух или трех буквенный префикс. Буквы префикса должны быть в нижнем регистре.

r-nam-glob: Используйте префикс библиотеки для всех глобальных имен.
Если объект внутри библиотеки входит в глобальное пространство имен, его имя должно начинаться с префикса библиотеки.



Правила, говорящие о том как имя составляется из префикса, даны в следующем разделе. Ответственность автора или человека, занимающийся поддержкой — обеспечить отсутствие конфликтов имен в библиотеке. Префикс будет предупреждать (надеемся!) конфликты имен вне библиотеки.

3. 2 Имена типов, переменных, функций, макросов

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



r-nam-type: Имена типов начинаются с большой буквы.
Имена типов пишутся маленькими буквами, каждое новое слово начинается с большой буквы. Они не должны содержать подчеркивания. Именам должен предшевствовать префикс написанный маленькими буквами. Вложенные классы/перечислимые типы (enum) редки и их использование не советуется в виду возможных проблем с компиляторами SGI и HP. Если они, однако, используются, их имена должны выбираться, тем не менее, как для глобальных имен классов из-за проблем с компилятором.
Пример: class zzMyClass {
...



r-nam-var: Следуйте предписанному способу именования переменных, локальных констант и аргументов функций.
Имена переменных, локальных констант и аргументов функций пишутся маленькими буквами, со словами, разделенными одним подчеркиванием. Их префиксы должны быть следующими:




Пример: const int local_const; Пример:
int local_var; Пример: int zz_global_var; Пример: int m_member_var; Пример:
static int s_static_member_var; // (рекомендуется)

Пример: void f(int a_argument);
// (рекомендуется)



r-nam-glcon: Имена глобальных констант и констант перечислений (enum) пишутся большими буквами.
Используйте один символ подчеркивания для разделения слов. Префикс должен быть набран в нижнем регистре. Пример:
const int zzGLOBAL_CONST;

Пример: enum zzKind { zzONE_KIND,
..


r-nam-func: Имена функций пишутся маленькими буквами.
Используйте один символ подчеркивания для разделения слов. Глобальные функции предваряются префиксом из маленьких букв со следующим за ним символом подчеркивания. Функции-члены класса не требуют префикса. Исключение: r-nam-farg.

Исключение: r-nam-fclass.

r-nam-farg: Если глобальной функции передается аргумент типа, содержащего префикс, то необязательно добавлять префикс к имени функции.
Риск возникновения конфликта имен в этом случае весьма мал. Это позволяет программисту писать функции, тесно связанные с классом, без синтаксического загромождения имени префиксом. Например, вы можете захотеть объявить функцию-член класса 'int zzClass::f() const' и дружественную функцию 'int f(const zzClass&)', которые позволяют упростить синтаксис, когда экземпляр участвует в вычислении выражения:
'i = (a+b).f()' заменится на 'i = f(a+b)'. Пример: void zzClass::member(); Пример:
void zz_global_func();

Пример: void global_func(const
zzClass&);



r-nam-fclass: Если имя типа является частью имени функции, тогда имя типа должно быть написано точно также как в определении типа (включая большие буквы).
Если тип объявлен в той же библиотеке, что и функция, необязательно добавлять дополнительный префикс библиотеки к имени функции, т.к. он уже есть в имени класса и также как и в r-nam-farg риск возникновения конфликта имен с другой библиотекой очень мал. Пример: void test_zzClass(); Пример: void zzClass::member();
Пример: void zz_global_func();

Пример: void global_func(const
zzClass&);


s-nam-funcstd: Используйте
стандартные имена функций-членов класса, если они существуют.

Некоторые функции-члены класса
являются общими для многих классов, и их имена стандартизированы: см.
раздел 12.2.1. Если функция доступа устанавливает атрибут класса <attrib> типа <zzAttrib> она
должна иметь прототип void set_<attrib>(const <zzAttrib>&);.
Если функция доступа возвращает атрибут она должна иметь прототип
const <zzAttrib>& <attrib>() const или <zzAttrib> <attrib>()
const, если атрибут имеет оператор присваивания и конструктор копирования
(см. раздел 12.3).


r-nam-macro: Имена макроопределений пишутся большими буквами.
Используйте один символ подчеркивания для разделения слов. Префикс библиотеки должен вводится большими буквами, со следующим за ним символом подчеркивания. См. также r-def-minusd §8. Исключение: макроопределения, вовлеченные в предотвращение включений заголовочных файлов, должны быть описаны как в r-cpp-ifdef §8.2.

Исключение: макроопределения 'объявления' и 'реализации', связанные с основными классами могут называться произвольным образом.
Пример: #define ZZ_LIB_MACRO
0

r-nam-und: Не начинайте
имена с символа подчеркивания.

Соглашения об именах исключают имена типов или переменных, начинающихся символом подчеркивания. Использование двух символов подчеркивания в идентификаторах зарезервировано согласно ANSI-C стандарту для внутреннего использования компилятором.
Имена, начинающиеся одним символом подчеркивания часто используются стандартными библиотечными функциями (напр. '_main' и '_exit' ).


r-nam-twound: Не используйте два символа подчеркивания подряд.


В приложении B приведена общая таблица всех соглашений об именах.

3. 2 Рекомендации по стилю



s-nam-abbrev: Реже используйте сокращения.
Сокращайте слова, только когда сокращение широко известно (либо в пределах компании, либо в мире).
Например: 'DDX' — сокращение, которое будет понято в Delcam; переменная цикла 'i' — 'стандартное' программистское сокращение и много лучше чем 'array_index'. Труднопроизносимые имена считаются плохими.


s-nam-length: Не переусердствуйте с длиной имени.

Не делайте имена слишком длинными, потому что некоторые среды накладывают ограничения на число распознаваемых символов. Например, команда unix 'ar' усекает имена файлов длиной больше 15 символов.

s-nam-desc: Используйте простые, описательные имена.
Имя переменной должно говорить об ее использовании. Самое лучшее, если имя переменной содержит одно, два слова. Если три, то это уже обращает на себя внимание, а если четыре, то имя может быть трудным для прочтения. При создании имени из двух слов, поместите самое важное слово первым.

s-nam-dif: Имена должны существенно различаться.
Не используйте имена переменных, отличающихся только на один, два символа. Не используйте имена типов, различающихся только регистром букв. В то же время, используйте похожие имена для переменных, которые выполняют сходные функции.


s-nam-std: Избегайте применения имен, используемых в стандартной библиотеке.
Это рекомендуется даже для локальных объектов, потому что макроопределения не ограничены рамками правил (проблема будет смягчена, если объекты в стандартной библиотеке имеют префиксы). Это рекомендация, а не правило, потому что, иногда, очень общие (а значит широко применяемые) слова (например, 'index') используются в стандартной библиотеке.


3. 4 Имена файлов


Существует четыре типа файлов:

  1. Заголовочные файлы содержат определения типов, прототипы функций и т. д. Файлы исходных текстов содержат реализации классов и т. д. Inline файлы содержат inline функции.
  2. Outline файлы гарантируют, что не-inline версии inline функций доступны для отладочных целей.



Правила именования файлов приведены ниже.


r-nam-clfile: Файлы, содержащие реализации класса, должны иметь то же имя, что и класс.
Имя файла должно быть написано в том же регистре, что и имя класса. Это также применимо и к основным классам. Правило применяется к заголовочным, inline файлам и файлам исходных текстов.


r-nam-utfile: Файл, реализующий набор утилит, должен иметь имя, набранное в нижнем регистре, предваренное префиксом библиотеки.
Префикс должен быть отделен от имени одним символом подчеркивания. Правило применяется к заголовочным, inline файлам и файлам исходных текстов.

r-nam-enum: Заголовочный файл, содержащий только одно перечисление (enum), может иметь то же имя, что и перечисление.

r-nam-ext: Расширения файлов: заголовочных .h, исходного текста/outline .c, inline .i.

Ellemtel предпочитает '.hh', '.cc' и '.icc', резервируя '.h' и '.c', для файлов, которые могут приниматься и C и C++ компиляторами. Поскольку весь код Delcam, согласно правил, должен компилироваться C++, мы не делаем таких различий.

r-nam-outline: Outline файл имеет то же имя, что и соответствующий заголовочный, с добавленной 'I' (перед расширением файла).

r-nam-split: Если файл исходного текста становится большим и необходимо его разбить, имя каждого нового файла, есть имя старого файла с последующим символом подчеркивания и с последующим словом в нижнем регистре.
Например, если файл zzClass.c содержит функцию-член zzClass::member(), которая реализуется в отдельном файле, файл может называться zzClass_member.c.


Примеры: zzClass.c: файл исходного текста, реализующий класс zzClass. zz_utils.h: заголовочный файл утилит. zz_utils.i: реализация встроенных (inline) функций, определенных в zz_utils.h.
zzClassI.c: outline версия встроенных (inline) функций, определенных в zzСlass.h.








4 Формат кода и форматирование текста

Эта секция определяет как должен выглядеть код.
4. 1 Общий формат кода



r-fmt-brace: Используйте единственно верный стиль расстановки скобок.
Скобки должны расставляться согласно 'краткой' форме K & R: открывающая скобка располагается в конце строки сразу за предшествующим кодом. Закрывающая скобка располагается на отдельной строке, в колонке, соответствующей первому не пробельному символу в строке, где расположена открывающая скобка. Код, между двумя скобками оформляется с отступом.
Пример:


if (function_returns_true()) {
execute_this_code();
}



Исключение: открывающая скобка ({) функции должна находится на отдельной строке.
Исключение: аргумент функции может занимать отдельную строку.

r-fmt-lines: Длина строки на должна превосходить 79 символов.
Этот предел был выбран из-за того, что стандартная установка 'emacs', используемая нами, создает окно, где строки большей длины сворачиваются. К тому же, 80x24 (80x25) — часто встречающийся размер окна, пошедший от старых терминалов, и свернутые строки сложнее читать.

r-fmt-indent: Отступ равен двум пробелам.

Отступайте на один уровень для каждого нового уровня логики.
Исключение: последовательности if-else легче читаются, когда элемент выравнивается — см. r-if-elseif.

r-fmt-tabs: Не используйте символы табуляции.
Они делают трудным сохранение отступов во время поддержки кода. Избегайте использования и других экзотических символов.

s-fmt-sep: Не используйте не нужных разделителей.

Пример: не используйте дополнительные запятые после последнего значения в перечислениях ('строгие' компиляторы будут выдавать ошибки, напр. IBM).


4. 2 Операторы



s-fmt-long: Избегайте применения очень длиных операторов.
Вместо этого используйте несколько более коротких операторов.

s-fmt-stat: Помещайте каждый оператор на отдельной строке.
Если вы не делаете этого, вы должны быть уверены, что ясность программы от этого улучшиться.

s-fmt-col: Когда в одной строке располагаете несколько операторов, группируйте операторы в колонки.


s-fmt-split: Разбивая строку, отступите еще на один уровень.
Если вам необходимо разбить строку на несколько строк, сделайте отступ всех строк кроме первой на один дополнительный уровень. Исключение: см. s-fmt-splitexpr.
Исключение: структуры управления разбиваются так, как описано в соответсвующем разделе (§6).


4. 3 Выражения



s-fmt-splitexpr: При разбивании выражения, подвыражения одного логического уровня должны быть выровнены вертикально.
Избегайте разбивать выражение там, где уровень вложенности высок. Помещайте арифметические и логические операторы в конце строки, а не в начале следующей. Так гораздо проще проследить логику длинного выражения.
Пример:


result = (((x1 + 1) * (x1 + 1)) -
((y1 + 1) * (y1 + 1)));


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



result = a_long * hypotenuse +
2.0*cos(alpha);


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


if (( a && b) ||
c) { // Не делайте так

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



if ((a && b) ||
©) { // Гораздо лучше



s-fmt-prec: Чаще используйте скобки, чтобы сделать ясным приоритет операций.
Не ищите приоритет в таблице, если вы не помните его: используйте скобки вместо этого. Если вы не можете запомнить приоритет операции, то есть большая вероятность того, что тот, кто читает ваш код не будет помнить его тоже. В качестве правила, можно считать известным приоритет между операциями +, / и +, -; используйте скобки во всех остальных случаях.


4. 4Пробелы


Эти рекомендации применяются к пробелам внутри строки, а не к тем, что используются для отступов.

  1. Никогда не используйте больше одного пробела
    (напр., поиск на ' class <class> ' должен найти определение класса <clas
    s>).
  2. Никогда не используйте пробелов между переменной-указателем и '[' (напр. поиск на 'array[' должен найти обращения к элементам массива). Никогда не используйте пробелов между именем функции и '(' (напр., поиск на 'func(' должен найти все вызовы функции). Всегда вводите пробел между '(' и оператором управления (напр. используйте 'while (x)', а не 'while(x)'). Всегда вводите пробел перед '{' и '}' (исключая случай, когда они находятся в начале строки).
  3. Всегда вводите пробел после '{' и '}' (исключая случай, когда они находятся в конце строки).
  4. Никогдане вводите пробел перед символами ',;)]'.
  5. Никогда не вводите пробел после '(' или ']'. Пробелы рекомендуется ставить после запятых в определениях и выражениях, но они не обязательны между аргументами в вызове функции. Пробелы рекомендуется ставить вокруг '?:'. Пробелы рекомендуется ставить вокруг оператора присваивания. Не вводите пробелы между унарными операциями и операндами. Не используйте пробелов вокруг '::', '. ' или '->'. Не вводите пробелы после ключевого слова 'operator'.
  6. За исключением вышеприведенных рекомендаций, пробелы необязательны вокруг операторов.



s-fmt-space: Следуйте рекомендациям по использованию пробелов.








5 Комментарии


Этот раздел поясняет как писать комментарии. Другая часть этого документа объясняет где следует писать комментарии.
Tags:программированиеправила программирования
Hubs: Website development
+2
1.7k 6
Comments 4
Popular right now