❌ Поговорим про код ✔
Александр Макаров
PHP Russia, Yii framework
Кто я такой? 🤠
Писать код легко! 👌
Особенно на PHP! 🤘
Ведь так? 😟
😭
План 📃
❌ Что ломает наш код? ☠
😍 Композиция и наследование 🤨
😍 Композиция или наследование 🤕
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;
}
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());
}
}
abstract Notifier — наполняем сообщение, отсылаем его
final EmailNotifier.
Проблемы?
Notifier завязан на конкретные реализации.
Новые требования:
Попробуем без наследования? 😎
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
interface Message
{
public function getSubject(): string;
public function getText(): string;
}
final class Address
{
public function getEmail(): string { … }
public function getPhone(): string { … }
}
interface Message
{
public function getSubject(): string;
public function getText(): string;
}
final class Address
{
public function getEmail(): string { … }
public function getPhone(): string { … }
}
interface Sender
{
public function send(Message $message, Address $to): void;
}
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);
То есть… наследование — зло?
Состояние🤯
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);
}
}
$query = (new Query())
->select()
->from('post');
$dataProvider = new DataProvider($query);
var_dump($dataProvider->one());
var_dump($dataProvider->all());
interface QueryInterface
{
public function select(string $columns): self;
public function from(string $table): self;
public function limit(int $limit): self;
}
🤯
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function one()
{
$limitedQuery = $this->query->limit(1);
return $this->connection->findOne($limitedQuery);
}
Как исправить?
public function limit(int $limit): self
{
$new = clone $this;
$new->limit = $limit;
return $new;
}
public function limit(int $limit): self
{
$new = clone $this;
$new->limit = $limit;
return $new;
}
Состояние
Зависимости и их инъекция 🤓
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
Можно сделать неправильно?
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);
}
}
public function __construct(ContainerInterface $container) {
// ...
}
public function send(Message $message, Address $to): void
{
$sender = $this->container->get(Sender::class);
Dependency Injection
А можно ещё хуже?
final class Notifier
{
public function send(Message $message, Address $to): void
{
$sender = App::$container->get(Sender::class);
$sender->send($message, $to);
}
}
final class Notifier
{
public function send(Message $message, Address $to): void
{
$sender = App::$container->get(Sender::class);
$sender->send($message, $to);
}
}
Методы 😎
Структура метода
Пред-проверки
Исправление плохих значений
Возможные ошибки
Основной путь
Пост-проверки
Как понять, что метод хороший?
Цикломатическая сложность
Число независимых путей исполнения в графе.
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;
}
V(G) = E - N + 2 = 7 - 6 + 2 = 3
V(G) = P + 1 = 2 + 1 = 3
Держите цикломатическую сложность низкой.
Делайте return/throw сразу
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.');
}
}
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();
}
function login(array $data)
{
$this->assertUsernameAndPasswordPresent($data);
$user = $this->findUser($data['username']);
$this->assertUserFound($user);
$this->assertPasswordValid($user, $data['password']);
$this->loginUser();
$this->refresh();
}
Булевы флаги
Если у метода есть флаг:
Области видимости
Private по умолчанию.
🔞 Исключения 🚳
Исключения
Сообщения в Exception
Дружественные исключения
Типы объектов 🦆 🦜 🦉
Сервисы и не сервисы
Доменные объекты
DTO
Сервисы 🛠
Сервисы / Компоненты
Правила для сервиса
Неумирающие серверы на PHP 🚀
Как они работают
// initialization
while ($request = $psr7->acceptRequest()) {
$response = $application->handle($request);
$psr7->respond($response);
gc_collect_cycles();
}
Правила
🪓🔨Тесты ⛏🏹
Тесты?
Как писать тесты
Принципы 📙
Нужна ли иммутабельность?
Как мутировать объект?
Чтение и запись
Command-query separation
Query-метод
class ShoppingCart
{
public function getTotal(): int {
}
}
Правила запросов
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;
}
}
Правила для команд
Модели
Read/write-модели
🥞 Слои и абстракция 🍔
Не переабстрагируй!
КОНЕЦ 🎬
Это, конечно, не всё 👀
Учиться, учиться и учиться! 💪
Думайте! 💡
Бонус: DRY
❓ Жду вопросов!