Полезные практики написания поддерживаемого кода на PHP

от автора

Привет, меня зовут Алексей и я должен признаться, я PHP разработчик. Последние несколько лет плотно занимаюсь проектами на symfony и решил поделиться с сообществом практиками, которые стараюсь соблюдать при работе.

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

1. Зависимости в di указываем явно

Явное лучше неявного. При описании классов не полагаемся на магию symfony di. Вообще мне кажется, что autowiring довольно вредная практика. При явном описании гораздо проще и быстрее при чтении кода понимать какие зависимости использует класс.

Плохо:

App\Services\MyService: ~ 

Хорошо:

app.services.my_service:     class: App\Services\MyService     arguments:         - '@doctrine.orm.entity_manager'         - '@app.services.my_service.dependency' 

2. В DI подставляем реализации, а не интерфейсы

Так повышается читабельность и поощряется множественные имплементации интерфейсов.

Плохо:

app.services.my_service:     class: App\Services\MyService     arguments:         - '@App\OtherService\Producer\ProducerInterface'  App\OtherService\Producer\ProducerInterface:     alias: App\OtherService\Producer\Amqp 

Хорошо:

app.services.my_service:     class: App\Services\MyService     arguments:         - '@app.other_service.producer'  app.other_service.producer:     class: App\OtherService\Producer\Amqp 

3. Не надо пробрасывать в классы весь контейнер целиком

Плохо:

final class MyService implements MyServiceInterface {     public function __construct(private readonly ContainerInterface $container) {}     public function doSomething()     {         $producer = $this->container->get('app.other_service.producer');         $producer->publish();     } } 

Хорошо:

final class MyService implements MyServiceInterface {     public function __construct(private readonly ProducerInterface $producer) {}     public function doSomething()     {         $producer->publish();     } } 

4. Каждый класс закрывается интерфейсом

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

final class MyService implements MyServiceInterface {     // ... } 

5. Чем меньше методов в интерфейсе, тем лучше.

Большие интерфейсы сложнее поддерживать и реализовать. Нарушается принцип единственной ответственности.

Плохо:

interface SomeEntityInterface {     public function getId(): int;     public function getName(): int;     public function getAge(): int; } 

Хорошо:

interface IdAwareInterface {     public function getId(): int; } interface NameAwareInterface {     public function getName(): int; } interface AgeAwareInterface {     public function getAge(): int; } 

6. В интерфейсах указываем какие exception могут быть выброшены

Кроме этих exceptions никакие другие выбрасываться не должны. Если у интерфейса нет блока @throws — значит в реализации все exception должны быть обработаны.

7. Максимально строгая типизация

Используем strict_types=1. Типы указываем максимально жестко.

Плохо:

public function doSomething(mixed $id) {     return $id; } 

Хорошо:

public function doSomething(int $id) {     return $id; } 

8. Флаги в аргументах функций

Нарушается принцип единственной ответственности. Отдельные классы проще тестировать, можно использовать в разных сценариях с помощью композиции.

Плохо:

final class SomeService {     public function doSomething($force = false)     {         if ($force) {             return $this->doForce()         }                  return $this->do()     } } 

Хорошо:

final class ForceService {     public function doSomething()     {         return $this->doForce();     } } 

9. Количество аргументов в функции максимум 3

Если аргументов много — значит метод выполняет слишком много действий, нарушается принцип единственной ответственности. Это сложно тестировать и поддерживать.

Плохо:

final class SomeService {     public function doSomething($own, $two, $three, $four)     {     } } 

10. Не мутировать объекты переданные в метод/функцию

Это неочевидное поведение для клиентского кода и легко приводит к ошибкам в процессе развития и изменения кода.

Плохо:

$item = new Item(); $this->calculateCount($item);  public function calculateCount(Item $item): void {     $count = 0;     // some logic     $item->setCount($count); } 

Хорошо:

$item = new Item(); $item->setCount($this->calculateCount());  public function calculateCount(): int {     $count = 0;     // some logic     return $count; } 

11. Заполнение DTO через конструктор с именованными аргументами, readonly свойствами, без get/set методов

Такая запись читабельна за счёт именованных аргументов, нет лишнего кода get/set, такая dto иммутабельна за счёт readonly.

Хорошо:

final class Item {     public function __construct(         public readonly int $id,         public readonly int $count,         public readonly string $originId,     ) {     } }  $item = new Item(     id: 1,     count: 2,     originId: 'aaa-bbb-ccc', ); 

12. Разбиваем большие классы и методы

Класс меньше ~150 строк, метод меньше ~30 строк. Это значительно упрощает чтение кода.

13. Unit тесты всегда

На любой код обязательно пишем unit тест (кроме dto). Если есть skiptests — то обязательно добавляем ссылку на то место, где это протестировано.

/**  * @skiptests tested in {@see \App\SomeOthreService}  */ final class SomeServiceTest extends TestCase { } 

14. Тестируем поведение, а не детали реализации

Тест должен проверять все варианты поведения модуля, но не должен тестировать внутреннюю реализацию этого поведения.
Не надо в тесте проверять вызов логгера, аргументы исходящих вызовов, вызовы приватных методов, если только в этом не заключается основная логика работы класса. Stubs и dummy заглушки намного предпочтительнее mocks и fakes.

Плохо:

final class SomeServiceTest extends TestCase {     public function testDoSomething()     {         $logger = $this->createMock(LoggerInterface::class);         $logger->expects($this->once())             ->method('info')             ->with('some message');         $service = new SomeService($logger);                  $service->doSomething();     } } 

Хорошо:

final class SomeServiceTest extends TestCase {     public function testDoSomething()     {         $service = new SomeService(             $this->createMock(LoggerInterface::class),         );                  $service->doSomething();     } } 

15. Композиция, а не наследование

Наследование это максимально сильная связь между классами.
Композиция в подавляющем большинстве случаев предпочтительнее.

Плохо:

final class SomeService extends BaseLogic { } 

Хорошо:

final class SomeService {     public function __construct(private readonly BaseLogic $logic) {}     public function execute()     {         $this->logic->doSomething();     } } 

16. По умолчанию указываем final для классов

Такое правило позволяет избежать проблем с наследованием и поощряет использование композиции.

final class SomeService { } 

17. Не работаем с ассоциативными массивами, только с объектами

Приходится писать много проверок, сложно расширять. Используем JMS\Serializer\SerializerInterface и
JMS\Serializer\ArrayTransformerInterface.

Плохо:

$id = $data['id']; 

Хорошо:

$id = $data->getId(); 

18. Соблюдаем закон Деметры

Класс должен взаимодействовать только с известными ему модулями, не взаимодействовать с незнакомцами.

Плохо:

$client = $this->clientFactory->create(); $client->send(); 

Хорошо:

$this->clientDecorator->send(); 

19. Минимальная цикломатическая сложность

Код должен быть максимально плоским. Чем меньше сложность, тем проще читать код и понимать, что он делает.
Иногда можно жертвовать микрооптимизациями ради читаемости.

Плохо:

foreach ($items as $item) {     if ($item->isAvailable()) {         $availableItems[] = $item;     }     if ($item->isAcvite()) {         $activeItems[] = $item;     } } 

Хорошо:

$availableItems = $this->filterAvailableItems($items); $activeItems = $this->filterActiveItems($items); 


ссылка на оригинал статьи https://habr.com/ru/articles/814995/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *