Подтвердить что ты не робот

PHP Lazy загрузки объектов и инъекции зависимостей

Недавно я начал читать об инъекции зависимостей, и он заставил меня переосмыслить некоторые из моих проектов.

Проблема у меня есть что-то вроде этого: Скажем, у меня есть два класса: автомобиль и пассажир; Для этих двух классов у меня есть некоторые данные для работы с базой данных: CarDataMapper и PassengerDataMapper

Я хочу иметь возможность сделать что-то вроде этого в коде:

$car = CarDataMapper->getCarById(23); // returns the car object
foreach($car->getPassengers() as $passenger){ // returns all passengers of that car
    $passenger->doSomething();
}

Прежде чем я узнал что-нибудь о DI, я бы построил свои классы следующим образом:

class Car {
    private $_id;
    private $_passengers = null;

    public function getPassengers(){
        if($this->_passengers === null){
            $passengerDataMapper = new PassengerDataMapper;
            $passengers = $passengerDataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        }
        return $this->_passengers;  
    }

}

У меня также был бы аналогичный код в методе Passenger- > getCar() для извлечения автомобиля, в котором находится пассажир.

Теперь я понимаю, что это создает зависимости (ну, я тоже это понял), но я не знал, что это "неправильно" ) между объектами "Автомобиль" и "Пассажир" и объектами отображения данных.

При попытке думать о решении для этих двух вариантов мне пришло в голову, но мне не очень-то нравится:

1: Выполнение примерно так:

$car = $carDataMapper->getCarById(23);
$passengers = $passengerDataMapper->getPassengersByCarId($car->getId());
$car->setPassengers($passengers);

foreach($car->getPassengers() as $passenger){
    $passenger->doSomething();
}

Но что делать, если у пассажиров есть объекты, которые им нужно вводить, и что, если вложенность идет до десяти или двадцати уровней... Я бы включил экземпляр почти каждого объекта в начале моего приложения, что в свою очередь запросило бы весь базы данных во время процесса. Если я должен отправить пассажира другому объекту, который должен что-то сделать с объектами, которые удерживает пассажир, я не хочу немедленно создавать эти объекты тоже.

2: Внедрение карт данных в автомобильные и пассажирские объекты и что-то вроде этого:

class Car {
    private $_id;
    private $_passengers = null;
    private $_dataMapper = null;

    public function __construct($dataMapper){
        $this->setDataMapper($dataMapper);
    }

    public function getPassengers(){
        if($this->_passengers === null && $this->_dataMapper instanceof PassengerDataMapper){
            $passengers = $this->_dataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        }
        return $this->_passengers;  
    }
}

Мне не нравится это лучше, потому что это не похоже на то, что автомобиль действительно не знает о карте данных, и без устройства отображения данных автомобиль может вести себя непредсказуемо (не возвращая пассажиров, когда они на самом деле есть)

Итак, мой первый вопрос: Я принимаю совершенно неправильный подход здесь, потому что, чем больше я смотрю на него, тем больше похоже, что я строю ORM вместо бизнес-уровня?

Второй вопрос: есть ли способ фактического развязывания объектов и карт данных таким образом, чтобы я мог использовать объекты, как описано в самом первом блоке кода?

Третий вопрос: Я видел некоторые ответы на другие языки (например, какая-то версия C), разрешая эту проблему с чем-то вроде описанного здесь: Каков правильный способ ввода зависимости доступа к данным для ленивой загрузки? Поскольку у меня не было времени играть на других языках, для меня это не имеет смысла, поэтому я был бы признателен, если бы кто-нибудь объяснил примеры в ссылке в PHP-ish.

Я также рассмотрел некоторые рамки DI и прочитал о DI Containers и Inversion of Control, но из того, что я понял, они используются для определения и ввода зависимостей для "нединамических" классов, где, например, Car будет зависеть на двигателе, но ему не нужно, чтобы двигатель динамически загружался из db, он просто был бы создан и инъецирован в автомобиль.

Извините за длинный пост и спасибо заранее.

4b9b3361

Ответ 1

