1 of 106

Поговорим про код

Александр Макаров

PHP Russia, Yii framework

2 of 106

Кто я такой? 🤠

3 of 106

Писать код легко! 👌

4 of 106

Особенно на PHP! 🤘

5 of 106

Ведь так? 😟

6 of 106

😭

7 of 106

План 📃

8 of 106

  1. Композиция и наследование.
  2. Состояние.
  3. Зависимости и их внедрение.
  4. Методы.
  5. Исключения.
  6. Типы объектов.
  7. Сервисы и долгоживущие серверы на PHP.
  8. Тесты.
  9. Принципы.
  10. Слои и абстракция.

9 of 106

Что ломает наш код?

10 of 106

  • Наследование.
  • Coupling (связанность).
  • Состояние.
  • Допущения.
  • Тесты (их отсутствие).
  • Архитектура.
  • ...

11 of 106

😍 Композиция и наследование 🤨

12 of 106

😍 Композиция или наследование 🤕

13 of 106

abstract class Notifier

{

private string $template;

private array $parameters;

public function __construct(string $template, array $parameters = [])

{

// ...

}

protected function renderTemplate(): string

{

// ...

}

abstract public function send(): void;

}

14 of 106

final class EmailNotifier extends Notifier

{

private string $to;

private string $subject;

public function __construct(string $to, string $subject, string $template, array $parameters = [])

{

// ...

}

public function send(): void

{

mail($this->to, $this->subject, $this->renderTemplate());

}

}

15 of 106

abstract Notifier — наполняем сообщение, отсылаем его

final EmailNotifier.

    • final SmsNotifier.
    • final DebugNotifier.

Проблемы?

Notifier завязан на конкретные реализации.

Новые требования:

    • Sms notifier и Email notifier должны логировать.
    • Sms notifier и Email notifier не должны позволять отправить дважды.
    • Отравлять в несколько каналов с fallback.
    • Не отправлять почту первого апреля.
    • 😱

16 of 106

Попробуем без наследования? 😎

17 of 106

final class Notifier

{

private Sender $sender;

public function __construct(Sender $sender) {

// ...

}

public function send(Message $message, Address $to): void

{

$this->sender->send($message, $to);

}

}

18 of 106

final class Notifier

{

private Sender $sender;

public function __construct(Sender $sender) {

// ...

}

public function send(Message $message, Address $to): void

{

$this->sender->send($message, $to);

}

}

19 of 106

interface Message

{

public function getSubject(): string;

public function getText(): string;

}

final class Address

{

public function getEmail(): string { … }

public function getPhone(): string { … }

}

20 of 106

interface Message

{

public function getSubject(): string;

public function getText(): string;

}

final class Address

{

public function getEmail(): string { … }

public function getPhone(): string { … }

}

21 of 106

interface Sender

{

public function send(Message $message, Address $to): void;

}

22 of 106

class EmailSender implements Sender

class SmsSender implements Sender

class Logger implements Sender

class DuplicatePreventer implements Sender

$sender = new DuplicatePreventer(new Logger(new SmsSender()));

$notifier = new Notifier($sender);

$notifier->send($message, $to);

23 of 106

То есть… наследование — зло?

  • Да. Попробуйте его избегать.
  • Используйте где подходит: исключения и другие иерархии.

24 of 106

Состояние🤯

25 of 106

final class DataProvider

{

public function __construct(Query $query, Connection $connection) { ... }

public function all()

{

return $this->connection->findAll($this->query);

}

public function one()

{

$limitedQuery = $this->query->limit(1);

return $this->connection->findOne($limitedQuery);

}

}

26 of 106

$query = (new Query())

->select()

->from('post');

$dataProvider = new DataProvider($query);

var_dump($dataProvider->one());

var_dump($dataProvider->all());

27 of 106

interface QueryInterface

{

public function select(string $columns): self;

public function from(string $table): self;

public function limit(int $limit): self;

}

28 of 106

🤯

29 of 106

30 of 106

public function limit(int $limit): self

{

$this->limit = $limit;

return $this;

}

31 of 106

public function one()

{

$limitedQuery = $this->query->limit(1);

return $this->connection->findOne($limitedQuery);

}

32 of 106

Как исправить?

33 of 106

public function limit(int $limit): self

{

$new = clone $this;

$new->limit = $limit;

return $new;

}

34 of 106

public function limit(int $limit): self

{

$new = clone $this;

$new->limit = $limit;

return $new;

}

35 of 106

Состояние

  • Избегайте когда возможно.
  • Инкапсулируйте.
  • Не используйте с fluent-интерфейсами.
  • Fluent-интерфейс = immutability.

36 of 106

Зависимости и их инъекция 🤓

37 of 106

final class Notifier

{

private Sender $sender;

public function __construct(Sender $sender) {

// ...

}

public function send(Message $message, Address $to): void

{

$this->sender->send($message, $to);

}

}

38 of 106

final class Notifier

