Корректное проектирование методов для описания поведения объекта. Инкапсуляция для контроля внутреннего состояния.
Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Чтобы не пропускать новые эпизоды подпишитесь на наш канал @deworkerpro в Telegram
Комментарии (57)
Alex
Дмитрий, здравствуйте. Давно смотрю ваши скринкасты и проч. Сейчас оформил подписку чисто из благодарности.Хотя вижу, и здесь много полезного( как всегда). Если уместно сейчас выражать пожелания, - можно сделать урок о проектировании баз данных, хотя бы несложных. Я думаю это всем надо, а нормального материала,что бы быстро можно было усвоить, нет.
Алексей
А, кстати, да )
S.Polessky
+.
А
Привет, подписку оформил, надеюсь будет мотивация продолжать свое дело. Хотелось бы записи о solid kiss dry
S.Polessky
В "Интенсив по ООП" есть. Но соглашусь, что обновленный материал не помешает, т.к. интенсив в олд-скульном формате на 30 часов хронометража :)
gfdgdf
+
Sergei
15:52 А разве не проще иметь предрасчитанную дату till_actual_date и сравнивать её? В плане быстродействия и дальнейших манипуляций.
Дмитрий Елисеев
Можно. Но надо будет её каждый раз обновлять при смене статуса.
Валентин
Дмитрий, возможно этот вопрос уже был, но что у вас за тема в phpstorm?
Дмитрий Елисеев
Настраиваю сам.
xfg
Не согласен, что геттеры не нужны. В большинстве систем объекты будут передаваться в методы других объектов, а методам в свою очередь требуется доступ к состоянию такого объекта, чтобы выполнить свою некоторую бизнес-логику.
Дмитрий Елисеев
Если программировать бизнес-логику проверки снаружи объекта:
if ($post->getStatus() === Status::PUBLISHED && $post->getPublishDate() >= $date) {
throw new DomainException('Post is not published yet.');
}
то прямые геттеры get нужны. Состояние приходится доставать наружу. И любое изменение полей внутри объекта может привести к слому таких проверок.
Доступ к этим полям можно замаскировать в методы is, привязанные к этим полям:
if ($post->isStatusPublished() && $post->isPublishDateGreaterThan($date)) {
throw new DomainException('Post is not published yet.');
}
Но от этого ситуация не очень меняется, так как мы всё равно продолжаем снаружи работать напрямую с полями status и publisDate по отдельности.
Но вместо этого можно инкапсулировать поведение и эту же бизнес-логику сокрыть внутрь именно "настоящего" метода:
if ($post->isPublishedForDate($date)) {
throw new DomainException('Post is not published yet.');
}
Тогда прямые отдельные методы get и is для поштучного извлечения или оценки значений нам уже не пригодятся. И мы здесь можем как угодно изменять имена и типы полей и внутренности метода isPublishedToDate(), не ломая внешний код.
Так что вместо того, чтобы поштучно доставать состояние прямыми геттерами можно переписать код так, чтобы этого не делать. Чтобы каждый объект работал со своими приватными полями. И чтобы внешние объекты вместо геттеров вызывали у внутренних реальные методы с их бизнес-логикой.
xfg
Спасибо, теперь я понял вашу идею.
Александр Панков
Здравствуйте, Дмитрий.
А подскажите пожалуйста почему на 21:26 в методе-запросе
$advert->getDate(){} : DateTimeImmutable
возвращается объект именно типа DateTimeImmutable, а не DateTime.
В чем преимущества и когда нужно возвращать иммутабельный объект, а когда нет, какие есть кейсы?
Я ни разу еще не видел ни в одной из библиотек такого подхода, везде DateTime выплевывают, иногда обернутую в Carbon.
Дмитрий Елисеев
Если рассматривать мутабельные объекты-значения, то если кто-то напишет такой код:
$date = new DateTime();
$advert->setStart($date);
$date->modify('+1 hour');
$advert->setEnd($date);
то после сохранения обнаружит, что в start и end запишется одинаковая дата.
Это потому, что когда мы работаем с объектами:
$a = new A(12);
echo $a->value; // 12
в переменной находится указатель на объект. И при присваивании его в другие переменные:
$b = $a;
$c = $a;
echo $с->value; // 12
копируется не сам объект, а указатель на него. И во всех переменных будут указатели на один и тот же объект. В итоге если мы поменяем значение в c у нас поменяется значение и в a:
$c->value = 5;
echo $a->value; // 5
Что логично, ведь все три переменные ссылаются один и тот же объект, а не на три разных.
Так что если нам нужны две отдельные даты, то чтобы не было проблем нам нужно сначала склонировать первую дату и уже менять этот клон:
забыв склонировать оригинал. И этим он через геттер изменит нашу дату внутри advert.
С этим нужно быть осторожным. Поэтому чтобы не было таких неожиданностей можно придумать иммутабельную версию даты, которая вместо изменения себя будет по умолчанию клонировать и возвращать новый изменённый объект:
$start = new DateTimeImmutable();
$advert->setStart($start);
$end = $start->modify('+1 hour'); // создаёт клон с другой датой
$advert->setEnd($end);
В таком случае мы не будем переживать о корректности принимаемых и возвращаемых значений. Мы будем уверены, что никакие чужие вызовы modify ничего у нас не собьют.
Поэтому популярна практика все объекты-значения вроде Id, Email, Phone делать иммутабельными одноразовыми. Вместо изменения внутреннего значения их просто целиком заменяют на новый объект.
Александр Панков
Все понятно, спасибо!
Руслан
Спасибо за развернутый ответ, Дмитрий.
Владимир
Дмитрий, здравствуйте!
У меня возник вопрос по уроку.
9:34.
Пример из урока.
class Advert {
public function findPhoto(int $id): ?Photo
{
if (!$this->photos->has($id)) {
return null;
}
return $this->photos->get($id);
}
}
Интересует в какой момент времени создается $this->photos.
На сколько я понимаю, это репозиторий наших фатографий. В конструктор мы его не прокидываем и я предполагаю, что он создается в конструкторе что-то вроде:
public function __construct(int $id, \DateTimeImmutable $date, int $userId, int $categoryId, string $content)
{
$this->id = $id;
$this->date = $date;
$this->userId = $userId;
$this->userId = $userId;
$this->categoryId = $categoryId;
$this->content = $content;
// вот тут
$this->photos = new PhotoRepository();
$this->tags = new TagRepository();
}
Если да, то на сколько это правильно?
На сколько не правильно их инжектить в конструктор? Избыточно?
Хотелось бы внести ясности в этот момент. Заранее благодарен за ответ!
Дмитрий Елисеев
Репозиторий нужен только для самого агрегата. Будет только AdvertRepository, который умеет сохранять Advert целиком с фотографиями. Фотографии здесь мы не используем самостоятельно отдельно от объявлений, поэтому отдельный репозиторий для фотографий нам здесь не нужен.
А в поле $photos будет простой массив или коллекция, создаваемая в конструкторе:
public function __construct(int $id, string $content)
{
$this->id = $id;
$this->content = $content;
$this->photos = new ArrayCollection();
}
В случае Doctrine ORM это может быть её коллекция ArrayCollection для связей.
Владимир
Понял. Благодарю за ответ.
Константин
Роберт Мартин в своей книге "Чистый код" пишет, что методы не должны возвращать null. Вы с ним не согласны?
Дмитрий Елисеев
На счёт возврата null это часто зависит от языка программирования.
В PHP и Kotlin есть возможность через nullable-тип вроде ?User явно указать, что метод вернёт объект юзера или null:
class Repo
{
public function a(): User
public function b(): ?User
public function c(): User
}
Поэтому можно спокойно возвращать null, и статические анализаторы и IDE подскажут, если мы забудем выше проверить на if (x === null). Там проблем с этим нет.
А в Java, наоборот, такой возможности нет:
class Repo
{
public User a()
public User b()
public User c()
}
Там если указан тип User туда можно всё равно вернуть или присвоить null. И анализатор и компилятор это проследить не могут. В итоге если из метода с типом User вернуть null, то код выше свалится с NullPointerException.
Поэтому в Java есть общепринятая договорённость никогда не возвращать null. Вместо этого принято кидать исключения, возвращать объект Null Object или оборачивать необязательный результат в объект Optional:
class Repo
{
public User a() throws NotFoundException
public Optional<User> b()
public User c() throws NotFoundException
}
А если брать функциональное программирование, то там активно используют монады вроде Either.
Константин
Спасибо за развернутый ответ, Дмитрий!
slo_nik
Добрый день, Дмитрий.
В php v.8.1 можно объявить public свойство как readonly.
Избавляет ли это от необходимости помечать свойство как private и создавать методы get для чтения этих свойств?
p.s. На 4:30 минуте об этом говорите.
Дмитрий Елисеев
Да, как раз избавляет написания громоздких геттеров, которые делали для защиты полей от записи.
slo_nik
Получается , что можно значительно сократить код, оставив только методы is(), has() и т.д.?
И при обновлении entity не будет проблем с записью новых значений?
Или всё-таки есть моменты, которые надо учесть?
Дмитрий Елисеев
Получается , что можно значительно сократить код, оставив только методы is(), has() и т.д.?
Верно. Теперь для неизменяемых структур данных такую кучу кода:
class SignUpCommand
{
private string $email;
private string $password;
public function __construct(string $email, string $password)
{
$this->email = $email;
$this->password = $password;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): string
{
return $this->password;
}
}
в современном PHP можно сократить до:
class SignUpCommand
{
public function __construct(
public readonly string $email,
public readonly string $password
) {}
}
и заполнять по именам:
$command = new SignUpCommand(
email: $request->post('email'),
password: $request->post('password')
);
И при обновлении entity не будет проблем с записью новых значений?
В readonly как и в класс без сеттеров не присваиваются новые значения.
Или всё-таки есть моменты, которые надо учесть?
Если класс реализует интерфейс, то придётся оставить методы для этого интерфейса.
И если захочется добавить логику, то придётся переписать обратно на private readonly + геттер.
Пока в PHP не добавили возможность создавать вычисляемые свойства с get {} и указывать свойства у интерфейсов как предложили в RFC.
slo_nik
И если захочется добавить логику, то придётся переписать обратно на private readonly + геттер.
Что-то типа проверки isEqual(), attachNetwork() и т.п.?
Дмитрий Елисеев
Да, так.
slo_nik
А как быть если email является собственным типом Email()?
$command = new SignUpCommand(
email: new Email($request->post('email')),
password: $request->post('password')
);
И по поводу перечислений.
Раньше Вы учили, что можно status вынести в отдельный класс
class Status
{
public const ACTIVE = 'active';
public const Waiting = 'waiting';
................................................
public const CLOSED = 'closed';
public function __construct(string $status)
{
Assert::oneOf($status, [
// перечисление констант
])
}
public static function active(): self
{
return new self(self::ACTIVE);
}
...
}
Теперь, как я понял можно частично избавиться от подобного кода перечислением
enum Status: string
{
case ACTIVE = 'active';
case Waiting = 'waiting';
...
case CLOSED = 'closed';
}
И присваивать через $status->value
$command = new SignUpCommand(
email: $request->post('email'),
status: $status->value
);
Но так не очень понятно, какой статус просвоен. Можно ли как-то в перечислениях использовать статические методы типа Status::active(); ?
Дмитрий Елисеев
А как быть если email является собственным типом Email?
Да, так. Но у нас в командах все поля простые без таких типов.
Дмитрий Елисеев
Можно ли как-то в перечислениях использовать статические методы?
В перечислениях обычно используются сами case как Status::ACTIVE. А так да, в enum можно добавлять статические методы.
slo_nik
Благодарю.
p.s. А когда ожидать выход видео о брокере очередей Rabbitmq?
slo_nik
Добрый вечер.
Снова накопилось немного вопросов о public/private readonly для embedded классов.
Есть некий класс File, для этого файла нужны настройки. Настройки я вынес в embedded класс Settings, все свойства public readonly
#[ORM\Embeddable]
class Settings
{
private const UPD_PERIOD = 'none';
#[ORM\Column(type: 'string', nullable: true)]
public readonly ?string $actionNoProducts;
#[ORM\Column(type: 'string', nullable: true)]
public readonly ?string $actionDroppedProducts;
public function __construct(
#[ORM\Column(type: 'json', nullable: true)]
public readonly array $updateFields,
#[ORM\Column(type: 'string')]
public readonly string $updatePeriod = self::UPD_PERIOD,
) {
}
public static function noPeriod(): string
{
return self::UPD_PERIOD;
}
}
В классе File settigns используются так
class File
{
#[ORM\Embedded(Settings::class, columnPrefix: false)]
public Settings $settings;
public function attachSettings(array $fields, string $period): void
{
$this->settings = new Settings(fields: $fields, period: $period);
}
}
В File я не могу назначить $settings readonly. Если не присваивать $settings private readonly и не создавать геттер, какие могут возникнуть проблемы? Даже если попробовать присвоить $file->settings что-то отличное от new Settings(...) всё-равно возникнет ошибка.
Или я ошибаюсь и правильней будет в File классе сделать private readonly для $settings?
И как поступить со свойствами в классе Settings
#[ORM\Column(type: 'string', nullable: true)]
public readonly ?string $actionNoProducts;
#[ORM\Column(type: 'string', nullable: true)]
public readonly ?string $actionDroppedProducts;
Дописать их в конструктор класса и присвоить значение по умолчанию? Как правильно будет их оформить, если понадобиться записать в них значение?
Дмитрий Елисеев
Если не присваивать $settings private readonly и не создавать геттер, какие могут возникнуть проблемы? Даже если попробовать присвоить $file->settings что-то отличное от new Settings(...) всё-равно возникнет ошибка.
Сейчас метод attachSettings не делает ничего кроме присваивания:
public $settings;
public function attachSettings($fields, $period)
{
$this->settings = new Settings($fields, $period);
}
поэтому пока проблем нет. Но если в него добавится какой-то другой код вроде:
public function attachSettings($fields, $period)
{
$this->settings = new Settings(
$fields,
max(self::MIN_PERIOD, $period)
);
}
то сразу будет проблема, если что-то присвоят прямо в поле в обход этого метода.
Дмитрий Елисеев
И как поступить со свойствами в классе Settings? Дописать их в конструктор класса и присвоить значение по умолчанию? Как правильно будет их оформить, если понадобиться записать в них значение?
В readonly поля всегда присваиваю значение в конструкторе. Иначе чтение этих полей без присвоенного значения упадёт с ошибкой PHP. Анализатор Psalm за этим строго следит.
slo_nik
А насколько плох такой подход?
В классе File свойство settings будет private, а в самом классе Settings свойства public readonly, чтобы не создавать геттеры в классе Settings.
Например так
class File
{
private Settings $settings;
public function getSettings(): Settings
{
return $this->settings;
}
}
class Settigns
{
public function __construct(
public readonly string $fields
){}
}
И потом получить значение $fields таким образом
echo $file->getSettings()->fields;
Дмитрий Елисеев
Да, можно так. Здесь никто ничего бесконтрольно не присвоит.
slo_nik
Благодарю.
Ещё такой вопрос.
В entity куча private свойств. Не хотелось бы загромождать сущность кучей геттеров. Получать данные планирую через fetcher-ы. Так что геттеры особо не нужны, разве что для uuid.
В phpstorm-е эти свойства подсвечиваются как "Property is only written but never read
". Если отключить эту инспекцию, то какие можно получить проблемы?
И как потом можно протестировать эту сущность unit тестами?
Дмитрий Елисеев
Если отключить эту инспекцию, то какие можно получить проблемы?
Проблем никаких. Просто после изменений могут где-то остаться старые поля. Но это на работу класса не повлияет.
И как потом можно протестировать эту сущность unit тестами?
Здесь делают по-разному:
Либо проверяют поля рефлексией, что делает тесты хрупкими.
Либо если сущность порождает событие, то проверяют поля этого события.
Либо в языках без рефлексии в класс добавляют вспомогательный метод для тестов вроде toArray() или isNameEqual($name).
Либо тестируют только методы вроде isActive(), которые остались.
slo_nik
что делает тесты хрупкими.
Что значит хрупкими?
Дмитрий Елисеев
Хрупкие тесты - это которые ломаются при малейшем изменении внутренностей тестируемого класса.
Например, если раньше активность была булевым полем:
class A
{
private bool $active;
public function isActive(): bool {
return $this->active;
}
}
а потом мы решили отрефакторить и поменять на число или enum:
class A
{
private int $status;
public function isActive(): bool {
return $this->status === STATUS_ACTIVE;
}
}
То тесты на проверку публичного метода isActive() продолжат работать с любым вариантом, а тесты с рефлексией на приватное поле $active будут ломаться при каждом рефакторинге.
slo_nik
Дмитрий, как Вы посоветуете поступить в данной ситуации?
В сущности есть свойства, которые содержат или bool или int. И таких свойств достаточно.
Сейчас они определены самым простым способом
private int $stok;
private bool $available
Как быть с дата-объектами для подобных свойств? Создать не проблема, но насколько это будет практично? Например для $stok, остаток в наличии, простое число. Или $available, true или false, как это будет выглядеть?
class Available
{
public function __construct(
public readonly bool $value;
){
}
// Далее теряюсь, как правильно оформить?
}
Есть такая мысль, что разбить кучу подобных свойств по смыслу и создать дата-объекты.
Например, всё что касается размеров, ширина, высота, длина.
Дмитрий Елисеев
Как быть с дата-объектами для подобных свойств? Создать не проблема, но насколько это будет практично?
Если это просто флаг, то смысла нет.
Если это неотрицательное число, то можно для всех сделать один класс с проверкой на $value >= 0. Это может иметь смысл.
slo_nik
Вы имеете ввиду что-то подобное?
class Integer
{
public function __construct(
private int $value;
){
// тут возможные проверки, что переданное значение число и что это
// значение больше 0
}
}
И использовать для всех свойств, которые принимают целое число.
Дмитрий Елисеев
Да, добавить проверку:
Assert::greaterThanEq($value, 0);
slo_nik
Если это неотрицательное число,
Вот тут некоторая проблема.
Для available в файле из которого парсятся товары, допускается строковое значение "true" или "false".
<offer id="123" availavle="true">
// остальные данные товара
</offer>
Есть другие параметры, где допускается подобное обозначение.
Получается, что это просто строка.
Проверять значение строки через константы в классе, какое именно значение пришло и просто писать эту строку. Или приводить к bool, например, или к "1"/"0" ?
Дмитрий Елисеев
Строки "true" и "false" переводить в bool. А если там либо это, либо число, то либо так и присваивать в одно поле:
public function __construct(
int|bool $value;
)
либо разнести на два поля:
public function __construct(
?int $amount;
?bool $available;
)
Это как больше нравится.
slo_nik
И ещё один вопрос.
Legacy проект на yii2, надо внедрить новый функционал, чем и занимаюсь. Пытаюсь писать код, который минимально привязан к yii2 и с использованием Doctrine. Но тут возникает проблема.
Например, в entity Products есть userId, companyId. Сущности User, Company пока не трогаю, то есть, они созданы под чистый yii2 с ActiveRecord и прочей шелухой.
Как можно их обозначить в сущности Product? Пока ограничился обычными int.
Можно ли в этом случае создать связь в doctrine на старые сущности и использовать data-object для User и Company?
Дмитрий Елисеев
Если выборки в будущем будете делать через отдельные фекчеры, то в Doctrine-сущностях связи на сущности можно не делать, оставив там просто эти int.
А если сейчас всё-таки нужны связи на объекты, то для Doctrine можно пока сделать классы-наследники от прошлых User и Company и туда вписать поля и связи Doctrine.
slo_nik
А если мне надо указать foreign key для yii2 entity не создавая класс-наследник?
сделать классы-наследники от прошлых User и Company и туда вписать поля и связи Doctrine.
То есть просто вот так?
#[ORM\Entity]
class Users extends User
{
.............
}
Дмитрий Елисеев
А если мне надо указать foreign key для yii2 entity не создавая класс-наследник?
Для Yii2 все связи будут в оригинальных старых классах. Для Doctrine всё будет пока в новых наследниках.
То есть просто вот так?
Да, можно попробовать так:
#[ORM\Entity]
class Doctrine\User extends ActiveRecord\User
{
#[ORM\Id]
public int $id;
#[ORM\Column]
public string $email;
}
slo_nik
Или можно делать так, как Вы описывали в этой статье?
Dinar
Добрый день. Есть проект на laravel. Над ним работают несколько программистов. Сам фреймворк классный, если аккуратно работать. Но не нравятся модели eloquent(Active Record). Можно ли их как-то безболезненно привести к корректному виду? Т.е. закрыть все атрибуты и т.д. Можно конечно переопределять магические методы get и set. Есть ли с этим какие-нибудь подводные камни? Стоит ли в это лезть, или оставить как есть, и смириться? Т.е. не хотелось бы переписывать пол фреймворка ради этого)
Dinar
Также есть идея вынести Eloquent в инфраструктуру и написать свой mapper. Туда же вынести реализацию интерфейса репозитория(EloquentRepositrory). Что думаете насчет этого?
Дмитрий Елисеев
Закрыть атрибуты не очень получится. Там лучше программистам договориться не присваивать поля напрямую, а всегда вызывать бизнес-методы.
Если сильно заморачиваться, то в проектах на Yii я иногда делал бизнес-сущности из его ActiveRecord так. Может что-то подойдёт для Laravel.
Евгений
Как быть теперь с тем, что Advert получился монструозный, и это противоречит первому принципу SOLID - SRP?, о котором вы очень классно объяснили на стриме про Solid.
Дмитрий, здравствуйте. Давно смотрю ваши скринкасты и проч. Сейчас оформил подписку чисто из благодарности.Хотя вижу, и здесь много полезного( как всегда). Если уместно сейчас выражать пожелания, - можно сделать урок о проектировании баз данных, хотя бы несложных. Я думаю это всем надо, а нормального материала,что бы быстро можно было усвоить, нет.
А, кстати, да )
+.
Привет, подписку оформил, надеюсь будет мотивация продолжать свое дело. Хотелось бы записи о solid kiss dry
В "Интенсив по ООП" есть. Но соглашусь, что обновленный материал не помешает, т.к. интенсив в олд-скульном формате на 30 часов хронометража :)
+
15:52 А разве не проще иметь предрасчитанную дату till_actual_date и сравнивать её? В плане быстродействия и дальнейших манипуляций.
Можно. Но надо будет её каждый раз обновлять при смене статуса.
Дмитрий, возможно этот вопрос уже был, но что у вас за тема в phpstorm?
Настраиваю сам.
Не согласен, что геттеры не нужны. В большинстве систем объекты будут передаваться в методы других объектов, а методам в свою очередь требуется доступ к состоянию такого объекта, чтобы выполнить свою некоторую бизнес-логику.
Если программировать бизнес-логику проверки снаружи объекта:
то прямые геттеры get нужны. Состояние приходится доставать наружу. И любое изменение полей внутри объекта может привести к слому таких проверок.
Доступ к этим полям можно замаскировать в методы is, привязанные к этим полям:
Но от этого ситуация не очень меняется, так как мы всё равно продолжаем снаружи работать напрямую с полями
status
иpublisDate
по отдельности.Но вместо этого можно инкапсулировать поведение и эту же бизнес-логику сокрыть внутрь именно "настоящего" метода:
Тогда прямые отдельные методы get и is для поштучного извлечения или оценки значений нам уже не пригодятся. И мы здесь можем как угодно изменять имена и типы полей и внутренности метода
isPublishedToDate()
, не ломая внешний код.Так что вместо того, чтобы поштучно доставать состояние прямыми геттерами можно переписать код так, чтобы этого не делать. Чтобы каждый объект работал со своими приватными полями. И чтобы внешние объекты вместо геттеров вызывали у внутренних реальные методы с их бизнес-логикой.
Спасибо, теперь я понял вашу идею.
Здравствуйте, Дмитрий.
А подскажите пожалуйста почему на 21:26 в методе-запросе
возвращается объект именно типа DateTimeImmutable, а не DateTime.
В чем преимущества и когда нужно возвращать иммутабельный объект, а когда нет, какие есть кейсы?
Я ни разу еще не видел ни в одной из библиотек такого подхода, везде DateTime выплевывают, иногда обернутую в Carbon.
Если рассматривать мутабельные объекты-значения, то если кто-то напишет такой код:
то после сохранения обнаружит, что в
start
иend
запишется одинаковая дата.Это потому, что когда мы работаем с объектами:
в переменной находится указатель на объект. И при присваивании его в другие переменные:
копируется не сам объект, а указатель на него. И во всех переменных будут указатели на один и тот же объект. В итоге если мы поменяем значение в
c
у нас поменяется значение и вa
:Что логично, ведь все три переменные ссылаются один и тот же объект, а не на три разных.
Так что если нам нужны две отдельные даты, то чтобы не было проблем нам нужно сначала склонировать первую дату и уже менять этот клон:
Так и у нас вызов
modify
второй даты не изменит значение первой.Аналогично у нас есть опасность, что кто-то напишет так:
забыв склонировать оригинал. И этим он через геттер изменит нашу дату внутри
advert
.С этим нужно быть осторожным. Поэтому чтобы не было таких неожиданностей можно придумать иммутабельную версию даты, которая вместо изменения себя будет по умолчанию клонировать и возвращать новый изменённый объект:
В таком случае мы не будем переживать о корректности принимаемых и возвращаемых значений. Мы будем уверены, что никакие чужие вызовы
modify
ничего у нас не собьют.Поэтому популярна практика все объекты-значения вроде Id, Email, Phone делать иммутабельными одноразовыми. Вместо изменения внутреннего значения их просто целиком заменяют на новый объект.
Все понятно, спасибо!
Спасибо за развернутый ответ, Дмитрий.
Дмитрий, здравствуйте!
У меня возник вопрос по уроку.
9:34.
Пример из урока.
Интересует в какой момент времени создается
$this->photos
. На сколько я понимаю, это репозиторий наших фатографий. В конструктор мы его не прокидываем и я предполагаю, что он создается в конструкторе что-то вроде:Если да, то на сколько это правильно?
На сколько не правильно их инжектить в конструктор? Избыточно?
Хотелось бы внести ясности в этот момент. Заранее благодарен за ответ!
Репозиторий нужен только для самого агрегата. Будет только
AdvertRepository
, который умеет сохранятьAdvert
целиком с фотографиями. Фотографии здесь мы не используем самостоятельно отдельно от объявлений, поэтому отдельный репозиторий для фотографий нам здесь не нужен.А в поле
$photos
будет простой массив или коллекция, создаваемая в конструкторе:В случае Doctrine ORM это может быть её коллекция
ArrayCollection
для связей.Понял. Благодарю за ответ.
Роберт Мартин в своей книге "Чистый код" пишет, что методы не должны возвращать null. Вы с ним не согласны?
На счёт возврата
null
это часто зависит от языка программирования.В PHP и Kotlin есть возможность через nullable-тип вроде
?User
явно указать, что метод вернёт объект юзера илиnull
:Поэтому можно спокойно возвращать
null
, и статические анализаторы и IDE подскажут, если мы забудем выше проверить наif (x === null)
. Там проблем с этим нет.А в Java, наоборот, такой возможности нет:
Там если указан тип
User
туда можно всё равно вернуть или присвоитьnull
. И анализатор и компилятор это проследить не могут. В итоге если из метода с типомUser
вернутьnull
, то код выше свалится сNullPointerException
.Поэтому в Java есть общепринятая договорённость никогда не возвращать
null
. Вместо этого принято кидать исключения, возвращать объект Null Object или оборачивать необязательный результат в объект Optional:А если брать функциональное программирование, то там активно используют монады вроде Either.
Спасибо за развернутый ответ, Дмитрий!
Добрый день, Дмитрий.
В php v.8.1 можно объявить public свойство как readonly.
Избавляет ли это от необходимости помечать свойство как private и создавать методы get для чтения этих свойств?
p.s. На 4:30 минуте об этом говорите.
Да, как раз избавляет написания громоздких геттеров, которые делали для защиты полей от записи.
Получается , что можно значительно сократить код, оставив только методы is(), has() и т.д.?
И при обновлении entity не будет проблем с записью новых значений?
Или всё-таки есть моменты, которые надо учесть?
Верно. Теперь для неизменяемых структур данных такую кучу кода:
в современном PHP можно сократить до:
и заполнять по именам:
В readonly как и в класс без сеттеров не присваиваются новые значения.
Если класс реализует интерфейс, то придётся оставить методы для этого интерфейса.
И если захочется добавить логику, то придётся переписать обратно на
private readonly
+ геттер.Пока в PHP не добавили возможность создавать вычисляемые свойства с
get {}
и указывать свойства у интерфейсов как предложили в RFC.Что-то типа проверки isEqual(), attachNetwork() и т.п.?
Да, так.
А как быть если email является собственным типом Email()?
И по поводу перечислений.
Раньше Вы учили, что можно status вынести в отдельный класс
Теперь, как я понял можно частично избавиться от подобного кода перечислением
И присваивать через $status->value
Но так не очень понятно, какой статус просвоен. Можно ли как-то в перечислениях использовать статические методы типа Status::active(); ?
Да, так. Но у нас в командах все поля простые без таких типов.
В перечислениях обычно используются сами
case
какStatus::ACTIVE
. А так да, вenum
можно добавлять статические методы.Благодарю.
p.s. А когда ожидать выход видео о брокере очередей Rabbitmq?
Добрый вечер.
Снова накопилось немного вопросов о public/private readonly для embedded классов.
Есть некий класс File, для этого файла нужны настройки. Настройки я вынес в embedded класс Settings, все свойства public readonly
В классе File settigns используются так
В File я не могу назначить
$settings
readonly. Если не присваивать$settings
private readonly и не создавать геттер, какие могут возникнуть проблемы? Даже если попробовать присвоить$file->settings
что-то отличное отnew Settings(...)
всё-равно возникнет ошибка.Или я ошибаюсь и правильней будет в File классе сделать private readonly для $settings?
И как поступить со свойствами в классе Settings
Дописать их в конструктор класса и присвоить значение по умолчанию? Как правильно будет их оформить, если понадобиться записать в них значение?
Сейчас метод
attachSettings
не делает ничего кроме присваивания:поэтому пока проблем нет. Но если в него добавится какой-то другой код вроде:
то сразу будет проблема, если что-то присвоят прямо в поле в обход этого метода.
В readonly поля всегда присваиваю значение в конструкторе. Иначе чтение этих полей без присвоенного значения упадёт с ошибкой PHP. Анализатор Psalm за этим строго следит.
А насколько плох такой подход? В классе File свойство settings будет private, а в самом классе Settings свойства public readonly, чтобы не создавать геттеры в классе Settings.
Например так
И потом получить значение $fields таким образом
Да, можно так. Здесь никто ничего бесконтрольно не присвоит.
Благодарю. Ещё такой вопрос. В entity куча private свойств. Не хотелось бы загромождать сущность кучей геттеров. Получать данные планирую через fetcher-ы. Так что геттеры особо не нужны, разве что для uuid. В phpstorm-е эти свойства подсвечиваются как "Property is only written but never read ". Если отключить эту инспекцию, то какие можно получить проблемы? И как потом можно протестировать эту сущность unit тестами?
Проблем никаких. Просто после изменений могут где-то остаться старые поля. Но это на работу класса не повлияет.
Здесь делают по-разному:
toArray()
илиisNameEqual($name)
.isActive()
, которые остались.Что значит хрупкими?
Хрупкие тесты - это которые ломаются при малейшем изменении внутренностей тестируемого класса.
Например, если раньше активность была булевым полем:
а потом мы решили отрефакторить и поменять на число или enum:
То тесты на проверку публичного метода
isActive()
продолжат работать с любым вариантом, а тесты с рефлексией на приватное поле$active
будут ломаться при каждом рефакторинге.Дмитрий, как Вы посоветуете поступить в данной ситуации?
В сущности есть свойства, которые содержат или bool или int. И таких свойств достаточно. Сейчас они определены самым простым способом
Как быть с дата-объектами для подобных свойств? Создать не проблема, но насколько это будет практично? Например для $stok, остаток в наличии, простое число. Или $available, true или false, как это будет выглядеть?
Есть такая мысль, что разбить кучу подобных свойств по смыслу и создать дата-объекты.
Например, всё что касается размеров, ширина, высота, длина.
Если это просто флаг, то смысла нет.
Если это неотрицательное число, то можно для всех сделать один класс с проверкой на
$value >= 0
. Это может иметь смысл.Вы имеете ввиду что-то подобное?
И использовать для всех свойств, которые принимают целое число.
Да, добавить проверку:
Вот тут некоторая проблема.
Для available в файле из которого парсятся товары, допускается строковое значение "true" или "false".
Есть другие параметры, где допускается подобное обозначение. Получается, что это просто строка.
Проверять значение строки через константы в классе, какое именно значение пришло и просто писать эту строку. Или приводить к bool, например, или к "1"/"0" ?
Строки "true" и "false" переводить в bool. А если там либо это, либо число, то либо так и присваивать в одно поле:
либо разнести на два поля:
Это как больше нравится.
И ещё один вопрос.
Legacy проект на yii2, надо внедрить новый функционал, чем и занимаюсь. Пытаюсь писать код, который минимально привязан к yii2 и с использованием Doctrine. Но тут возникает проблема. Например, в entity Products есть userId, companyId. Сущности User, Company пока не трогаю, то есть, они созданы под чистый yii2 с ActiveRecord и прочей шелухой. Как можно их обозначить в сущности Product? Пока ограничился обычными int. Можно ли в этом случае создать связь в doctrine на старые сущности и использовать data-object для User и Company?
Если выборки в будущем будете делать через отдельные фекчеры, то в Doctrine-сущностях связи на сущности можно не делать, оставив там просто эти int.
А если сейчас всё-таки нужны связи на объекты, то для Doctrine можно пока сделать классы-наследники от прошлых User и Company и туда вписать поля и связи Doctrine.
А если мне надо указать foreign key для yii2 entity не создавая класс-наследник?
То есть просто вот так?
Для Yii2 все связи будут в оригинальных старых классах. Для Doctrine всё будет пока в новых наследниках.
Да, можно попробовать так:
Или можно делать так, как Вы описывали в этой статье?
Добрый день. Есть проект на laravel. Над ним работают несколько программистов. Сам фреймворк классный, если аккуратно работать. Но не нравятся модели eloquent(Active Record). Можно ли их как-то безболезненно привести к корректному виду? Т.е. закрыть все атрибуты и т.д. Можно конечно переопределять магические методы get и set. Есть ли с этим какие-нибудь подводные камни? Стоит ли в это лезть, или оставить как есть, и смириться? Т.е. не хотелось бы переписывать пол фреймворка ради этого)
Также есть идея вынести Eloquent в инфраструктуру и написать свой mapper. Туда же вынести реализацию интерфейса репозитория(EloquentRepositrory). Что думаете насчет этого?
Закрыть атрибуты не очень получится. Там лучше программистам договориться не присваивать поля напрямую, а всегда вызывать бизнес-методы.
Если сильно заморачиваться, то в проектах на Yii я иногда делал бизнес-сущности из его ActiveRecord так. Может что-то подойдёт для Laravel.
Как быть теперь с тем, что Advert получился монструозный, и это противоречит первому принципу SOLID - SRP?, о котором вы очень классно объяснили на стриме про Solid.
Или войти через: