Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!

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

понедельник, 27 августа 2012 г.

TDD на PHP используя Zend Studio - выделяем из калькулятора зависимость и мочим ее

Краткое содержание прошлых серий:
- подружили Zend Studio и Zend Server
- подружили Zend Studio и GitHub
- написали калькулятор по TDD

Калькулятор умеет суммировать числа в разных системах счисления от 2-ричной до 17-ричной. Его можно развивать дальше (умножение, деление, работа с отрицательными числами), но это не так интересно и будет выглядить +/- так же как и в прошлой серии. Сейчас же пойдем другим путем - от заказчика пришло требование-нежданчик. На базе существующего калькулятора необходимо реализовать калькулятор, работающий с римскими числами, при этом старая возможность суммировать в разных системах счисления должна оставаться не тронутой.

Как быть? Вариантов реализаций множество, но как показывает практика ту, которую мы разберем тут называют в самую последнюю очередь (или не называют вообще).

Если присмотреться, то в существубщем калькуляторе уже наблюдается нарушение принципа единой ответственности (SRP или Single Responsibility Principle)

class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;

        if ($this->isContainsInvalidNumber($expression)) {
            throw new RuntimeException('Invalid number');
        }
        
        if ($base > strlen($this->Digits) || $base <= 1) {
            throw new RuntimeException('Invalid base');
        }
        
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
            
        $sum = $this->hexToInt($out[0][0]) + 
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function isContainsInvalidNumber($expression) {
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
            
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;                
        }            
        return $is_invalid;        
    }
    
    private function intToHex($int) {
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
        
        return $result;
    }

    
    private function toHex($int) {
        return $this->Digits[$int];
    }
    
    private function hexToInt($hex) {        
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;

    }
    
    private function toInt($hex) {
        if (is_numeric($hex)) return $hex; 
        return strpos($this->Digits, $hex);
    }

}

Сейчас можно углядеть что он работает так - получая на вход два числа, валидирует правильность чисел, после конвертирует их в 10ричную систему, после складывает и конфертирует обратно в исходную систему счисления. конвертирует -> валидирует -> суммирует -> конвертирует. Не знаю как у тебя, но со словом калькулятор у меня больше ассоциируется глагол "суммирует", а вот конвертация и валидация - это что-то несвейственное ему. Кроме того именно конвертация+валидация подвергается обстрелу с новым требованием-нежданчиком от любимого заказчика. А раз уж меняется - инкапсулируй! Это один из ООП-шных советов. Давай проследуем ему.

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

Первым делом я хочу выделить логику конвертации из x-ричной системы счисления в 10-ричную.

[тест]
class ConvertorTest extends PHPUnit_Framework_TestCase {
  
    private $Convertor;
 

    protected function setUp() {
        parent::setUp ();
        $this->Convertor = new Convertor();
    }
 
    protected function tearDown() {
        $this->Convertor = null;
        parent::tearDown ();
    }
 
    public function testShouldConvertHexToInt() { // вот самое интересное
        $actual =  $this->Convertor->decode('1ABC', '16');
        $this->assertEquals(6844, $actual);
    }
  
}
[фикс]
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) { // все строки кроме этого метода были перенесены копипастом
        $this->Base = $base;
        
        return $this->hexToInt($hex);
    }
    
    private function hexToInt($hex) {
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;
    
    }
    
    private function toInt($hex) {
        if (is_numeric($hex)) return $hex;
        return strpos($this->Digits, $hex);
    }
}
[коммит] Начал выносить зависимости по конвертации в отдельный класс
[тест]
    public function testShouldConvertIntToHex() {
        $actual =  $this->Convertor->code('6844', '16');
        $this->assertEquals('1ABC', $actual);
    }
[фикс]
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) {
        $this->Base = $base;
        
        return $this->hexToInt($hex);
    }
    
    public function code($hex, $base) { // добавили новый метод
        $this->Base = $base;
    
        return $this->intToHex($hex);
    }
    
    private function intToHex($int) { // это скопитырили из калькулятора
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
    
        return $result;
    }
    
    private function toHex($int) {  // это тоже скопитырили из калькулятора
        return $this->Digits[$int];
    }
    
   ... 
}
[коммит] Вынес вторую, операцию кодирование в x-ричную систему Теперь сразу два теста, один положительный а другой отрицательный.
[тест]
    public function testShouldValidateTrueIfValid() {
        $this->assertTrue($this->Convertor->isValid('6844', '16'));
    }
    
    public function testShouldValidateFalseIfInvalid() {
        $this->assertFalse($this->Convertor->isValid('1ABG', '16'));
    }
[фикс]
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) {
        $this->Base = $base;
        
        return $this->hexToInt($hex);
    }
    
    public function code($hex, $base) {
        $this->Base = $base;
    
        return $this->intToHex($hex);
    }
    
    public function isValid($hex, $base) { // новый метод
        $this->Base = $base;
    
        return !$this->isContainsInvalidNumber($hex);
    }
    
    ... 
    
    private function isContainsInvalidNumber($expression) { // копитыренный метод
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
                
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;
        }
        return $is_invalid;
    }
}

?>
[коммит] Вынес вторую, операцию кодирование в x-ричную систему Я вижу много дублирования, а потому сделаю рефакторинг
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) { // тут передается $base
        $this->Base = $base; // а тут устанавливается в поле
        
        return $this->hexToInt($hex);
    }
    
    public function code($hex, $base) { // и тут
        $this->Base = $base; // и тут
    
        return $this->intToHex($hex);
    }
    
    public function isValid($hex, $base) { // и тут
        $this->Base = $base; // и тут
    
        return !$this->isContainsInvalidNumber($hex);
    }
[рефакторинг] избавился от дублирования
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function __construct($base) { // появился конструктор принимающий основание
        $this->Base = $base;
    }
    
    public function decode($hex) { // зато основание пропало отсюда
        return $this->hexToInt($hex);
    }
    
    public function code($hex) {
        return $this->intToHex($hex);
    }
    
    public function isValid($hex) {
        return !$this->isContainsInvalidNumber($hex);
    }
Естественно это привело к исправлению тестов.
class ConvertorTest extends PHPUnit_Framework_TestCase {
        
    private $Convertor;
    
    protected function setUp() {
        parent::setUp ();
        $this->Convertor = new Convertor(16); // тут
    
    }
    
    protected function tearDown() {
        $this->Convertor = null;
        parent::tearDown ();
    }
    
    public function testShouldConvertHexToInt() {
        $actual =  $this->Convertor->decode('1ABC'); // тут
        $this->assertEquals(6844, $actual);
    }
    
    public function testShouldConvertIntToHex() {
        $actual =  $this->Convertor->code('6844'); // тут
        $this->assertEquals('1ABC', $actual);
    }
    
    public function testShouldValidateTrueIfValid() {
        $this->assertTrue($this->Convertor->isValid('6844')); // тут
    }
    
    public function testShouldValidateFalseIfInvalid() {
        $this->assertFalse($this->Convertor->isValid('1ABG')); // а тут даже тест немного переделал
    }
    
}
[коммит]
[рефакторинг] Дальше пачка методов ничего не делающих, а только валидирующих
    public function decode($hex) {
        return $this->hexToInt($hex); 
    }
    
    public function code($hex) {
        return $this->intToHex($hex);
    }
    
    public function isValid($hex) {
        return !$this->isContainsInvalidNumber($hex);
    }