{

private Sender $sender;

public function __construct(Sender $sender) {

// ...

}

public function send(Message $message, Address $to): void

{

$this->sender->send($message, $to);

}

}

39 of 106

Можно сделать неправильно?

40 of 106

final class Notifier

{

private ContainerInterface $container;

public function __construct(ContainerInterface $container) {

// ...

}

public function send(Message $message, Address $to): void

{

$sender = $this->container->get(Sender::class);

$sender->send($message, $to);

}

}

41 of 106

public function __construct(ContainerInterface $container) {

// ...

}

public function send(Message $message, Address $to): void

{

$sender = $this->container->get(Sender::class);

42 of 106

Dependency Injection

  • DI — это просто.
  • Можно и без контейнера.
  • Композиция лучше наследования.
  • Требуйте то, что нужно, а не то, откуда нужное можно получить.
  • Не таскайте за собой контейнер.

43 of 106

А можно ещё хуже?

44 of 106

45 of 106

final class Notifier

{

public function send(Message $message, Address $to): void

{

$sender = App::$container->get(Sender::class);

$sender->send($message, $to);

}

}

46 of 106

final class Notifier

{

public function send(Message $message, Address $to): void

{

$sender = App::$container->get(Sender::class);

$sender->send($message, $to);

}

}

47 of 106

Методы 😎

48 of 106

Структура метода

  1. Пред-проверки.
  2. Возможные ошибки.
  3. Основной путь.
  4. Пост-проверки.
  5. return.

49 of 106

Пред-проверки

  • Аргументы валидны?
  • Кидаем \InvalidArgumentException.
  • Не пытаемся исправить плохие значения.

50 of 106

Исправление плохих значений

  • Аутентификация через Facebook. Никнейм уже есть. Цепляем Facebook ID к аккаунту.
  • Можно вводить только буквы в никнейм. Ввели "samdark123". Фиксим, убирая все не буквы.

51 of 106

52 of 106

Возможные ошибки

  • Кидаем \RuntimeException.
  • Не пробуем ничего исправить.

53 of 106

Основной путь

  • То, что делает метод.

54 of 106

Пост-проверки

  • Проверяем, что всё сделано нормально.
  • Если нет — кидаем \RuntimeException.

55 of 106

Как понять, что метод хороший?

56 of 106

Цикломатическая сложность

Число независимых путей исполнения в графе.

57 of 106

58 of 106

function doIt(int $number) {

if ($number === 42) {

$result = calculateSpecialResult();

} else {

$result = calculateResult(42);

}

if ($result === null) {

throw new \RuntimeException('Unable to get result');

}

return $result;

}

59 of 106

V(G) = E - N + 2 = 7 - 6 + 2 = 3

V(G) = P + 1 = 2 + 1 = 3

  • V(G) — цикломатическая сложность.
  • E — рёбра.
  • N — узлы.
  • P — точки контроля:
    • if → 1.
    • составной if → количество условий.
    • итерация → 1.
    • switch → 1 на ветку.

Держите цикломатическую сложность низкой.

60 of 106

Делайте return/throw сразу

61 of 106

function login(array $data)

{

if (isset($data['username'], $data['password'])) {

$user = $this->findUser($data['username']);

if ($user !== null) {

if ($user->isPasswordValid($data['password'])) {

$this->loginUser();

$this->refresh();

} else {

throw new \InvalidArgumentException('Password is not valid.');

}

} else {

throw new \InvalidArgumentException('User not found.');

}

} else {

throw new \InvalidArgumentException('Both username and password are required.');

}

}

62 of 106

function login(array $data)

{

if (!isset($data['username'], $data['password'])) {

throw new \InvalidArgumentException('Both username and password are required.');

}

$user = $this->findUser($data['username']);

if ($user === null) {

throw new \InvalidArgumentException('User not found.');

}

if (!$user->isPasswordValid($data['password'])) {

throw new \InvalidArgumentException('Password is not valid.');

}

$this->loginUser();

$this->refresh();

}

63 of 106

function login(array $data)

{

$this->assertUsernameAndPasswordPresent($data);

$user = $this->findUser($data['username']);

$this->assertUserFound($user);

$this->assertPasswordValid($user, $data['password']);

$this->loginUser();

$this->refresh();

}

64 of 106

Булевы флаги

65 of 106

Если у метода есть флаг:

  1. Нарушен принцип единственной ответственности.
  2. Нужно разбить на несколько методов.

66 of 106

Области видимости

Private по умолчанию.

67 of 106

🔞 Исключения 🚳

68 of 106

Исключения

  • Два типа:
    • Можем обработать.
      • Свои, доменные.
    • Не должны обрабатывать.
      • Встроенные:
        • \RunitmeException.
        • \InvalidArgumentException.

69 of 106

Сообщения в Exception

  • Должны быть понятны.
  • Используйте нормальные английские предложения.

70 of 106

Дружественные исключения

71 of 106

Типы объектов 🦆 🦜 🦉

72 of 106

Сервисы и не сервисы

  • Сервисы что-то делают.
  • Не сервисы содержат данные и возвращают или меняют их.

73 of 106

Доменные объекты

  • Выделяйте объекты, чтобы меньше проверять: Money, Point, Email.
  • Составные значения — тоже объекты.
  • Без зависимостей. Только значения.
  • Статические конструкторы: fromString, fromInt и т.д.
  • Приватный конструктор для валидации.
  • Не раздувайте.
  • Иммутабельность.

74 of 106

DTO

  • Можно как угодно. Но лучше проще.
  • Public-свойства — норм :)
  • Собираем ошибки, не кидаем исключения.
  • Используем массовое присваивание.

