Зареждане на интерфейси в конструктора

teroristd

Registered
Здравейте, продължавам с темата за DI Container. До сега съм направил да зарежда автоматично нормални класове и сингълтони. Сега искам да направя когато има интерфейс, контейнерът да види че има интерфейс, да види кой клас го инплементира и да зареди този клас. Как може да стане това?

Ето как изглежда контейнера в момента.

Код:
<?php

namespace application;

class Container {

    private static $_instance = NULL;

    private function __construct() {
        
    }





    /** @return \application\Container */
    public static function getInstance() {

        if (self::$_instance === NULL) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }





    public function make($class) {

        $reflector = new \ReflectionClass($class);

        $staticProperties = $reflector -> getStaticProperties();

        if ($staticProperties) {

            foreach ($staticProperties as $key => $val) {

                if ($key == '_instance' && $val === NULL) {

                    return $class::getInstance();
                }
            }
        }

        if (!$reflector -> isInstantiable()) {

            throw new \Exception('This Class ' . $class . ' Is Not Instantiable In: ' . __METHOD__, 500);
        }

        $constructor = $reflector -> getConstructor();

        if (is_null($constructor)) {

            return new $class;
        }

        $parameters = $constructor -> getParameters();

        $dependencies = $this -> getDependencies($parameters);

        return $reflector -> newInstanceArgs($dependencies);
    }





    public function getDependencies($parameters) {

        $dependencies = array();

        foreach ($parameters as $parameter) {

            $dependency = $parameter -> getClass();

            if (is_null($dependency)) {

                $dependencies[] = $this -> makeNoClass($parameter);
            }
            else {

                $dependencies[] = $this -> make($dependency -> getName());
            }
        }

        return $dependencies;
    }





    public function makeNoClass(ReflectionParameter $parameter) {

        if ($parameter -> isDefaultValueAvailable()) {

            return $parameter -> getDefaultValue();
        }

        throw new \Exception('This Parameter ' . $parameter . ' Is Unknown In: ' . __METHOD__, 500);
    }





}
 
teroristd каза:
Сега искам да направя когато има интерфейс, контейнерът да види че има интерфейс, да види кой клас го инплементира и да зареди този клас.

Това няма смисъл.

Интерфейса не се използва за class resolution (предполагам може да се направи), а за проверка (от самия език) дали класът, който смяташ да използваш, ще прави това, което искаш. Виж долния пример:



PHP:
<?php

interface IStore {
	public function storeData($data);
}
interface ICache {
	public function cacheData($data);
}

class DatabaseStore implements IStore {
	public function storeData($data) { echo 'storing in DB'; }
}
class FileStore implements IStore {
	public function storeData($data) { echo 'storing in File'; }
}

class DatabaseCache implements ICache {
	public function cacheData($data) { echo 'caching in DB'; }
}
class FileCache implements ICache {
	public function cacheData($data) { echo 'caching in File'; }
}

class Store {
	protected $store;
	public function __construct(IStore $store) {
		$this->store = $store;
	}

	public function save($data) {
		$this->store->storeData($data);
	}
}

class Cache {
	protected $cache;
	public function __construct(ICache $cache) {
		$this->cache = $cache;
	}

	public function save($data) {
		$this->cache->cacheData($data);
	}
}

$fs = new FileStore();
$dbc = new DatabaseCache();

$s = new Store($fs);
$c = new Cache($dbc);

$data = [1 => 'a'];

$s->save($data);
$c->save($data);

$dbs = new DatabaseStore();
// Плюе изключение защото DatabaseStore не имплементира ICache - така и трябва
$c = new Cache($dbs);
$c->save();

Мисля, че това което търсиш е Factory или Adapter в зависимост от класовете, които ще използваш.

Виж този сайт: https://sourcemaking.com/design_patterns
Там са описани много решения на различни проблеми с примери на PHP.
 
Не знам дали те разбирам правилно, но например на Ларавел контейнерът прави точно това.
Или поне така си мисля :).
При тях разбира се е направено по-доста различен начин от колкото при мен. Трябва да се описват разни пътища и имена, правят се през някакви методи нещата и т.н. При мен в момента всичко става автоматично, не описвам нищо никъде.

Ще пусна част от статията на Иван Ванков за DI, може би той го е обяснил по добре.

Както виждаме от статията по-долу в Ларавел трябва някъде да опишеш
нещо подобно.

App:bind( 'SessionStorage', 'MysqlSession' );
$session = $this->app->make('Session');

