Codeconventions
Правила написания кода (Code Conventions) в компании "Программный Центр"
Code Conventions — это правила, которые нужно соблюдать при написании любого кода. Мы различаем Code Style и Code Conventions. Для нас Code Style — это внешний вид кода. То есть расстановка отступов, запятых, скобок и прочего. А Code Conventions — это смысловое содержание кода. Правильные алгоритмы действий, правильные по смыслу названия переменных и методов, правильная композиция кода. Соблюдение Code Style легко проверяется автоматикой. А вот проверить соблюдение Code Conventions в большинстве случаев может только человек.
Ценности
Главная цель Code Conventios — сохранение низкой стоимости разработки и поддержки кода на длинной дистанции.
Основные ценности, помогающие достичь этой цели:
-
Читаемость
Код должен легко читаться, а не легко записываться. Это значит, что такие вещи как синтаксический сахар (если он направлен на ускорение записи, а не дальнейшего чтения кода) вредны. Не нужно экономить переменные, буквы для их названий и так далее.
-
Вандалоустойчивость
Код надо писать так, чтобы у разработчика, который с ним будет работать, было как можно меньше возможности внести ошибку.
-
Поддержание наименьшей энтропии
Энтропия — это количество информации, из которой состоит проект (информационная емкость проекта). Код проекта должен выполнять продуктовые требования с сохранением наименьшей возможной энтропии.
Принципы
Принципы — это способы соблюдения описанных выше ценностей. Они чуть более детальны, содержат основные методологии разработки и подходы, которыми мы руководствуемся.
Код должен быть:
- Понятным, явным. Явное лучше, чем неявное.
- Удобным для использования сейчас.
- Удобным для использования в будущем.
- Должен стремиться к соблюдению принципов KISS, SOLID, DRY, GRASP.
- Код должен обладать низкой связанностью и высокой связностью (подробно это описано в GRASP). Любая часть системы должна иметь изолированную логику и при надобности внешний интерфейс, который позволяет с этой логикой работать. Любая внутренняя часть должна иметь возможность быть измененной без какого-либо ущерба внешним системам.
- Код должен быть таким, чтобы его можно было автоматически отрефакторить в IDE (например, Find usages и Rename в PHPStorm). То есть должен быть слинкован типизацией и PHPDoc'ами.
- В БД не должны храниться части кода (даже названия классов, переменных и констант), так как это делает невозможным автоматический рефакторинг.
- Последовательным. Код должен читаться сверху вниз. Читающий не должен держать что-то в уме, возвращаться назад и интерпретировать код иначе. Например, надо избегать обратных циклов do {} while ();
- Должен иметь минимальную цикломатическую сложность.
Общие правила
-
Запрещен неиспользуемый код
Если код можно убрать, и работа системы от этого не изменится, его быть не должно.
Плохо:
if (false) { legacyMethodCall(); } // ... $legacyCondition = true; if ($legacyCondition) { finalizeData($data); }Хорошо:
// ... finalizeData($data); -
Название переменных должно соответствовать содержанию
Нельзя писать короткие названия, например
$c. Нельзя назвать переменную$dayи хранить в ней массив статистических данных за этот день. -
Часто упоминаемые объекты именуются одинаково во всем проекте
Плохо:
$customer = new User(); $client = new User(); $object = new User();Хорошо:
$user = new User(); -
Признак объекта добавляется к названию
Если это отфильтрованный по какому-то признаку заказ, то признак добавляется к названию. Например,
$unpaidOrder. -
Переменные, отражающие свойства объекта, должны включать название объекта
Плохо:
$project = new Project(); $name = $project->name; $project = $project->name;Хорошо:
$project = new Project(); $projectName = $project->name; -
Переменные по возможности должны называться на корректном английском
Плохо:
$usersStored = [];Хорошо:
$storedUsers = [];Исключение: сгруппированные по некому признаку поля или константы. В этом случае можно использовать префикс.
-
К переменной нельзя обращаться по ссылке (через &)
Амперсанды могут использоваться только как логические или битовые операторы.
Плохо:
function removePrefix(string &$name) { // ... }Хорошо:
function removePrefix(string $name): string { // ... return $result; } -
Переменные и свойства объекта должны являться существительными и называться так, чтобы они правильно читались при использовании, а не при инициализации
Плохо:
$object->expire_at $object->setExpireAt($date); $object->getExpireAt();Хорошо:
$object->expiration_date; $object->setExpirationDate($date); $object->getExpirationDate(); -
В названии переменной не должно быть указания типа
Нельзя писать
$projectsArray, надо писать просто$projects. Это же касается и форматов (JSON, XML и т.п.), и любой другой не относящейся к предметной области информации.Плохо:
$projectsList = $repository->loadProjects(); $projectsListIds = $utils->extractField('id', $projectsList);Хорошо:
$projects = $repository->loadProjects(); $projectsIds = $utils->extractField('id', $projects); -
Нельзя изменять переменные, которые передаются в метод на вход
Исключение — если эта переменная объект.
Плохо:
function parseText(string $text) { $text = trim($text); // ... }Хорошо:
function parseText(string $text) { $trimmedText = trim($text); // ... }
Нельзя нескольким переменным присваивать одно и то же значение
Плохо:
```
function loadUsers(array $ids)
{
$usersIds = $ids;
// ...
}
```
-
Оператор
cloneдолжен использоваться только в тех случаях, когда без него не обойтисьТакже можно использовать
clone, если без него код серьёзно усложнится, а с ним станет понятным и очевидным. Простой пример — клонирование объектов DateTime. Или использование клонирования для сравнения двух версий объекта: старой и новой. -
Запрещено использовать результат операции присваивания
Плохо:
$foo = $bar = strlen($someVar); // ... $this->_callSomeFunc($bar = strlen($foo)); // ... if (strlen($foo = json_encode($bar)) > 100) { // ... }Хорошо:
$bar = strlen($someVar); $foo = $bar; // ... $bar = strlen($foo); $this->_callSomeFunc($bar); // ... $foo = json_encode($bar); if (strlen($foo) > 100) { // ... } -
У свойства сущности должно быть одно назначение
Запрещено использовать одно свойство для разных целей в зависимости от ситуации. В таком случае нужно создать дополнительное свойство.
Логические переменные и методы
-
Названия
booleanметодов и переменных должны содержать глаголis,hasилиcanПеременные правильно называть, описывая их содержимое, а методы — задавая вопрос. Если переменная содержит свойство объекта, следуем правилу признак объекта добавляется к названию.
Плохо:
Хорошо:$isUserValid = $user->valid(); $isProjectAnalytics = $accessManager->getProjectAccess($project, 'analytics');$userIsValid = $user->isValid(); $projectCanAccessAnalytics = $accessManager->canProjectAccess($project, 'analytics');Такое именование позволяет легче читать условия:
// if user is valid, then do something if ($userIsValid) { // do something } -
Запрещены отрицательные логические названия
Плохо:
if ($project->isInvalid()) { // ... } if ($project->isNotValid()) { // ... } if ($accessManager->isAccessDenied()) { // ... }Хорошо:
if (!$project->isValid()) { // ... } if (!$accessManager->isAccessAllowed()) { // ... } if (!$accessManager->canAccess()) { // ... } -
Не используйте boolean переменные (флаги) как параметры функции
Флаг в качестве параметра это признак того, что функция делает больше одной вещи, нарушая принцип единственной ответственности (Single Responsibility Principle или SRP). Избавляйтесь от них, выделяя код внутри логических блоков в отдельные ветви выполнения.
Плохо:
function someMethod() { // ... $projectNotificationIsEnabled = $notificationManager->isProjectNotificationEnabled($project); storeUser($user, $projectNotificationIsEnabled); } function storeUser(User $user, bool $isNotificationEnabled) { // ... if ($isNotificationEnabled) { notify('new user'); } }Хорошо:
function someMethod() { // ... storeUser($user); if ($notificationManager->isProjectNotificationEnabled($project)) { notify('new user'); } } function storeUser(User $user) { // ... }
Работа с массивами
-
Для конкатенации массивов запрещено использовать оператор
+Обратите внимание, что
array_mergeвсе числовые ключи приводит к int, даже если они записаны строкой.Плохо:
return $initialData + $loadedData;Хорошо:
namespace Service; class ArrayUtils { public function mergeArrays(array $array1, array $array2): array { return array_merge($array1, $array2); } } public function someMethod() { return $this->_arrayUtils->mergeArrays($initialData, $loadedData); } -
Для проверки наличия ключа в ассоциативном массиве используем
array_key_exists, а неissetissetпроверяет не ключ на его наличие, а значение этого ключа, если он есть. Это разные методы с разным поведением и назначением. Если вы хотите проверить значение ключа, то делайте это явно. Сначала явно проверьте наличие ключа черезarray_key_existsи обработайте ситуацию его отсутствия, затем приступайте к работе со значением.Плохо:
function getProjectKey(array $requestData) { return isset($requestData['project_key']) ? $requestData['project_key'] : null; }Хорошо:
function getProjectKey(array $requestData) { if (!array_key_exists('project_key', $requestData)) { return null; } return $requestData['project_key']; }
Работа со строками
-
Строки обрамляются одинарными кавычками
Двойные кавычки используются только, если:
- Внутри строки должны быть одинарные кавычки
- Внутри строки используется подстановка переменных
- Внутри строки используются спец. символы
\n,\r,\tи т.д.
Плохо:
$string = "Some string"; $string = 'Some \'string\''; $string = "\t".'Some string'."\n";Хорошо:
$string = 'Some string'; $string = "Some 'string'"; $string = "\tSome string\n"; -
Вместо лишней конкатенации используем подстановку переменных в двойных кавычках
Плохо:
$string = 'Object with type "' . $object->type() . '" has been removed';Хорошо:
$string = "Object with type \"{$object->type()}\" has been removed";
Работа с датами
-
Дата всегда должна быть представлена DateTime, интервал как DateInterval
Исключение, если в метод нужно передать объект \Bitrix\Main\Type\DateTime.
Плохо:
$date = $request->get('date'); $interval = 86400*30; loadSomeData($date, $interval);Хорошо:
$date = $this->_dateService->instance($request->get('date')); $interval = new \DateInterval('P30D'); loadSomeData($date, $interval);
Работа с пространствами имён
-
Все пространства имён должны быть подключены через use в начале файла. В самом коде не должно быть обратного слеша перед названием пространства имён
Плохо:
$object = new \Some\Object();Хорошо:
use Some; $object = new Some\Object(); -
Нельзя подключать несколько классов из одного пространства имён через
useПлохо:
use Entity\User; use Entity\Project; $user = new User(); $project = new Project();Хорошо:
use Entity; $user = new Entity\User(); $project = new Entity\Project();
Работа с методами
-
Должна быть использована максимально возможная типизация для вашей версии
PHP. Все параметры и их типы должны быть описаны в объявлении метода либо вPHPDoc. Возвращаемое значение тожеПлохо:
/** * @param $id * @param $name * @param $tags * @return mixed */ function storeUser($id, $name, $tags = []) { // ... }Хорошо:
// в PHP 7.1 тип элементов массива в объявлении метода указать нельзя, поэтому добавляем PHPDoc /** * @param int $id * @param string $name * @param string[] $tags * @return User */ function storeUser(int $id, string $name, array $tags = []): User { // ... } -
Все возможные типы должны быть определены в
PHPDocНаибольшую пользу это приносит при работе с массивами:
Плохо:
/** * @param array $users * @param mixed $project * @param int $timestamp * @return mixed */ public function someMethod($users, $project, $timestmap) { foreach ($users as $user) { // IDE не сможет определить тип $user } // ... }Хорошо:
/** * @param Users[] $users * @param Project $project * @param int $timestamp * @return Foo */ public function someMethod(array $users, Project $project, int $timestmap): Foo { foreach ($users as $user) { // подсказки IDE и рефакторинг работают корректно } // ... } -
В PHPDoc у аргументов не надо указывать
nullПлохо:
/** * @param string|null $sortBy * @return \DateTimeZone[] */ public function getTimeZonesList($sortBy = null) { // ... }Хорошо:
/** * @param string $sortBy * @return \DateTimeZone[] */ public function getTimeZonesList($sortBy = null) { // ... } -
Название метода должно начинаться с глагола и соответствовать правилам именования переменных
Плохо:
public function items() { // ... } public function convertedDataObject(array $data) { // ... }Хорошо:
public function loadItems() { // ... } public function convertDataToObject(array $data) { // ... } -
Все методы класса по умолчанию должны быть
privateЕсли метод используется наследниками класса, то он объявляется
protected. Если используется сторонними классами, тогдаpublic. -
Параметры в методах должны следовать в следующем порядке:
обязательные→часто используемые→редко используемыеНужно соблюдать читаемость при написании вызова.
Плохо:
public function method($required, $practicallyUnused = 5, $often = [], $lessOften = null) public function filter($value, $name, $operator) // ...$service->filter(15, "id", "=")Хорошо:
public function method($required, $often = [], $lessOften = null, $practicallyUnused = 5) public function filter($name, $operator, $value) // ...$service->filter("id", "=", 15) -
Если переменные, объявленные на вход к методу, могут быть
null, они должны явно обозначаться как таковыеХорошо:
public function someMethod(string $projectName = null) { // ... }
Возврат результата работы метода
-
Метод всегда должен возвращать только одну структуру данных (или
null) или ничего не возвращатьМетод не может в разных ситуациях возвращать разные типы данных.
Плохо:
function loadUser() { if ($someCondition) { return ['id' => 1]; } return new User(); }Хорошо:
function loadUser(): User { if ($someCondition) { $user = new User(); $user->id = 1; return $user; } return new User(); } -
В больших методах возвращаемая переменная должна называться
$resultЕсли у вас большой метод (больше 15 строк), возвращаемая переменная должна называться
$result, если с ней могут происходить изменения в середине работы метода. В любом месте в методе должно быть понятно, где вы оперируете результатом, а где локальными переменными.Плохо:
function loadUsers(): array { $users = []; // ... много кода, изменяющего переменную $users return $users; }
Хорошо:
function loadUsers(): array
{
$result = [];
// ... много кода, изменяющего переменную $result
return $result;
}
Работа с классами
-
Трейты имеют постфикс
TraitИсключение, если
traitявляется хелперомХорошо:
trait AjaxResponseTrait { // ... } -
Интерфейсы имеют постфикс
InterfaceХорошо:
interface ApplicationInterface { // ... } -
Абстрактные классы имеют префикс
AbstractХорошо:
abstract class AbstractApplication { // ... } -
Все свойства и константы класса по умолчанию должны быть
privateЕсли свойство используется наследниками класса, то оно объявляется
protected. Если используется сторонними классами, тогдаpublic.
Работа с объектами
-
Все объекты должны быть неизменяемыми (immutable), если от них не требуется обратного
Плохо:
class SomeObject { /** * @var int */ public $id; }Хорошо:
class SomeObject { /** * @var int */ private $_id; public function __construct(int $id) { $this->_id = $id; } public function id(): int { return $this->_id; } }
-
Статические вызовы можно делать только у самого класса. У экземпляра можно обращаться только к его свойствам и методам
Плохо:
$type = $user::TYPE;Хорошо:
$type = User::TYPE;