Статьи

Практический рефакторинг PHP: измените двунаправленную ассоциацию на однонаправленную

Этот рефакторинг является обратным изменением однонаправленной ассоциации на двунаправленную . Мы начинаем с двунаправленной ассоциации, где два объекта ссылаются друг на друга; ассоциация упрощена, чтобы понять только одно направление, удалив один из двух обратных указателей.

Почему я должен поменять ассоциацию?

Однонаправленную ассоциацию проще установить во время создания экземпляра и проще поддерживать : изменение объекта, участвующего в одной стороне ассоциации, или простое удаление экземпляра ассоциации проще, поскольку в нем используется один указатель (фактически называемый обработчиком в PHP).

Другая проблема, связанная с двунаправленностью, заключается в сборке мусора : обычно в PHP объекты, относящиеся к циклу, не удаляются, даже если они недоступны из какой-либо другой области. (У них обоих все еще есть потенциальная ссылка: другой объект). Таким образом, поддерживается двунаправленная ссылка. что занятая память никогда не будет освобождена до конца сценария.

Однако в PHP 5.3 был добавлен патч для обнаружения циклов и решения этой проблемы раз и навсегда. Таким образом, производительность не должна быть параметром для выбора однонаправленной или двунаправленной ассоциации.

Вместо этого всегда будет существовать проблема зависимости : два класса, участвующих в двунаправленных ассоциациях, обычно зависят друг от друга, что делает их неразделимыми. Класс A не будет инстанцируемым или завершенным с классом B, и наоборот: разделение их обязанностей в разных классах не очень помогает без независимости.

Цикл зависимости может быть нарушен введением интерфейса для реализации для одной из двух сторон:

Однако интерфейс — это дополнительная движущаяся часть, которая также имеет стоимость обслуживания Таким образом, я бы всегда предпочел однонаправленную ассоциацию идеально разделенной двунаправленной.

 

Когда я должен упростить?

По возможности: когда нет доступа к ссылке на поле с одной стороны, эта ссылка на поле больше не нужна и может быть удалена с помощью этого рефакторинга.

В особом случае ORM, для существования которого требуется определенная сторона, вы, возможно, обнаружили конкретную конфигурацию, допускающую однонаправленные ассоциации в случае «один ко многим».

Каждый раз, когда я говорю «одна сторона не используется», вы также можете прочитать «одна сторона действительно редко используется». Существуют альтернативы для получения ссылки на его постоянное поддержание (например, передача его в качестве параметра метода при необходимости), поэтому всегда можно рассмотреть рефакторинг для однонаправленной ассоциации.

меры

Рассмотрим одну из двух ссылок, которые вы хотите удалить. Для того чтобы рефакторинг был осуществим, должен быть выполнен один из следующих вариантов:

  • ссылка не должна использоваться ;
  • должен быть другой способ получения ссылки или сообщения другому объекту, но это предмет других рефакторингов;
  • другой объект может быть добавлен в качестве аргумента к методам, которые его используют, изменяя ссылку с поля на параметр один.

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

  1. Ссылка должна быть проверена на использование или заменена параметром или другим алгоритмом, в зависимости от случая, в который входит этот сценарий.
  2. Удалите все присвоения полю и вызовы методов, которые присвоили это поле, если они станут пустыми.

Например, исключив неконтролирующую сторону , вы исключите вызов из контролирующей. Удаляя контролирующую сторону, которая является более редкой, вы устраняете метод на этой стороне и изменяете имя неконтролирующей, чтобы стать частью API класса.

пример

Изначально мы проверяем, что поле Phonenumber :: $ user больше не используется. Мы также можем передать объект User в качестве параметра в __toString () в случае, если на него ссылаются (конечно, нам нужно будет изменить имя, поскольку оно больше не будет магическим методом).

<?php
class ChangeBidirectionalAssociationToUnidirectional extends PHPUnit_Framework_TestCase
{
    public function testPhonenumbersDoNotNeedToReferToUsers()
    {
        $user = new User('Giorgio');
        $user->addPhonenumber(new Phonenumber('012345'));
        $this->assertEquals("012345\n", $user->phonenumbersList());
    }
}

class User
{
    private $name;
    private $phonenumbers;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function addPhonenumber(Phonenumber $phonenumber)
    {
        $this->phonenumbers[] = $phonenumber;
        $phonenumber->internalSetUser($this);
    }

    public function phonenumbersList()
    {
        $list = '';
        foreach ($this->phonenumbers as $number) {
            $list .= "$number\n";
        }
        return $list;
    }

    public function getName()
    {
        return $this->name;
    }
}

class Phonenumber
{
    private $number;

    /**
     * @var User
     */
    private $user;

    public function __construct($number)
    {
        $this->number = $number;
    }

    public function internalSetUser(User $u)
    {
        $this->user = $u;
    }

    public function __toString()
    {
        return $this->number;
    }
}

Удаляем код присваивания и поле. Код все еще проходит тест.

class User
{
    // ...

    public function addPhonenumber(Phonenumber $phonenumber)
    {
        $this->phonenumbers[] = $phonenumber;
        $phonenumber->internalSetUser($this);
    }
}

class Phonenumber
{
    private $number;

    public function __construct($number)
    {
        $this->number = $number;
    }

    public function internalSetUser(User $u)
    {
    }

    public function __toString()
    {
        return $this->number;
    }
}

Теперь мы можем удалить Api неконтролирующей стороны (поскольку эта сторона не имеет ссылок на другую). Объекты Phonenumber теперь проще и больше не зависят от класса User.

class User
{
    private $name;
    private $phonenumbers;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function addPhonenumber(Phonenumber $phonenumber)
    {
        $this->phonenumbers[] = $phonenumber;
    }

    public function phonenumbersList()
    {
        $list = '';
        foreach ($this->phonenumbers as $number) {
            $list .= "$number\n";
        }
        return $list;
    }

    public function getName()
    {
        return $this->name;
    }
}

class Phonenumber
{
    private $number;

    public function __construct($number)
    {
        $this->number = $number;
    }

    public function __toString()
    {
        return $this->number;
    }
}