Я собирался сказать: "Я знаю, что это старый вопрос, но...", тогда я понял, что вы отправили его 9 часов назад, и это круто, потому что я просто пришел к удовлетворительному "разрешению" для себя. Я подумал о реализации, и тогда я понял, что люди называют "инъекцией зависимостей".

Вот пример:

class Ticket {

    private $__replies;
    private $__replyFetcher;
    private $__replyCallback;
    private $__replyArgs;

    public function setReplyFetcher(&$instance, $callback, array $args) {
        if (!is_object($instance))
            throw new Exception ('blah');
        if (!is_string($callback))
            throw new Exception ('blah');
        if (!is_array($args) || empty($args))
            throw new Exception ('blah');
        $this->__replyFetcher = $instance;
        $this->__replyCallback = $callback;
        $this->__replyArgs = $args;
        return $this;
    }

    public function getReplies () {
        if (!is_object($this->__replyFetcher)) throw new Exception ('Fetcher not set');
        return call_user_func_array(array($this->__replyFetcher,$this->__replyCallback),$this->__replyArgs);
    }

}

Затем на вашем уровне обслуживания (где вы "координируете" действия между несколькими картографами и моделями) вы можете вызвать метод setReplyFetcher во всех объектах билета, прежде чем вы вернете их во все, что вызывает уровень сервиса - ИЛИ - вы можете сделать что-то очень похожее с каждым картографом, предоставив картографу личное свойство "fetcherInstance" и "callback" для каждого картографа, в котором этот объект понадобится, и затем установите THAT на уровне сервиса, тогда он будет позаботьтесь о подготовке объектов. Я все еще взвешиваю различия между двумя подходами.

Пример координации на уровне сервиса:

class Some_Service_Class {
    private $__mapper;
    private $__otherMapper;
    public function __construct() {
        $this->__mapper = new Some_Mapper();
        $this->__otherMapper = new Some_Other_Mapper();
    }
    public function getObjects() {
        $objects = $this->__mapper->fetchObjects();
        foreach ($objects as &$object) {
            $object->setDependentObjectFetcher($this->__otherMapper,'fetchDependents',array($object->getId()));
        }
        return $objects;
    }
}

В любом случае классы объектов не зависят от классов сопоставления, а классы-сопоставления не зависят друг от друга.

EDIT: Вот пример другого способа сделать это:

class Some_Service {
    private $__mapper;
    private $__otherMapper;
    public function __construct(){
        $this->__mapper = new Some_Mapper();
        $this->__otherMapper = new Some_Other_Mapper();
        $this->__mapper->setDependentFetcher($this->__otherMapper,'someCallback');
    }
    public function fetchObjects () {
        return $this->__mapper->fetchObjects();
    }        
}

class Some_Mapper {
    private $__dependentMapper;
    private $__dependentCallback;
    public function __construct ( $mapper, $callback ) {
        if (!is_object($mapper) || !is_string($callback)) throw new Exception ('message');
        $this->__dependentMapper = $mapper;
        $this->__dependentCallback = $callback;
        return $this;
    }
    public function fetchObjects() {
        //Some database logic here, returns $results
        $args[0] = &$this->__dependentMapper;
        $args[1] = &$this->__dependentCallback;
        foreach ($results as $result) {
            // Do your mapping logic here, assigning values to properties of $object
            $args[2] = $object->getId();
            $objects[] = call_user_func_array(array($object,'setDependentFetcher'),$args)
        }
    }
}

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

Ответ 2

Может быть, вне темы, но я думаю, что это поможет вам немного:

Я думаю, что вы пытаетесь достичь идеального решения. Но независимо от того, что вы придумали, через пару лет вы станете более опытными, и вы определенно сможете улучшить свой дизайн.

В последние годы с моими коллегами мы разработали множество ORM/бизнес-моделей, но почти для каждого нового проекта мы начинаем с нуля, так как все были более опытными, все узнали из предыдущих ошибок, и все столкнулись с новые модели и идеи. Все это добавило дополнительный месяц или около того в развитие, что увеличило стоимость конечного продукта.

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

Если, конечно, вы не кодируете для исследования или для удовольствия.