Аз искам да става автоматично. Контейнерът да види че съм подал интерфейс в конструктора, да види кой клас го инплементира и да зареди този клас. Всичко трябва да стане в метода make. В случая ако подам интерфейс ми трещи грешка, което е нормално защото контейнера търси клас.



Код:
DI е механизъм, който (най-често) използва кодиране спрямо интерфейси за да оправи цялата сложност по създаването на новите инстанции и тяхното инжектиране. Различните езици и библиотеки имат различни имплементации, но всички се свеждат до следните прости стъпки:

Пишете си кода спрямо интерфейси
Декларирайте зависимости на обект като опишете интерфейсите в конструктора на обекта
Дефинирайте коя конкретна имплементация трябва да се използва е момента (най-често в конфигурационен файл) и решете какво да е поведението и, дали да е само една, или всеки път да се прави нова инстанция
Това е, нищо сложно, понеже DI контейнера се грижи за всичко останало.

Ще се опитам да дам донякъде реалистичен (силно опростен) пример от света на PHP и Laravel как точно се случват нещата, но всички езици и frameworks имат множество DI библиотеки, които се различават и имат различни възможности.

interface SessionStorage{
  public function get( $key );
  public function set( $key, $value );
}

class FileSession implements SessionStorage{

  public function get( $key ){}

  public function set( $key, $value ){}
}

class MysqlSession implements SessionStorage{

  
  public function get( $key ){}

  public function set( $key, $value ){}
}

class Session{

  protected $session;

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

App:bind( 'SessionStorage', 'MysqlSession' );

$session = $this->app->make('Session');

Нека да видим какво точно се случва тук. Първо правим интерфейс какво трябва да може да прави  всеки един SessionStorage клас. След това декларираме конкретните имплементации. До тук е ясно. Магията е в последните два реда.

App:bind( 'SessionStorage', 'MysqlSession' );

Тук казваме на Laravel, че когато нещо някъде има нужда от SessionStorage ще използва MysqlSession

$session = $this->app->make('Session');

А така караме DI контейнера да направи нова инстанция на Session, която зависи от интерфейса SessionStorage. В този случай обекта Session ще получи автоматично MysqlSession в конструктора си. Ако в последствие решим, че искаме да използваме FileSession ще трябва да сменим само един ред:

App:bind( 'SessionStorage', 'FileSession' );

Разбира се, това е много елементарен пример, но показва ясно колко работа може да си спестим. Този подход работи много добре с тестване, continuous integration и автоматизация, защото кое какво използва се определя на едно място, което може да е конфигурационен файл.

Също така повечето хубави DI контейнери имат много възможности, добавяне на условия кога коя имплементация да се използва, дали да е една за всички или не, тагове, рециклиране…. Но ако разберете тази проста идея няма да имате затруднения в разбирането на останалите детайли.

Някой от вас ще си кажат, че това може да го постигнете и с нещо като Factory. Има допирни точки, но при Factory шаблона трябва да се опишете всичко на ръка, което ще доведе до безумна серия от IF/ELSE или нещо подобно. С DI този процес е автоматизиран. Също така в PHP света (и не само) има тенденция да се правят спецификации и стандарти за интерфейси. Примерно какви са интерфейсите за кеширане, за HTTTP заявки и подобни, и всички библиотеки започват да пишат спрямо тях. При използване на DI и общи интерфейси може да подмените огромна подсистема само с един ред код.
 
Да, колегите са прави, че ако говориш за injection няма смисъл да се прави, защото много класове могат да имплементират един интерфейс. Dependency inversion ти трябва, при което просто в контейнера регистрираш даден интерфейс на кой клас отговаря и после подаваш вместо класа интерфейса като type-hinting. Тогава можеш лесно и от едно място да смениш различни имплементации, както става и в laravel.

Пример:
PHP:
class Container {
protected $interfaces; //$interface=>$class

//...

public function registerInterface($interface, $class) {
  $interfaces[$interface] = $class;
}

public function getInterfaceClass($interface) {
  return $interfaces[$interface];
}
}

И вече при резолюцията се допитваш до getInterfaceClass дали има регистриран клас към дадения интерфейс и тогава правиш нова инстанция/връщаш singleton или каквото искаш
 
Въпроса е че аз не искам да описвам нищо ръчно. При мен ще е 1 към 1, един интерфейс за един клас. За това ми трябва начин да проверя първо дали подадения параметър е интерфейс и ако е кой клас го инплементира.
 
Е това е безсмислено, като имаш 1 клас дето имплементира интерфейс защо ти е интерфейс? Иначе направи си една папка interfaces една implementations с еднакви имена на клас и интерфейс само че интерфейса да завършва на Interface примерно и като попаднеш на интерфейс от resolve-ра просто търсиш в папка implementations
 
Не е безмислено, дали един два или сто класа имплементират един интерфрйс е все едно, важното в случая е че класа ще има точно тези методи които са необходими. Нали точно това е смисъла на интерфейса.

Първата част е лесна, така проверявам дали е интерфейс и взимам неймспейса. Сега въпроса е как да проверя кой клас го имплементира?

Код:
$interface = $reflector -> isInterface();