Вердикт - убрать, переименовав hexToInt в decode, intToHex в code, isContainsInvalidNumber в isValid с инвертированием return!
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function __construct($base) {
        $this->Base = $base;
    }
    
    public function code($int) { // переименовали и сделали public
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
    
        return $result;
    }
    
    
    private function toHex($int) {
        return $this->Digits[$int];
    }
    
    public function decode($hex) { // переименовали и сделали public
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;
    
    }
    
    private function toInt($hex) {
        if (is_numeric($hex)) return $hex;
        return strpos($this->Digits, $hex);
    }
    
    public function isValid($expression) { // переименовали и сделали public
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
                
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;
        }
        return !$is_invalid; // инвертировали результат перед возвратом
    }
}
[коммит]
[рефакторинг]Теперь можно подключить акуратно перенесенную реализацию к калькулятору и удалить у него лишние методы.
require_once 'application\models\Convertor.php'; // незабываем импорт

class Calculator {    
    
    // это нам больше тут не понадобится - удаляем
    // private $Base; 
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $convertor = new Convertor($base); // создаем новый инстанс конвертера

        if (!$convertor->isValid($expression)) { // валидация (осторожно, инвертируем результат)
            throw new RuntimeException('Invalid number');
        }
        
        if ($base > strlen($this->Digits) || $base <= 1) {
            throw new RuntimeException('Invalid base');
        }
        
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
            
        $sum = $convertor->decode($out[0][0]) + // конвертер декодирует, а калькулятор суммирует
               $convertor->decode($out[0][1]);    

        return $convertor->code($sum); // конвертер кодирует обратно
    }

}
[коммит]

Что мы имеем сейчас? Зависимость акуратно шаг за шагом была вынесена в отдельный класс Convertor.

Старые тесты калькулятора тестируют два класса вместе, что не совсем юнит тест. Есть зависимость у калькулятора в виде инстанциирования new Convertor в теле метода calculate. Эту зависимость необходимо разорвать.

Часть тестов из текущего интеграционного (т.к. тестирует два класса в связке) перенести в ConvertorTest, а CalculatorTest переписать на моках.

Успокоения ради можно оставить один интеграционный тест, который проверит два класса в связке.

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

Так же стоит еще проверку на base вынести в конструктор Converter - ей там больше место.

После этой операции можно приступать к разработке сумматора римских чисел.

Это мой тест лист на текущий момент. Из него выбираем следующий наиболее простой шаг и вперед!

[рефакторинг] Начну с того, чтобы isValid не знал про суммирование, а валидация основания проходила в конструкторе конвертора.

class Calculator {    
    
//     private $Digits = "0123456789ABCDEFG"; // это лишнее теперь
    
    public function calculate($expression, $base) {
        $convertor = new Convertor($base);

//         if (!$convertor->isValid($expression)) {   // валидацию разместили ниже, после парсинга
//             throw new RuntimeException('Invalid number');
//         }
        
//         if ($base > strlen($this->Digits) || $base <= 1) { // это отнесли в конструктор конвертора
//             throw new RuntimeException('Invalid base');
//         }
        
        preg_match_all('/[0-9A-Z]+/', $expression, $out); // поменяли регекспу - теперь она выкусывает все символьно-цифровое
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
        
        if (!$convertor->isValid($out[0][0]) || !$convertor->isValid($out[0][1])) { // добавили новую проверку для операндов
            throw new RuntimeException('Invalid number');
        }
            
        $sum = $convertor->decode($out[0][0]) + 
               $convertor->decode($out[0][1]);    

        return $convertor->code($sum);
    }

}

class Convertor {  // а теперь класс конвертора
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function __construct($base) {
        if ($base > strlen($this->Digits) || $base <= 1) { // добавили проверку
            throw new RuntimeException('Invalid base');
        }
        
        $this->Base = $base;
    }
     
        public function isValid($expression) {
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
//             if ($expression[$index] == '+') { // это лишнее уже
//                 continue;
//             }
                
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;
        }
        return !$is_invalid;
    }

    ...
[коммит]
[рефакторинг] Сейчас я бы перенес часть тестов на их законное место. Так как класс калькулятора теперь уже занимается только суммированием, то часто тестов из CalculatorTest должна быть перенесена в ConvertorTest. Данные для тестов конвертора будем брать из рассчета того, что передается в него сейчас при запуске тестов CalculatorTest.

class ConvertorTest extends PHPUnit_Framework_TestCase {
        
    public static function validDataProvider() {
        return array(
                array(10, '2', '2'),
                array(10, '4', '4'),
                array(10, '3', '3'),
                array(10, '7', '7'),
                array(10, '11', '11'),
                array(10, '22', '22'),
                array(10, '33', '33'),
                
                array(16, '1', '1'),
                array(16, '2', '2'),
                array(16, '5', '5'),
                array(16, '6', '6'),
                array(16, '8', '8'),
                array(16, '9', '9'),
                array(16, 'A', '10'),
                array(16, 'B', '11'),
                array(16, 'C', '12'),
                array(16, 'D', '13'),
                array(16, 'E', '14'),
                array(16, 'F', '15'),
                array(16, '11', '17'),
                array(16, '19', '25'),
                array(16, '1B', '27'),
                array(16, '1E', '30'),
                array(16, '1C', '28'),
                array(16, '1D', '29'),
                array(16, '22', '34'),
                array(16, '44', '68'),
                array(16, '99', '153'),
                array(16, '100', '256'),
                array(16, '101', '257'),
                array(16, '132', '306'),
                
                array(17, '2', '2'),
                array(17, '3', '3'),
                array(17, '4', '4'),
                array(17, 'D', '13'),
                array(17, 'F', '15'),
                array(17, 'G', '16'),
                array(17, '10', '17'),
                array(17, '1G', '33'),
                array(17, '1F', '32'),
                array(17, '3F', '66'),
                
                array(2, '0101010101', '341'),
                array(2, '1010101010', '682'),
                array(2, '1111111111', '1023'),
        );
    }
    
    public static function invalidDataProvider() {
        return array(
            array(16, 'G'),
            array(2, '10300'),
            array(4, '1A1'),
            array(16, 'QWE'),
            array(16, 'ASD'),    
        );
    }
    
    /**
     * @dataProvider validDataProvider
     */
    public function testShouldConvertHexToInt($base, $hex, $int) {
        $convertor = new Convertor($base);
        $actual =  $convertor->decode($hex);
        $this->assertEquals($int, $actual);
    }
    
    /**
     * @dataProvider validDataProvider
     */
    public function testShouldConvertIntToHex($base, $hex, $int) {
        $convertor = new Convertor($base);
        $actual =  $convertor->code($int);
        $this->assertEquals($hex, $actual);
    }
    
    /**
     * @dataProvider validDataProvider
     */
    public function testShouldValidateTrueIfValid($base, $hex) {
        $this->validate($base, $hex, true);
    }
    
    /**
     * @dataProvider invalidDataProvider
     */
    public function testShouldValidateFalseIfInvalid($base, $hex) {
        $this->validate($base, $hex, false);
    }
    
