Проектирование методов объектов

Корректное проектирование методов для описания поведения объекта. Инкапсуляция для контроля внутреннего состояния.

Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (32)
Alex

Дмитрий, здравствуйте. Давно смотрю ваши скринкасты и проч. Сейчас оформил подписку чисто из благодарности.Хотя вижу, и здесь много полезного( как всегда). Если уместно сейчас выражать пожелания, - можно сделать урок о проектировании баз данных, хотя бы несложных. Я думаю это всем надо, а нормального материала,что бы быстро можно было усвоить, нет.

Ответить
Алексей

А, кстати, да )

Ответить
S.Polessky

+.

Ответить
Анатолий

Привет, подписку оформил, надеюсь будет мотивация продолжать свое дело. Хотелось бы записи о solid kiss dry

Ответить
S.Polessky

В "Интенсив по ООП" есть. Но соглашусь, что обновленный материал не помешает, т.к. интенсив в олд-скульном формате на 30 часов хронометража :)

Ответить
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

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

Так что если нам нужны две отдельные даты, то чтобы не было проблем нам нужно сначала склонировать первую дату и уже менять этот клон:

$start = new DateTime();
$advert->setStart($start);

$end = clone $start;

$end->modify('+1 hour');
$advert->setEnd($end);

Так и у нас вызов modify второй даты не изменит значение первой.

Аналогично у нас есть опасность, что кто-то напишет так:

$start = $advert->getDate()
$start->modify('+1 hour');

забыв склонировать оригинал. И этим он через геттер изменит нашу дату внутри 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?

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Google
GitHub
Yandex
MailRu