        if ($interface) {

            echo $name = $reflector -> getName();

        }
 
От тук: http://stackoverflow.com/questions/3993759/php-how-to-get-a-list-of-classes-that-implement-certain-interface

PHP:
$interface_name = 'IStore';
if (interface_exists($interface_name)) {
	var_dump(array_filter(get_declared_classes(), create_function('$className', "return in_array(\"$interface_name\", class_implements(\"\$className\"));")));
}
else {
	echo '0';
}
 
По някаква причина някои класове ги няма в списъка когато викам функцията get_declared_classes(), включително тези с интерфейсите които пробвам. По тази причина така не става.
 
Е какъв е смисълът като ти сам си казваш кои методи ще има? А какво става ако решиш да смениш имплементациите, приверно имаш MongoClass и MysqlClass
 
Е какъв е смисълът като ти сам си казваш кои методи ще има? А какво става ако решиш да смениш имплементациите, приверно имаш MongoClass и MysqlClass
 
teroristd каза:
По някаква причина някои класове ги няма в списъка когато викам функцията get_declared_classes(), включително тези с интерфейсите които пробвам. По тази причина така не става.

Може би класовете не са load-нати в момента на проверката.
 
eLite каза:
teroristd каза:
По някаква причина някои класове ги няма в списъка когато викам функцията get_declared_classes(), включително тези с интерфейсите които пробвам. По тази причина така не става.

Може би класовете не са load-нати в момента на проверката.

Не би трябвало, защото load-инга ми се изпълнява преди контейнера. Иначе той и самия контейнер нямаше да зареди. Не знам какво всъщност означава един клас да е деклариран.
 
lam3r4370 каза:
Е какъв е смисълът като ти сам си казваш кои методи ще има? А какво става ако решиш да смениш имплементациите, приверно имаш MongoClass и MysqlClass

Еми то по тая логика интерфейсите са безмислени сами по себе си. Ти и два класа да имаш пак сам казваш кои методи ще имат :D . А имплементациите са си сами за себе си защото аз някъде из приложението си викам метода който ми е гарантиран от интерфейса че съществува и така няма да изгърми нищо, а инплементацията мога да си я сменям колкото си искам.
 
Ами точно, че като искаш да смениш интерфейса какво правиш? Ще изтриеш стария и ще сложиш нов или ще запазиш стария и просто ще кажеш на контейнера да зарежда новия? Затова е най-добре ти да посочваш кой интерфейс към кой клас се resolve-ва
 
lam3r4370 каза:
Ами точно, че като искаш да смениш интерфейса какво правиш? Ще изтриеш стария и ще сложиш нов или ще запазиш стария и просто ще кажеш на контейнера да зарежда новия? Затова е най-добре ти да посочваш кой интерфейс към кой клас се resolve-ва

Еми то същите неща важат и за този начин който казваш. Ти ако смениш интерфейса сменяш апи-то, което значи че ще трябва да преправиш и класа който го имплементира и навсякъде където нещата зависят от този клас. Същото е замисли се, независимо дали си посочил кой интерфейс към кой клас се resolve-ва или си го взел автоматично. Разликата е там че ти ще можеш да използваш един интерфейс за повече от един клас, но нищо не ти гарантира че и двата класа ще се нуждаят точно от едни и същи методи. А и така може да стане много по голяма каша от колкото когато е 1 към 1. При мене недостатъкът е че може да имам повече интерфейси от колкото са ми необходими, но пък никъде не е казано че за всеки клас трябва да ползваш интерфейс.
 
Я var_dump-ни следните функции в следните позиции:

PHP:
get_declared_classes();
get_declared_interfaces();

В началото на index.php (който предполагам ти зарежда цялото приложение) и в някоя крайна функция (примерно функция на controller).
 
В индекса изобщо не излизат контролерите, нито интерфейсите. В контролерите излиза само този от който викам функцията и евентуално такъв който съм инжектирал в конструктора, включително ако е интерфейс ми вади и класа който го инплементира. Един вид има значение от къде я викаш.
 

Back
Горе