    private function validate($base, $hex, $isValid) {
        $convertor = new Convertor($base);
        $actual =  $convertor->isValid($hex);
        $this->assertEquals($isValid, $actual);
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsMoreThan17() {
        new Convertor(18);
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsLessThan2() {
        new Convertor(1);
    }
}
а вот что остается от стартого CalculatorTest
class CalculatorUnitTest extends PHPUnit_Framework_TestCase {
        
    private $Calculator;
    
    protected function setUp() {
        parent::setUp ();
        $this->Calculator = new Calculator();
    
    }
    
    protected function tearDown() {
        $this->Calculator = null;
        parent::tearDown ();
    }
    
    public function testShouldSumWhenXPlusY() {
        $this->assertEquals(444, $this->Calculator->calculate('123+321', 10));
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenInvalidExpression() {
        $this->Calculator->calculate('1', '10');
    }

    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenMoreThanOnePlus() {
        $this->Calculator->calculate('1++3', '10');
    }
        
}
[коммит]
 
Я его назвал CalculatorUnitTest, CalculatorTest оставил как есть (для сравнения, на реально проекте я бы удалил интеграционный). Мне в юнит тест для калькулятора предстоит еще добавить несколько тестов, чтобы покрытие калькулятора было максимальным. Тесты не на состояние а на поведение - мне интересно как общается калькулятор с конвертером, и тут без моков нам не обойтись....

Давай сделаем шажок немножечко побольше (рискнем) и провернем сразу несколько операций. Чтобы продемонстрировать что изменилось, покажу diff с помощью тортилки

1) выделим из Calculator зависимость от Converter (будем ее через конструктор инджектить). А так как $base - свойство Converter то из метода calculate так же убираем $base. 


Это приведет к тому, что нам надо будет исправить интеграционный тест (Calculator + Convertor). Напомню его я держу для сравнения, в реальном мире я бы его удалил где-то сейчас (или чуть раньше).


Это безобразно - снова купа дублирования, потому как сетап уже нам не поможет ($base у разных тестов разный и теперь он задается в конструкторе). Но еще можно заметить, что в некоторых тестах мы явно тестируем Convetror и не место тут этим тестам - в общем как ни крути интеграционный тест чем дальше тем больше путает...


2) А что касается юнит теста, то тут все более менее красиво. Настроили в сетапе мок, 

 Сконфигурировали в тесте моки с помощью методов should...... (я повуделял их для лучшей читабельности теста). И усе!
[коммит] 





Пару линков:
Хорошая статья по мокам.
И репозиторий, если удруг понадобится.


Дальше сделаем сумматор римских чисел. Тут чисстое ТДД, нас на секундочку больше не интересует калькулятор - мы просто переименуем Convertor на HexConvertor, и начнем разрабатывать новый класс RymConvertor. Но это уже в следующий раз.... 

понедельник, 19 марта 2012 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #6

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

Итак исходники качаем тут.

Начнем как всегда с тестов.

package container;
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;

import container.ioc.*;
import container.flashlight.*;
import container.battery.*;

public class TestBaterry {

    private Container container;
    
    @Before 
    public void initContainer() {
        // обрати внимание перед каждым тестом проинитится контейнер, которому мы сообщили, что 
        // заместь батарейки используй ChinaBattery а вместо фонарика SomeFlashlight
        container = new ContainerImpl(
                 Binder.use(ChinaBattery.class).as(Battery.class), 
                 Binder.use(SomeFlashlight.class).as(Flashlight.class));
    }
    
    @Test
    public void testDischargeNewBattery() {    
        // таким нехитрым способом по интерфейсу мы получаем реализацию
        // контейнер пройдется по всем полям новосозданного объекта и 
        // если там найдет знакомые типы то вставит в них реализации, 
        // в соответствии с настройками, которые мы указали
        Flashlight flashlight = container.get(Flashlight.class);        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        for (int count = 0; count < 1000; count ++) {
            flashlight.swithOff();                            
            flashlight.swithOn();                    
        }
        
        flashlight.swithOn();
        assertFalse(flashlight.isShines());        
    }
    
    @Test
    public void testBadBattery() {                                
        Battery battery = new Battery(){
            @Override
            public boolean getVoltage() {            
                return false;
            }
        };
        
        // а вот так мы вдруг можем передумать и переопределить настройки контейнера
        container.update(Binder.use(battery).as(Battery.class));
        
        Flashlight flashlight = container.get(Flashlight.class);        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertFalse(flashlight.isShines());            
    }
        
    @Test
    public void testNoGetPowerIfDoubleSwithOn() {                        
        Flashlight flashlight = container.get(Flashlight.class);    
        assertFalse(flashlight.isShines());
        
        for (int count = 0; count < 1000; count ++) {                    
            flashlight.swithOn();                    
        }
        
        assertTrue(flashlight.isShines());            
    }    
    
    @Test 
    public void testNoBatteryNoLight() {        
        container.remove(Battery.class);
        
        Flashlight flashlight = container.get(Flashlight.class);
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertFalse(flashlight.isShines());    
    }

    @Test
    public void integrationTestGetPowerFormNewChinaBattery() {                
        Flashlight flashlight = container.get(Flashlight.class);    
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertTrue(flashlight.isShines());    
    }  
}

Именно так я и написал код, когда я начал разработку этого примера. Никаких классов контейнера естественно не было, и тест не компилился. Но я создал пустые классы с помощью IDE (Ctrl1-1 в Eclipse или Alt-Enter в Idea).

Вот он код пустой
package container.ioc;

public class Binder {    

    public static Binder use(Class<?> classToCreate) {
        return null;
    }
    
    public static Binder use(Object object) {
        return null;
    }

    public Binder as(Class<?> interfaceClass) {
        return null;
    }    
}

package container.ioc;

public interface Container {

    <T> T get(Class<T> interfaceClass);

    void remove(Class<?> clazz);

    void update(Binder binder);
}

package container.ioc;

public class ContainerImpl implements Container {

    public ContainerImpl(Binder...binders) {    
    }
    
    @Override
    public <T> T get(Class<T> interfaceClass) {
        return null;
    }

    @Override
    public void remove(Class<?> clazz) {        
    }

    @Override
    public void update(Binder binder) {
    }
}

Так теперь код хоть компилится! :) После того как была создана интерфейсная часть, я взялся за реализацию с помощью TDD.

Вот те тестовые сценарии, которые я один за другим написал и реализовал. Кода очень много :) потому что должен был проверить всевозможные случаи инъекции с разными полями, с одинаковыми полями, с наследниками, с реализациями, с примитивами и так далее. Если хочешь скипнуть - жми сюда.

Текста на самом деле много. А еще я все классы сделал Inner классами хотя в реальной версии они должны быть отдельными public классами иначе ничего не получится. Работа с иннераклассами у меня у туду.

package container2.ioc.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

import container.ioc.Binder;
import container.ioc.ContainerImpl;

public class IoCTest {
            
    interface Marker1 {
    }
    
    class Marker1Impl implements Marker1{
    }
    
    interface Marker2 {
    }
    
    class Marker2Impl implements Marker2 {
    }
    
    interface Marker3 {
    }
    
    class Marker3Impl implements Marker3 {
    }
    
    interface MainMarker {
    }
    
    private <T> T getImpl(Class<T> mainClass) {
        return (T)new ContainerImpl(
                Binder.use(Marker1Impl.class).as(Marker1.class), 
                Binder.use(Marker2Impl.class).as(Marker2.class),
                Binder.use(Marker3Impl.class).as(Marker3.class),                
                Binder.use(mainClass).as(MainMarker.class)).get(MainMarker.class);
    }
    
    class ClassWithDefaultConstructor implements MainMarker {
        Marker1 marker1; 
        
        public ClassWithDefaultConstructor() {
            
        }
    }
    
