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
, а неisset
isset
проверяет не ключ на его наличие, а значение этого ключа, если он есть. Это разные методы с разным поведением и назначением. Если вы хотите проверить значение ключа, то делайте это явно. Сначала явно проверьте наличие ключа через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;