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

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

проблема у меня-это что-то вроде этого: Допустим, у меня два класса: автомобиль и пассажир; Для этих двух классов у меня есть некоторые карты данных для работы с базой данных: 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;  
    }
}

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

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

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

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

Я также посмотрел на некоторые рамки DI и прочитал о контейнерах DI и инверсии управления, но из того, что я понял, они используются для определения и введения зависимостей для "нединамических" классов, где, например, автомобиль будет зависеть от двигателя, но ему не понадобится двигатель, который будет динамически загружаться из БД, будет просто создан и впрыснут в автомобиль.

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

3 ответов


Я собирался сказать: "я знаю, это старый вопрос, но..."тогда я понял, что вы отправили его 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" для каждого сопоставителя, который понадобится объекту, а затем установите его в слое сервиса, тогда сопоставитель позаботится о подготовке объектов. Я все еще взвешиваю различия между этими двумя подходами.

пример координации в слой сервиса:

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;
    }
}

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

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 с идентификаторами объектов в качестве параметров. Я уверен, что с некоторыми сидя и думая, что есть элегантное решение для включения других параметров, скажем, получение "открытых" билетов против "закрытых" билетов, принадлежащих объекту отдела.


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

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

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

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

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

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

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

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

  2. Что вы хотите в первом блоке кода это вполне осуществимо. Посмотрите, как работает доктрина ORM, или этот очень простой подход ORM, который я сделал несколько месяцев назад для проекта weekend:

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


вот еще один подход, о котором я подумал. Вы можете создать объект "DAOInjection", который действует как контейнер для определенных DAO, обратного вызова и args, необходимых для возврата требуемых объектов. Классы тогда должны знать только об этом классе DAOInjection, поэтому они все еще отделены от всех ваших DAOs/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, вам не придется жестко кодировать его в объект. Кроме того, если вам нужно "трубить" методы вместе-скажем, вам нужно пройти $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 в качестве аргументов для конструкторов сопоставителей. Затем картографы позаботятся о том, чтобы каждый объект имел свои инъекции, потому что задача картографа-вернуть полные объекты и обработать сохранение / удаление объектов, в то время как задача службы-координировать отношения между различными картографами, объектами и т. д.

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

надеюсь, что это помогает!