    @Test
    public void testWithDefaultConstructor() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithDefaultConstructor.class).marker1.getClass());            
    }
    
    class ClassWithoutAnyConstructors implements MainMarker {
        Marker1 marker1; 
        
        private ClassWithoutAnyConstructors() {        
        }
    }
    
    @Test
    public void testWithoutAnyConstructors() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithoutAnyConstructors.class).marker1.getClass());            
    }
    
    class ClassWithPrivateField implements MainMarker {
        private Marker1 marker1; 
        
        public ClassWithPrivateField() {}

        public Object getMarker1() {
            return marker1;
        }
    }
    
    @Test
    public void testWithPrivateField() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithPrivateField.class).getMarker1().getClass());            
    }
    
    class ClassWithInjectorConstructor implements MainMarker {
        private Marker1 marker1;
        
        public ClassWithInjectorConstructor(Marker1 marker1) {
            this.marker1 = marker1;
        }

        public Marker1 getMarker1() {
            return marker1;
        }            
    }
                
    @Test
    public void testWithInjectorConstructor() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithInjectorConstructor.class).getMarker1().getClass());            
    }
    
    class СlassWithInjectorConstructorAndTwoFields implements MainMarker {
        
        private Marker1 marker1;
        private Marker2 marker2;
        
        public СlassWithInjectorConstructorAndTwoFields(Marker2 marker2, Marker1 marker1) {
            this.marker1 = marker1;
            this.marker2 = marker2;
        }

        public Marker1 getMarker1() {
            return marker1;
        }
        
        public Marker2 getMarker2() {
            return marker2;
        }    
    
    }
    
    @Test
    public void testWithInjectorConstructorAndTwoFieldsCheckField1() {
        assertEquals(Marker1Impl.class, 
                getImpl(СlassWithInjectorConstructorAndTwoFields.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithInjectorConstructorAndTwoFieldsCheckField2() {
        assertEquals(Marker2Impl.class, 
                getImpl(СlassWithInjectorConstructorAndTwoFields.class).getMarker2().getClass());            
    }    
    
    class СlassWithPrivateInjectorConstructorAndTwoFields implements MainMarker {
        private Marker1 marker1;
        private Marker2 marker2;
        
        private СlassWithPrivateInjectorConstructorAndTwoFields(Marker2 marker2, Marker1 marker1) {
            this.marker1 = marker1;
            this.marker2 = marker2;
        }

        public Marker1 getMarker1() {
            return marker1;
        }
        
        public Marker2 getMarker2() {
            return marker2;
        }        
    }
    
    @Test
    public void testWithPrivateInjectorConstructorAndTwoFieldsCheckField1() {
        assertEquals(Marker1Impl.class, 
                getImpl(СlassWithPrivateInjectorConstructorAndTwoFields.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithPrivateInjectorConstructorAndTwoFieldsCheckField2() {
        assertEquals(Marker2Impl.class, 
                getImpl(СlassWithPrivateInjectorConstructorAndTwoFields.class).getMarker2().getClass());            
    }
    
    class ClassWithTwoConstructors implements MainMarker {
        private Marker1 marker1; 
        
        public ClassWithTwoConstructors() {
            
        }
        
        private ClassWithTwoConstructors(Marker1 marker) {
            this.marker1 = marker;
        }
        
        public Object getMarker1() {
            return marker1;
        }
    }
    
    @Test
    public void testWithTwoConstructors() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithTwoConstructors.class).getMarker1().getClass());            
    }
    
    class ClassWithTwoConstructorsWitnSameParametersCount implements MainMarker {
        private Marker1 marker1; 
            
        private ClassWithTwoConstructorsWitnSameParametersCount(Marker1 marker) {
            this.marker1 = marker;
        }
        
        private ClassWithTwoConstructorsWitnSameParametersCount(String string) {
        }
        
        public Object getMarker1() {
            return marker1;
        }
    }
    
    @Test
    public void testWithTwoConstructorsWithSameParametersCount() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithTwoConstructorsWitnSameParametersCount.class).getMarker1().getClass());            
    }
    
    class ClassWithTwoFieldsAndOnlyOneInjectedViaConstructor implements MainMarker {
        private Marker1 marker1; 
        private Marker2 marker2;
        private boolean costructorCall = false;
            
        private ClassWithTwoFieldsAndOnlyOneInjectedViaConstructor(Marker1 marker) {
            this.costructorCall = true;
            this.marker1 = marker;
        }
            
        public Object getMarker1() {
            return marker1;
        }
        
        public Object getMarker2() {
            return marker2;
        }

        public boolean isCostructorCall() {
            return costructorCall;
        }
    }
    
    @Test
    public void testWithTwoFieldsAndOnlyOneInjectedViaConstructor1() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithTwoFieldsAndOnlyOneInjectedViaConstructor.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithTwoFieldsAndOnlyOneInjectedViaConstructor2() {
        assertEquals(Marker2Impl.class, 
                getImpl(ClassWithTwoFieldsAndOnlyOneInjectedViaConstructor.class).getMarker2().getClass());            
    }
    
    @Test
    public void testWithTwoFieldsAndOnlyOneInjectedViaConstructor_constructorUsed() {
        assertTrue(getImpl(ClassWithTwoFieldsAndOnlyOneInjectedViaConstructor.class).isCostructorCall());            
    }
    
    class ClassWithTwoFieldsAndOtherOneInjectedViaConstructor implements MainMarker {
        private Marker1 marker1; 
        private Marker2 marker2;
        private boolean costructorCall = false;
            
        private ClassWithTwoFieldsAndOtherOneInjectedViaConstructor(Marker2 marker) {
            this.costructorCall = true;
            this.marker2 = marker;
        }
            
        public Object getMarker1() {
            return marker1;
        }
        
        public Object getMarker2() {
            return marker2;
        }

        public boolean isCostructorCall() {
            return costructorCall;
        }
    }
    
    @Test
    public void testWithTwoFieldsAndOtherOneInjectedViaConstructor_CheckField1() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithTwoFieldsAndOtherOneInjectedViaConstructor.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithTwoFieldsAndOtherOneInjectedViaConstructor_CheckField2() {
        assertEquals(Marker2Impl.class, 
                getImpl(ClassWithTwoFieldsAndOtherOneInjectedViaConstructor.class).getMarker2().getClass());            
    }
    
    @Test
    public void testWithTwoFieldsAndOtherOneInjectedViaConstructor_constructorUsed() {
        assertTrue(getImpl(ClassWithTwoFieldsAndOtherOneInjectedViaConstructor.class).isCostructorCall());            
    }
    
    class ClassWithThreeFieldsAndOnlyOneInjectedViaConstructor implements MainMarker {
        private Marker1 marker1; 
        private Marker2 marker2;
        private boolean costructorCall = false;
        private Marker3 marker3;
            
        private ClassWithThreeFieldsAndOnlyOneInjectedViaConstructor(Marker3 marker) {
            this.costructorCall = true;
            this.marker3 = marker;
        }
            
        public Object getMarker1() {
            return marker1;
        }
        
        public Object getMarker2() {
            return marker2;
        }

        public boolean isCostructorCall() {
            return costructorCall;
        }

        public Object getMarker3() {
            return marker3;
        }
    }
    
    @Test
    public void testWithThreeFieldsAndOtherOneInjectedViaConstructor_CheckField1() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithThreeFieldsAndOnlyOneInjectedViaConstructor.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndOtherOneInjectedViaConstructor_CheckField2() {
        assertEquals(Marker2Impl.class, 
                getImpl(ClassWithThreeFieldsAndOnlyOneInjectedViaConstructor.class).getMarker2().getClass());            
    }
    
    
    @Test
    public void testWithThreeFieldsAndOtherOneInjectedViaConstructor_CheckField3() {
        assertEquals(Marker3Impl.class, 
                getImpl(ClassWithThreeFieldsAndOnlyOneInjectedViaConstructor.class).getMarker3().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndOtherOneInjectedViaConstructor_constructorUsed() {
        assertTrue(getImpl(ClassWithThreeFieldsAndOnlyOneInjectedViaConstructor.class).isCostructorCall());            
    }
    
    class Marker1Fasade implements Marker1 {

        private Marker1 marker;

        public Marker1Fasade(Marker1 marker) {
            this.marker = marker; 
        }
        
        public Object getMarker() {
            return marker;
        }
        
    }
    
    class ClassWithTwoFieldsAndOnlyOneInjectedViaConstructorWithFasade implements MainMarker {
        private Marker1 marker1; 
        private Marker2 marker2;
            
        private ClassWithTwoFieldsAndOnlyOneInjectedViaConstructorWithFasade(Marker1 marker) {
            this.marker1 = new Marker1Fasade(marker);
        }
            
        public Object getMarker1() {
            return marker1;
        }
        
        public Object getMarker2() {
            return marker2;
        }
    }
    
    @Test
    public void testWithTwoFieldsAndOnlyOneInjectedViaConstructorWithFasade_checkThatNoReflectionInjectionIfConstructorInjected() {
        ClassWithTwoFieldsAndOnlyOneInjectedViaConstructorWithFasade impl = 
            getImpl(ClassWithTwoFieldsAndOnlyOneInjectedViaConstructorWithFasade.class);
        
        assertEquals(Marker1Fasade.class, impl.getMarker1().getClass());            
        assertEquals(Marker1Impl.class, ((Marker1Fasade)impl.getMarker1()).getMarker().getClass());
    }
    
    class ClassWithThreeFieldsAndTwoInjectedViaConstructor implements MainMarker {
        private Marker3 marker3;
        private boolean costructorCall = false;
        private Marker1 marker1; 
        private Marker2 marker2;
            
        private ClassWithThreeFieldsAndTwoInjectedViaConstructor(Marker3 marker3, Marker2 marker2) {
            this.costructorCall = true;
            this.marker3 = marker3;
            this.marker2 = marker2;
        }
            
        public Object getMarker1() {
            return marker1;
        }
        
        public Object getMarker2() {
            return marker2;
        }

        public boolean isCostructorCall() {
            return costructorCall;
        }

        public Object getMarker3() {
            return marker3;
        }
    }
    
    @Test
    public void testWithThreeFieldsAndTwoInjectedViaConstructor_CheckField1() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithThreeFieldsAndTwoInjectedViaConstructor.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndTwoInjectedViaConstructor_CheckField2() {
        assertEquals(Marker2Impl.class, 
                getImpl(ClassWithThreeFieldsAndTwoInjectedViaConstructor.class).getMarker2().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndTwoInjectedViaConstructor_CheckField3() {
        assertEquals(Marker3Impl.class, 
                getImpl(ClassWithThreeFieldsAndTwoInjectedViaConstructor.class).getMarker3().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndTwoInjectedViaConstructor_constructorUsed() {
        assertTrue(getImpl(ClassWithThreeFieldsAndTwoInjectedViaConstructor.class).isCostructorCall());            
    }    
    
    class Marker3Fasade implements Marker3 {

        private Marker3 marker;

        public Marker3Fasade(Marker3 marker) {
            this.marker = marker; 
        }
        
        public Object getMarker() {
            return marker;
        }    
    }
    
    class ClassWithThreeFieldsAndTwoConstructors implements MainMarker {
        private Marker3 marker3;
        private boolean isCommonCostructorCall = false;
        private Marker1 marker1; 
        private Marker2 marker2;
        
        private ClassWithThreeFieldsAndTwoConstructors(Marker1 marker1) {
            this.marker1 = marker1;
        }
        
        private ClassWithThreeFieldsAndTwoConstructors(Marker3 marker3, Marker2 marker2) {
            this.isCommonCostructorCall = true;
            this.marker3 = new Marker3Fasade(marker3);
            this.marker2 = marker2;
        }
            
        public Object getMarker1() {
            return marker1;
        }
        
        public Object getMarker2() {
            return marker2;
        }

        public boolean isCommonCostructorCall() {
            return isCommonCostructorCall;
        }
        
        public Object getMarker3() {
            return marker3;
        }
    }
    
    @Test
    public void testWithThreeFieldsAndTwoConstructors_CheckField1() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithThreeFieldsAndTwoConstructors.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndTwoConstructors_CheckField2() {
        assertEquals(Marker2Impl.class, 
                getImpl(ClassWithThreeFieldsAndTwoConstructors.class).getMarker2().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndTwoConstructors_CheckField3() {        
        ClassWithThreeFieldsAndTwoConstructors impl = 
            getImpl(ClassWithThreeFieldsAndTwoConstructors.class);
        
        assertEquals(Marker3Fasade.class, impl.getMarker3().getClass());            
        assertEquals(Marker3Impl.class, ((Marker3Fasade)impl.getMarker3()).getMarker().getClass());        
    }
    
    @Test
    public void testWithThreeFieldsAndTwoConstructors_constructorUsed() {
        assertTrue(getImpl(ClassWithThreeFieldsAndTwoConstructors.class).isCommonCostructorCall());            
    }    
    
    class ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed implements MainMarker {
        private Marker3 marker3;
        private boolean isCommonCostructorCall = false;
        private Marker1 marker1; 
        private Marker2 marker2;
        
        private ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed(Marker1 marker1) {
            this.marker1 = marker1;
        }
        
        private ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed(Marker3 marker3, Marker2 marker2) {
            this.isCommonCostructorCall = true;
            this.marker3 = new Marker3Fasade(marker3);
            this.marker2 = marker2;
        }
        
        private ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed(Marker3 marker3, Marker2 marker2, String bla) {
            this.marker3 = marker3;
            this.marker2 = marker2;
        }
            
        public Object getMarker1() {
            return marker1;
        }
        
        public Object getMarker2() {
            return marker2;
        }

        public boolean isCommonCostructorCall() {
            return isCommonCostructorCall;
        }
        
        public Object getMarker3() {
            return marker3;
        }
    }
    
    @Test
    public void testWithThreeFieldsAndThreeConstructorsOneIsNotUsed_CheckField1() {
        assertEquals(Marker1Impl.class, 
                getImpl(ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed.class).getMarker1().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndThreeConstructorsOneIsNotUsed_CheckField2() {
        assertEquals(Marker2Impl.class, 
                getImpl(ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed.class).getMarker2().getClass());            
    }
    
    @Test
    public void testWithThreeFieldsAndThreeConstructorsOneIsNotUsed_CheckField3() {        
        ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed impl = 
            getImpl(ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed.class);
        
        assertEquals(Marker3Fasade.class, impl.getMarker3().getClass());            
        assertEquals(Marker3Impl.class, ((Marker3Fasade)impl.getMarker3()).getMarker().getClass());        
    }
    
    @Test
    public void testWithThreeFieldsAndThreeConstructorsOneIsNotUsed_constructorUsed() {
        assertTrue(getImpl(ClassWithThreeFieldsAndThreeConstructorsOneIsNotUsed.class).isCommonCostructorCall());            
    }
}

Постепенно появлялась реализация. После несокльких подходов к коду я родил вот эти реализации

package container.ioc;

public class Binder {

    Class<?> interfaceClass;
    Class<?> classToCreate;
    Object object;

    public Binder(Class<?> classToCreate) {
        this.classToCreate = classToCreate;
    }

    public Binder(Object object) {
        this.object = object;
    }

    public static Binder use(Class<?> classToCreate) {
        return new Binder(classToCreate);
    }
    
    public static Binder use(Object object) {
        return new Binder(object);
    }

    public Binder as(Class<?> interfaceClass) {
        this.interfaceClass = interfaceClass;
        return this;
    }
    
}

package container.ioc;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

public class ContainerImpl implements Container {

    private Collection<Binder> binders;

    public ContainerImpl(Binder...binders) {
        this.binders = new LinkedList<Binder>(Arrays.asList(binders));
    }
    
    @Override
    public <T> T get(Class<T> interfaceClass) {
        Binder binder = find(interfaceClass);
        
        if (binder.object != null) {
            return (T) binder.object;            
        }
        
        Class<?> classToCreate = binder.classToCreate;
        
        List<Constructor<?>> constructors = getCostructors(classToCreate);
        
        List<Object> dependencies = getObjectsFor(onlyInterfaceTypes(getTypes(getFields(classToCreate))));                                                    
                
        return (T) foundCommonConstructor(constructors, dependencies).newInstanceFor(dependencies);                                     
    }

    private InstanceMaker foundCommonConstructor(
            List<Constructor<?>> constructors, List<Object> dependencies) 
    {
        sortByParameterCount(constructors);
        
        for (Constructor<?> constructor : constructors) {
            if (sufficiently(constructor.getParameterTypes(), dependencies)) {
                return new InstanceMaker(constructor); 
            }
        }        
        
        throw new RuntimeException("Constructor not found");
    }

    private void sortByParameterCount(List<Constructor<?>> constructors) {
        Collections.sort(constructors, new Comparator<Constructor<?>>() {
            @Override
            public int compare(Constructor<?> constructor1, Constructor<?> constructor2) {
                return constructor2.getParameterTypes().length - 
                    constructor1.getParameterTypes().length;
            }                
        });
    }    

    private boolean sufficiently(Class<?>[] parameterTypes, List<Object> dependencies) {        
        for (Class<?> clazz : parameterTypes) {
            if (notIn(clazz, dependencies)) {
                return false;
            }
        }
        return true;        
    }

    private boolean notIn(Class<?> clazz, List<Object> dependencies) {
        for (Object dependency : dependencies) {
            if (clazz.isAssignableFrom(dependency.getClass())) {
                return false;
            }
        }
        return true;
    }

    private List<Class<?>> getTypes(Collection<Field> fields) {
        List<Class<?>> result = new LinkedList<Class<?>>();
        for (Field field : fields) {            
            result.add(field.getType());
        }
        return result;
    }
    
    private List<Class<?>> onlyInterfaceTypes(List<Class<?>> classes) {
        List<Class<?>> result = new LinkedList<Class<?>>();
        for (Class<?> clazz : classes) {
            
            try {
                find(clazz);
                result.add(clazz);
            } catch (RuntimeException e) {
                continue;
            }
        }
        return result;                
    }
    
    private Binder find(Class<?> interfaceClass) {
        for (Binder binder : binders) {
            if (binder.interfaceClass.equals(interfaceClass)) {
                return binder;
            }
        }
        throw new RuntimeException("Dependency class not found for interface" + interfaceClass.getName());
    }

    private List<Object> getObjectsFor(List<Class<?>> parameterTypes) {
        List<Object> result = new LinkedList<Object>();
        for (Class<?> clazz : parameterTypes) {
            result.add(get(clazz));
        }
        return result;        
    }
    
    private List<Constructor<?>> getCostructors(Class<?> classToCreate) {
        return Arrays.asList(classToCreate.getDeclaredConstructors());        
    }
    
    private Collection<Field> getFields(Class<?> classToCreate) {
        return Arrays.asList(classToCreate.getDeclaredFields());
    }

    @Override
    public void remove(Class<?> clazz) {
        for (Binder binder : binders) {
            if (binder.interfaceClass.equals(clazz)) {
                binders.remove(binder);
                return;
            }
        }        
    }

    @Override
    public void update(Binder binder) {
        remove(binder.interfaceClass);
        binders.add(binder);
    }
}

package container.ioc;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList;
import java.util.List;

public class InstanceMaker {
    private Constructor<?> constructor;
    
    public InstanceMaker(Constructor<?> constructor) {
        this.constructor = constructor;
    }
    
    public Object newInstanceFor(List<Object> dependencies) {
        List<Object> foundDependencies = getDependenciesFor(constructor, dependencies);
        Object object = getObject(constructor, foundDependencies);            
        dependencies.removeAll(foundDependencies);
            
        return injectToField(object, dependencies);
    }
    
    private List<Object> getDependenciesFor(Constructor<?> constructor, List<Object> dependencies) {
        List<Object> result = new LinkedList<Object> ();
        for (Class<?> parameterType : constructor.getParameterTypes()) {
            for (Object dependency : dependencies) {
                if (parameterType.isAssignableFrom(dependency.getClass())) { 
                    result.add(dependency);
                    break; 
                }
            }
        }
        return result;
    }
        
    private Object getObject(Constructor<?> constructor, List<Object> dependencies) {
        try {
            constructor.setAccessible(true);
            Object newInstance = constructor.newInstance(dependencies.toArray(new Object[0]));
            constructor.setAccessible(false);
            
            return newInstance;
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
    
    private <T> T injectToField(T object, List<Object> dependencies) {
        for (Object dependency : dependencies) {
            Injector.injectAll(object, dependency);
        }
        return object;
    }
}

Классы достаточно сложновастые, но рефлексия все же. Библиотеку Fest Reflection я не хотел подключать из за лишней jar-dependency. В следующий раз попробую сделаю с ней - за одно попиарю этот классный инструмент для работы с рефлексией.

В будущих планах есть сделать то же но со Spring IoC. Ждите, продолжение следует..

понедельник, 10 октября 2011 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #5

В прошлом посту мы использовали простую фабрику для создания фонарика с уже вставленными батарейками.

Рассмотрим теперь вариант legаcy code, когда у нас в конструкторе фонарика захардкоджен тип батарейки. Очень неприятный момент потому как классы сцеплены между собой жестко. Усугубим его тем фактом, что менять фонарик нельзя (маленькие рефакторинги допустимы, но как и раньше конструктор фонарика должен в себе содержать вызов конструктора батарейки). Заданием нашим будет проверить этот самый фонарик в отрыве от батарейки которую он упорно инстанциирует.

Вот фонарик.

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
    
    public SomeFlashlight() {
        this.swithOn = false;
        this.battery = new ChinaBattery(); // обратите внимание на эту наглость!
    }
    
    @Override
    public boolean isShines() {
        return (battery != null) && swithOn;
    }

    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();             
        }            
    }

    @Override
    public void swithOff() {
        swithOn = false;
    }

}

Как его протестировать со другой батарейкой не добавляя никаких конструкторов, не нарушая инкапсуляцию объекта, изменением модификатора доступа поля battery?

Надо локализировать вызов конструктора ChinaBattery в отдельном методе, который позже переопределить в наследнике. Выглядит это так.

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
    
    public SomeFlashlight() {
        this.swithOn = false;
        this.battery = getBattery(); // вот отсюда ...
    }

    // ... вот сюда инкапсулировали мы зависимость.  
    // protected для того, чтобы можно было субклассировать и переопределить этот метод
    protected Battery getBattery() {
        return new ChinaBattery();
    }
    
...

А переопределять будем уже в тесте вот так

public class TestBaterry {

...

    // внимание! это поле теста
    private Battery battery;
 
    // субклассируем SomeFlashlight и переопределяем в нем getBattery
    class MockedSomeFlashlight extends SomeFlashlight {
     
        @Override
        protected Battery getBattery() {
            return battery;
        }
    }
 
    @Test
    public void testDischargeNewBattery() {
        // устанавливаем себе батарейку такую как захотим
        this.battery = new DisposableBattery();

        // и инстанциируем наш субкласс
        MockedSomeFlashlight flashlight = new MockedSomeFlashlight();
        assertFalse(flashlight.isShines());

        flashlight.swithOn();
        assertTrue(flashlight.isShines());

        flashlight.swithOff();
        assertFalse(flashlight.isShines());

        flashlight.swithOn();
        assertFalse(flashlight.isShines());
    }

...

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

Исходники этого примера можно качнуть тут.

суббота, 23 июля 2011 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #4

На этот раз воспользуемся простой фабрикой для создания фонарика с уже вставленными батарейками. Напомню в прошлый раз мы научились использовать setter для инъекции.

Скажу сразу исходники этого примера можно качнуть тут.

Чтобы не дать возможность никому создавать фонарик мы сделаем его конструктор package-защищенным, так же как и сам класс.

package factory.flashlight;

import factory.battery.Battery;

// Класс защищен потому что мы не хотим, чтобы кто-то из другого пакета
// вызывал его. Только фабрика, которая находится в том же пакете. 
class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
    
    // Констурктор так же защищен. Береженого Бог бережет.  
    SomeFlashlight(Battery battery) {
        this.swithOn = false;
        this.battery = battery;
    }
    
    @Override
    public boolean isShines() {
        return (battery != null) && swithOn;
    }

    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();             
        }            
    }

    @Override
    public void swithOff() {
        swithOn = false;
    }
}

Интерфейс фонарика без изменений.

package factory.flashlight;

public interface Flashlight {

    void swithOn();
    
    void swithOff();

    boolean isShines();

}

Появился новый класс - фабрика, которая устанавливает в новый фонарик батпрейку.

package factory.flashlight;

import factory.battery.BatteryFactory;

public class FlashLightFactory {
    
    // простая фабрика знает, какой фонарик и какую батарейку использовать
    // но возвращает интерфейс!
    public Flashlight getFlashlight() {
        return new SomeFlashlight(new BatteryFactory().getBattery());        
    }
    
}

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

Интерфейс батарейки без изменений.

package factory.battery;

public interface Battery {

    boolean getVoltage();

}

А вот с созданием батарейки я учудил так же как и с фонариком. Есть фабрика батареек и только она знает, какие батарейки выпускает.

package factory.battery;

public class BatteryFactory {
    
    // Заметь, везде где только можно возвращается интерфейс!
    public Battery getBattery() {
        return new ChinaBattery();        
    }
    
}

А вот, собственно, и батарейка. Никто не узнает что в фонариках используется китайская батарейка - только батареечная фабрика. О фабрике батареек знает фабрика фонариков, но только о том, что она есть и может выпускать батарейки.

package factory.battery;

// класс скрыт в пакете
class ChinaBattery implements Battery {

    private int power = 5;     
    
    @Override
    public boolean getVoltage() {
        if (power > 0) {
            power--;
            return true;
        }
        
        return false;
    }

}

Посмотрим тесты:

package factory;
import static org.junit.Assert.*;

import org.junit.Test;

import factory.flashlight.FlashLightFactory;
import factory.flashlight.Flashlight;

public class TestBaterry {
            
    @Test
    public void testDischargeNewBattery() {                                
        Flashlight flashlight = new FlashLightFactory().getFlashlight();        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        for (int count = 0; count < 1000; count ++) {
            flashlight.swithOff();                            
            flashlight.swithOn();                    
        }
        
        flashlight.swithOn();
        assertFalse(flashlight.isShines());        
    }
        
    @Test
    public void testNoGetPowerIfDoubleSwithOn() {                        
        Flashlight flashlight = new FlashLightFactory().getFlashlight();    
        assertFalse(flashlight.isShines());
        
        for (int count = 0; count < 1000; count ++) {                    
            flashlight.swithOn();                    
        }
        
        assertTrue(flashlight.isShines());            
    }    

    @Test
    public void integrationTestGetPowerFormNewChinaBattery() {                
        Flashlight flashlight = new FlashLightFactory().getFlashlight();    
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertTrue(flashlight.isShines());    
    }
    
}
Т.к. теперь нет возможности получить фонарик без батарейки и извлечь батарейку, то и тестов поменьше будет. Мы пользуемся фабрикой чтобы получить фонарик, от вызова к вызову фонарик будет с заряженной батарейкой на борту. Вот и все пока. Дальше мы попробуем разорвать зависимость, если нам для этого не подходит ни DI через конструктор ни через setter. Читаем тут....

среда, 13 июля 2011 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #3

Привет!! Вчера я писал про инъекцию с помощью конструктора и, как результат, композицию из зависимого объекта и объекта, его использующего. Но фонарик мы привыкли видеть со съемными батарейками, а потому представляю слегка модифицированную версию вчерашнего кода. Итак инъекция посредством get'тера.

Скажу сразу исходники этого примера можно качнуть тут.

Начнем как всегда с интерфейсов. Описание батарейки - не менялся:

package getter.battery;

public interface Battery {

    boolean getVoltage();

}

Описание фонарика - добавили метод установки батарейки (setBattery):

package getter.flashlight;

import getter.battery.Battery;

public interface Flashlight {

    void swithOn();
    
    void swithOff();

    boolean isShines();

    void setBattery(Battery battery);

}

Дальше у нас пойдет китайская батарейка - она так же не менялась:

package getter.battery;

public class ChinaBattery implements Battery {

    private int power = 5; 
    
    @Override
    public boolean getVoltage() {
        if (power > 0) {
            power--;
            return true;
        }
        
        return false;
    }

}

Немного поменялся класс фонарика:

- Конструктор, принимающий батарейку, был удален. Напомню он выглядел так:

public SomeFlashlight(Battery battery) {
    this.battery = battery;
    this.swithOn = false;
}

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

public SomeFlashlight() {
    this.swithOn = false;
}

- Так же был добавлен один setter для установки батарейки на место. Метод определен в интерфейсе.

public void setBattery(Battery battery) {
    this.battery = battery;
}

Вот класс целиком и полностью:

package getter.flashlight;

import getter.battery.Battery;

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
    
    public SomeFlashlight() {
        this.swithOn = false;
    }
    
    public void setBattery(Battery battery) {
        this.battery = battery;
    }

    @Override
    public boolean isShines() {
        return (battery != null) && swithOn;
    }

    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();             
        }            
    }

    @Override
    public void swithOff() {
        swithOn = false;
    }

}

А вот и тесты. Они так же поменялись - как минимум был добавлен тест на проверку "а что, если во время работы батарейки нагло извлекли из фонарика?"

package getter;
import static org.junit.Assert.*;
import getter.battery.Battery;
import getter.battery.ChinaBattery;
import getter.flashlight.Flashlight;
import getter.flashlight.SomeFlashlight;

import org.junit.Test;

public class TestBaterry {
        
    class DisposableBattery implements Battery{

        private boolean full = true;
        
        @Override
        public boolean getVoltage() {            
            if (full) {
                full = false;
                return true;
            }
            return false;
        }
    }
    
    @Test
    public void testDischargeNewBattery() {                        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.swithOff();        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertFalse(flashlight.isShines());        
    }
    
    @Test
    public void testChangeDischargedBattery() {                        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.swithOff();        
        assertFalse(flashlight.isShines());
        
        flashlight.setBattery(new DisposableBattery());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());        
    }
    
    @Test
    public void testBadBattery() {                        
        Battery battery = new Battery(){
            @Override
            public boolean getVoltage() {            
                return false;
            }
        };
        
        Flashlight flashlight = new SomeFlashlight();        
        flashlight.setBattery(battery);
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertFalse(flashlight.isShines());            
    }
    
    @Test
    public void testNoGetPowerIfDoubleSwithOn() {                        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());            
    }
            
    @Test 
    public void testNoBatteryNoLight() {        
        Flashlight flashlight = new SomeFlashlight();
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertFalse(flashlight.isShines());    
    }
    
    @Test 
    public void testRemoveBatteryFromFlashlightDurringLightOn() {        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
                
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.setBattery(null);
        assertFalse(flashlight.isShines());
    }
    
    @Test
    public void integrationTestGetPowerFormNewChinaBattery() {                
        Battery battery = new ChinaBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertTrue(flashlight.isShines());    
    }
    
}

Многа букоф :)

Глядя на тесты (как документацию), можно заметить что изначально фонарик не рабочий

Flashlight flashlight = new SomeFlashlight();      
assertFalse(flashlight.isShines());

А еще если батарейки высунуть, то он перестанет светиться

assertTrue(flashlight.isShines());        
flashlight.setBattery(null);
assertFalse(flashlight.isShines());

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

Battery battery = ...        
Flashlight flashlight = new SomeFlashlight();
flashlight.setBattery(battery);

Мораль. Если нам нужна несколько большая гибкость (внимание! тут и ошибок больше можно допустить) то мы вместо композиции используем более общий случай - агрегацию.

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

Возвращаясь к фонарикам: обычный фонарик со сменными батарейками - агрегация; светодиодный фонарик, который встроен в зажигалку - композиция.

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

вторник, 12 июля 2011 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #2

Привет. Уже давненько я писал первую часть поста, видимо пришло время перейти к практической части.

Рассматривать будем 4 случая инъекции:
- инъекция через конструктор
- инъекция через setter
- подготовка объекта с помощью простой фабрики
- подготовка объекта с помощью IoC контейнера написанного собственноручно

Действующие лица:
- какой-то фонарик - class SomeFlashlight
- описание фонарика - integface Flashlight
- описание батарейки - interface Battery
- китайская батарейка - class ChinaBattery