75 of 106

Сервисы 🛠

76 of 106

Сервисы / Компоненты

  • Создаются один раз.
  • Обычно иммутабельные.
  • С нормальным DI.
  • Хранятся в DI контейнере и/или сервис-локаторе.

77 of 106

Правила для сервиса

  • Без состояния.
  • Без инъекции зависимостей через методы и свойства.
  • Без опциональных зависимостей.
  • Явные зависимости (хороший DI).
  • Данные для действий передаются в метод действия.
  • Лёгкие конструкторы. Откладываем инициализацию.
  • Не делаем допущений. Кидаем исключения.

78 of 106

Неумирающие серверы на PHP 🚀

79 of 106

Как они работают

// initialization

while ($request = $psr7->acceptRequest()) {

$response = $application->handle($request);

$psr7->respond($response);

gc_collect_cycles();

}

80 of 106

Правила

  • Сервисы без состояния или с его сбросом.
  • Чистим память.
  • Убиваем то, что не используется.

81 of 106

🪓🔨Тесты 🏹

82 of 106

Тесты?

  • Что тестить? Сложное! Домен!
  • Как сделать норм? Язык бизнеса → DDD.
  • Без clean code можно забыть об unit (моках).

83 of 106

Как писать тесты

  • Arrange, act, assert.
  • Без зависимостей.
  • Один тест — один AAA.
  • Только black box:
    • Публичный API.
    • Руки прочь от внутренностей!
    • Не лезем во внутреннее состояние!
    • Не тестим методы, тестим поведение.

84 of 106

Принципы 📙

85 of 106

Нужна ли иммутабельность?

  • Service = да (если возможно).
  • Entity = нет.
  • Другие объекты = да.
  • Fluent-интерфейс = да (в мутабельных объектах это плохо).

86 of 106

Как мутировать объект?

  • DTO:
    • Напрямую.
  • Value object'ы:
    • Иммутабельная цепочка вызовов.
  • Entity:
    • Записывать изменения как события.
  • Другие объекты:
    • Иммутабельная цепочка вызовов.

87 of 106

Чтение и запись

88 of 106

Command-query separation

  • Делать сразу и command и query — непонятно.
  • В некоторых случаях без этого никак.

89 of 106

Query-метод

class ShoppingCart

{

public function getTotal(): int {

}

}

90 of 106

Правила запросов

  • Возвращаем один тип.
  • Не возвращаем внутреннее состояние.
  • Конкретные методы. Слишком гибкие — плохо.
  • Используйте абстракцию для запросов, пересекающих границы системы.
  • Не используйте command в query.

91 of 106

Command-метод

class ShoppingCart

{

public function addItem(Item $item): void {

if ($this->getTotal() + $item->cost > self::MAX_TOTAL) {

throw new MaxTotalReached('Leave something for others!');

}

$this->items[] = $item;

}

}

92 of 106

Правила для команд

  • Именуем в императивной форме.
  • Ограничиваем scope, откладываем через события и очереди.
  • Кидаем исключения.
  • Абстрагируем при пересечении границы системы.
  • Используем запросы для получения данных, команды для их обработки.

93 of 106

Модели

94 of 106

Read/write-модели

  • Похожи на query / command.
  • Ищем в Elastic / пишем в MySQL.
  • Read-модели должны быть специфичны под кейс.
  • Read-модели должны быть близко к источнику данных.

95 of 106

🥞 Слои и абстракция 🍔

96 of 106

97 of 106

98 of 106

99 of 106

Не переабстрагируй!

  • Некоторые объекты должны быть конкретными:
    • Контроллеры.
    • Сервисы приложения.
    • Сущности.
    • Value object'ы.

100 of 106

КОНЕЦ 🎬

101 of 106

  • Композиция и наследование.
  • Состояние.
  • Зависимости и их внедрение.
  • Методы.
  • Исключения.
  • Типы объектов.
  • Сервисы и долгоживущие серверы на PHP.
  • Тесты.
  • Принципы.
  • Слои и абстракция.

102 of 106

Это, конечно, не всё 👀

103 of 106

Учиться, учиться и учиться! 💪

104 of 106

Думайте! 💡

105 of 106

Бонус: DRY

  • Работает безупречно только в границах модуля.

106 of 106

Жду вопросов!