TL; DR: Ваше будущее "я" всегда перехитрит ваше нынешнее "я", поэтому не переусердствуйте. Просто тщательно подбирайте рабочее решение, осваивайте его и придерживайтесь его, пока оно не решит ваши проблемы: D

Чтобы ответить на ваши вопросы:

  • Ваш код в порядке, но чем больше вы попытаетесь сделать его "умным", "абстрактным" или "без зависимости", тем больше вы будете склоняться к ORM.

  • То, что вы хотите в первом блоке кода, вполне возможно. Посмотрите, как работает Doctrine ORM, или этот очень простой подход ORM, который я сделал несколько месяцев назад для проекта на выходные:

https://github.com/aletzo/dweet/blob/master/app/models

Ответ 3

Вот еще один подход, о котором я думал. Вы можете создать объект DAOInjection, который действует как контейнер для определенных DAO, callback и args, необходимых для возврата желаемых объектов. Затем классы должны знать только об этом классе DAOInjection, поэтому они все еще отделены от всех ваших DAO/mappers/services/etc.

class DAOInjection {
    private $_DAO;
    private $_callback;
    private $_args;
    public function __construct($DAO, $callback, array $args){
        if (!is_object($DAO)) throw new Exception;
        if (!is_string($callback)) throw new Exception;
        $this->_DAO = $DAO;
        $this->_callback = $callback;
        $this->_args = $args;
    }
    public function execute( $objectInstance ) {
        if (!is_object($objectInstance)) throw new Exception;
        $args = $this->_prepareArgs($objectInstance);
        return call_user_func_array(array($this->_DAO,$this->_callback),$args);
    }
    private function _prepareArgs($instance) {
        $args = $this->_args;
        for($i=0; $i < count($args); $i++){
            if ($args[$i] instanceof InjectionHelper) {
                $helper = $args[$i];
                $args[$i] = $helper->prepareArg($instance);
            }
        }
        return $args;
    }
}

Вы также можете передать "InjectionHelper" в качестве аргумента. InjectionHelper действует как другой контейнер обратного вызова - таким образом, если вам нужно передать любую информацию о ленивом загружаемом объекте в его вложенный DAO, вам не придется жестко закодировать его в объекте. Кроме того, если вам нужно совместно использовать методы 'pipe', скажите, что вам нужно передать $this->getDepartment()->getManager()->getId() в введенную DAO по любой причине - вы можете. Просто передайте его как getDepartment|getManager|getId конструктору InjectionHelper.

class InjectionHelper {
    private $_callback;
    public function __construct( $callback ) {
        if (!is_string($callback)) throw new Exception;
        $this->_callback = $callback;
    }
    public function prepareArg( $instance ) {
        if (!is_object($instance)) throw new Exception;
        $callback = explode("|",$this->_callback);
        $firstCallback = $callback[0];
        $result = $instance->$firstCallback();
        array_shift($callback);
        if (!empty($callback) && is_object($result)) {
            for ($i=0; $i<count($callback); $i++) {
                $result = $result->$callback[$i];
                if (!is_object($result)) break;
            }
        }
        return $result;
    }
}

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

class Some_Object {
    private $_childInjection;
    private $_parentInjection;
    public function __construct(DAOInjection $childInj, DAOInjection $parInj) {
        $this->_childInjection = $childInj;
        $this->_parentInjection = $parInj;
    }
    public function getChildObjects() {
        if ($this->_children == null)
            $this->_children = $this->_childInjection->execute($this);
        return $this->_children;
    }
    public function getParentObjects() {
        if ($this->_parent == null)
            $this->_parent = $this->_parentInjection->execute($this);
        return $this->_parent;
    }
}

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

В конечном счете вы можете использовать его для ввода обратных вызовов для служб или карт, поэтому скажите, что вы хотите, чтобы ваш объект "Ticket" получал родительского пользователя, который, как оказалось, находится за пределами "Ticket Service" - служба билета может просто вставьте обратный вызов в "Сервис пользователя", и ему не нужно будет знать, как работает DAL для других объектов.

Надеюсь, это поможет!