Скажу сразу исходники этого примера можно качнуть тут.

Батарейка - с ней все просто, она дает некоторое напряжение (true) или не дает его (false), если севшая.

package constructor.battery;

public interface Battery {

    boolean getVoltage();

}

По задумке батарейка разряжается от последовательных вызовов ее метода getVoltage, что реализовано в китайской батарейке

package constructor.battery;

public class ChinaBattery implements Battery {

    private int power = 5; 
 
    @Override
    public boolean getVoltage() {
        if (power > 0) {
            power--;
            return true;
        }
        return false;
    }

}

Так как она китайская, то садится за 5 раз.

Идем дальше - интерфейс фонарик

package constructor.flashlight;

public interface Flashlight {

    void swithOn();
 
    void swithOff();

    boolean isShines();

}

Фонарик может включаться (swithOn) и выключаться (swithOff) а светится он или нет, мы узнаем с помощью метода isShines, возвращающего true/false.

Если посмотреть на реализацию, то станет видно, что свет горит (isShines = true) только, если батарейка есть + если она дает заряд. При включении с батарейки снимается заряд, что равносильно ее разрядке, если вспомнить реализацию китайской одноразки.

package constructor.flashlight;

import constructor.battery.Battery;

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
 
    public SomeFlashlight(Battery battery) {
        this.battery = battery;
        this.swithOn = false;
    }

    @Override
    public boolean isShines() {
        return (battery != null) && swithOn;
    }

    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();    
        }   
    }

    @Override
    public void swithOff() {
        swithOn = false;
    }

}

