Статьи

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

Графы объектов строятся путем сохранения ссылок на другие объекты внутри полей объекта (обычно приватных). Эти ассоциации сохраняются и хранятся в ORM или другом механизме или просто создаются PHP-кодом, вызывающим конструкторы и установщик во время создания экземпляра вашего приложения.

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

Обратите внимание, что кратность, с которой объекты участвуют в ассоциации, не обязательно равна 1 : у нас может быть ссылка на один объект B в классе A и ссылка на массив объектов A в классе B. Действительно, многие для Одна ассоциация является наиболее распространенным случаем.

Зачем мне вводить двунаправленную ассоциацию?

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

  • вызов метода для общего объекта с состоянием (скрытого интерфейсом), который составляет текущий . Например, Front Controller — это объект самого высокого уровня, на который могут ссылаться задние контроллеры нижнего уровня, чтобы изменить состояние приложения (переадресация на другие контроллеры или немедленное перенаправление).
  • Объектам, имеющим многозначные ассоциации, обычно необходимо поддерживать связь на одной стороне (стороне-владельце) в современных ORM , чтобы имитировать внешний ключ и упростить реализацию. Таким образом, Пользователь, составляющий N Phonenumbers, обнаружит, что Phonenumbers должен поддерживать ссылку на него, даже если он никогда не перезванивает Пользователю в своем коде.

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

меры

  1. Введите личное поле для обратного указателя.
  2. Выберите , какой класс контролирующей стороны , которая означает , что он принимает метод , который инициирует создание или обновление ассоциации.
  3. Создайте «внутренний» метод на неконтролирующей стороне . Мы фактически используем внутренний префикс (например, internalSetUser (User $ u)), чтобы показать, что этот метод никогда не должен вызываться клиентским кодом.
  4. Обновите существующий модификатор : если он находится на контролирующей стороне, он должен вызвать наш новый внутренний метод. Если он находится на неконтролирующей стороне, его следует переместить на управляющую.

В этом примере мы начнем с пользователя, содержащего метод addPhonenumber (Phonenumber $ number), и добавим метод internalSetUser (User $ u) в класс Phonenumber, привязав его к первому методу.

пример

В исходном состоянии пользователь создает массив объектов Phonenumber. Он может легко составить список своих телефонных номеров, полных имени.

<?php
class ChangeUnidirectionalAssociationToBidirectional extends PHPUnit_Framework_TestCase
{
    public function testPhonumbersWillReferBackToUsers()
    {
        $user = new User('Giorgio');
        $user->addPhonenumber(new Phonenumber('012345'));
        $this->assertEquals("Giorgio: 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;
    }

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

class Phonenumber
{
    private $number;

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

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

Мы хотим перенести ответственность за построение каждого строкового элемента списка на Phonenumber.

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

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
{
    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 .= "$this->name: $number\n";
        }
        return $list;
    }
}

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

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

class Phonenumber
{
    ...
    public function __toString()
    {
        return $this->user->getName() . ': ' . $this->number;
    }
}