Вот тест, демонстрирующий работу фонарика

package constructor;

import static org.junit.Assert.*;
import constructor.battery.Battery;
import constructor.battery.ChinaBattery;
import constructor.flashlight.Flashlight;
import constructor.flashlight.SomeFlashlight;

import org.junit.Test;

public class TestBaterry {
  
    class DisposableBattery implements Battery{

        private boolean full = true;
  
        @Override
        public boolean getVoltage() {   
            if (full) {
                full = false;
                return true;
            }
            return false;
        }
    }
 
    @Test
    public void testDischargeNewBattery() {      
        Battery battery = new DisposableBattery();
  
        Flashlight flashlight = new SomeFlashlight(battery);  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertTrue(flashlight.isShines());
  
        flashlight.swithOff();  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertFalse(flashlight.isShines());  
    }
 
    @Test
    public void testBadBattery() {      
        Battery battery = new Battery(){
            @Override
            public boolean getVoltage() {   
                 return false;
            }
        };
  
        Flashlight flashlight = new SomeFlashlight(battery);  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertFalse(flashlight.isShines());   
    }
 
    @Test
    public void testNoGetPowerIfDoubleSwithOn() {      
        Battery battery = new DisposableBattery();
  
        Flashlight flashlight = new SomeFlashlight(battery);  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertTrue(flashlight.isShines());
  
        flashlight.swithOn();  
        assertTrue(flashlight.isShines());   
    }
  
    @Test 
    public void testNoBatteryNoLight() {  
        Flashlight flashlight = new SomeFlashlight(null);
  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();
  
        assertFalse(flashlight.isShines()); 
    }
 
    @Test
    public void integrationTestGetPowerFormNewChinaBattery() {    
        Battery battery = new ChinaBattery();
        
        Flashlight flashlight = new SomeFlashlight(battery);
  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();
  
        assertTrue(flashlight.isShines()); 
    }
 
}

Если посмотреть на каждый тест, то тут что происходит
Battery battery = ...
Flashlight flashlight = new SomeFlashlight(battery);
...

То есть вставляется через конструктор какая-то батарейка. Внутри фонарика все равно какая это будет батарейка, лишь бы соответствовала интерфейсу

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    ...
 
    public SomeFlashlight(Battery battery) {
        this.battery = battery;
        ...
    }
    ...     

Конструктор фонарика мило подставит любую батарейку (которая, implements Battery) в свое поле. Это позволит в дальнейшем пользоваться из фонарика всеми методами, объявленными в интерфейсе Battery

public interface Battery {

    boolean getVoltage();

}

Вот так

public class SomeFlashlight implements Flashlight {
    ...
    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();    
        }   
    }
...

Любая батарейка (то есть любой класс, главное, чтобы implements Battery) может быть использована фонариком. Фонарик при включении (swithOn()) попросит у батарейки энергии (battery.getVoltage()).

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

И это первый вариант инъекции зависимости - через конструктор. Используется она тогда, когда фонарик не может существовать без батарейки. Представь себе такой одноразовый фонарик, в который вмонтирована батарейка и доступа к ней нет. Села батарейка - выкидываешь фонарик.

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

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

Но об этом в следующей серии "инъекция через setter"...

суббота, 30 октября 2010 г.

Java for fun: Spring IoC для бедных

Spring IoC контейнер для бедных:

/**
  * Метод служит для инъекции одного объекта в соответствующие поля другого. 
  * Все поля, которые могут принять этот объект да заполнятся им (окромя Object).    
  * @param object Кого укалываем. 
  * @param injectible Что вводим. 
  * @return вернули то, что дали на вход (удобства ради).
  */
 <T> T injectAll(T object, Object injectible);
Если надо это чудо - читаем дальше...

пятница, 6 августа 2010 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #1

Сегодня я общался с чудесным человеком, и имел честь ему рассказывать, как я понимаю эту всю хрень, чаще именуемую как dependency injection. Что такое Dependency injection? Как реализуется Dependency injection в Java? Что такое IoC контейнер (из Spring Framework)? Что такое Java Reflection? Почему шаблон проектирования Простая фабрика называется именно так, а не иначе? И много другого вы узнаете из этого поста. Пишу его, пока свежа память. Читаем дальше...