Dependency Injection, Dependency Inversion, and You

0f930e13633535c1c4041e95b8881308?s=47 Jeff Carouth
September 13, 2014

Dependency Injection, Dependency Inversion, and You

Poor application design can be an incredibly frustrating experience. Often we can attribute frustration in software projects to tight coupling and high interdependence between modules within the design. As developers, the Dependency Inversion Principle helps us design maintainable software by showing us the direction dependencies should flow. In this session we will look at the ideas of dependency injection and dependency inversion and how they can make your applications easier to work with for yourself and your colleagues. At the end of this session you will have a grasp on DI and even a taste of using modern PHP dependency injection containers correctly in your applications.

0f930e13633535c1c4041e95b8881308?s=128

Jeff Carouth

September 13, 2014
Tweet

Transcript

  1. 3.

    Past experience developing software shows us coupling is bad. But

    it also suggests coupling is unavoidable.
  2. 5.

    Dependencies are the other objects, resources, or functions any given

    object uses to accomplish its responsibility.
  3. 9.

    Common Dependencies • Infrastructure – e.g., Database, third party module

    • Utility objects – e.g., Logger • Environment – e.g., FileSystem, system clock
  4. 10.

    Common Dependencies • Infrastructure – e.g., Database, third party module

    • Utility objects – e.g., Logger • Environment – e.g., FileSystem, system clock • Static Method calls – e.g., DataAccess::saveUser($u)
  5. 11.

    class OrderProcessor { public function __construct() { $this->orderRepository = new

    MysqlOrderRepository(); $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }
  6. 16.

    Constructor Injection Dependencies are injected upon instantiation through the constructor.

    class Alpha { } class DependsOnAlpha { public function __construct(Alpha $a) { /*...*/ } }
  7. 17.

    Constructor Injection Makes dependencies explicit. You cannot modify the dependencies

    after instantiation. Discourages violation of the Single Responsibility Principle. You might end up with many constructor params. You could have dependencies stored in the object’s state which are not needed for all methods.
  8. 18.

    class Database { public function where($column, $value) { $query =

    "..."; return $this->connection->fetchAll($query); } } class UserService { public function __construct(Database $db) { $this->db = $db; } public function findByEmail($email) { $data = $this->db->where('email', $email); return new User($data); } }
  9. 19.

    Setter Injection Dependencies are injected through a setter (or using

    a public property if you like to be wild.) class Beta { } class DependsOnBeta { public function setBeta(Beta $b) { /*...*/ } }
  10. 20.

    Setter Injection Works well with optional dependencies for a class.

    Works well with optional dependencies for a class. Flexible across the object’s lifecycle. Can leads to invalid state because setters must be called in certain order.
  11. 21.

    class PaymentGateway { public function process(Payment $payment) { /*…*/ }

    } class OrderService { private $paymentGateway; public function setPaymentGateway(PaymentGateway $gateway) { $this->paymentGateway = $gateway; } public function charge(Order $order) { foreach ($order->getPayments() as $payment) { $this->paymentGateway->process($payment); } } }
  12. 22.

    Parameter “Injection” Dependencies are passed as parameters to specific methods

    which need them. class Gamma {} class Delta { public function dependsOnGamma(Gamma $c) { /*...*/ } }
  13. 23.

    Parameter “Injection” Does not clutter your object with refs to

    collaborators which are not needed. Almost everything else about it.
  14. 24.

    class EmailService { public function send() { /*…*/ } }

    class Order { public function complete(EmailService $mailer) { //... $mailer->send(new OrderCompleteMessage($this)); } }
  15. 26.
  16. 33.

    The power of a DiC comes in it’s ability to

    resolve specific dependencies and their dependencies at runtime.
  17. 35.

    Twittee class Container { protected $s=array(); function __set($k, $c) {

    $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } } A Dependency Injection Container in a Tweet http://twittee.org
  18. 37.

    class FizzBuzz { public function __construct(Fizz $fizz, Buzz $buzz) {

    $this->fizz = $fizz; $this->buzz = $buzz; } public function of($number) { if ($number % 3 == 0 && $number % 5 == 0) { return "{$this->fizz}{$this->buzz}"; } else if ($number % 3 == 0) { return "{$this->fizz}"; } else if ($number % 5 == 0) { return "{$this->buzz}"; } return "{$number}"; } }
  19. 38.

    class FizzBuzz { public function __construct(Fizz $fizz, Buzz $buzz) {

    $this->fizz = $fizz; $this->buzz = $buzz; } public function of($number) { if ($number % 3 == 0 && $number % 5 == 0) { return "{$this->fizz}{$this->buzz}"; } else if ($number % 3 == 0) { return "{$this->fizz}"; } else if ($number % 5 == 0) { return "{$this->buzz}"; } return "{$number}"; } }
  20. 39.

    class Fizz { public function __toString() { return "Fizz"; }

    } class Buzz { public function __toString() { return "Buzz"; } }
  21. 40.

    $fizzbuzz = new FizzBuzz(new Fizz(), new Buzz()); for ($n =

    1; $n <= 20; $n++) { print "FizzBuzz of {$n} is {$fizzbuzz->of($n)}" . PHP_EOL; } class Fizz { } class Buzz { } class FizzBuzz { public function __construct(Fizz $fizz, Buzz $buzz) { } public function of($number) { } }
  22. 41.

    FizzBuzz of 1 is 1 FizzBuzz of 2 is 2

    FizzBuzz of 3 is Fizz FizzBuzz of 4 is 4 FizzBuzz of 5 is Buzz FizzBuzz of 6 is Fizz FizzBuzz of 7 is 7 FizzBuzz of 8 is 8 FizzBuzz of 9 is Fizz FizzBuzz of 10 is Buzz FizzBuzz of 11 is 11 FizzBuzz of 12 is Fizz FizzBuzz of 13 is 13 FizzBuzz of 14 is 14 FizzBuzz of 15 is FizzBuzz FizzBuzz of 16 is 16 FizzBuzz of 17 is 17 FizzBuzz of 18 is Fizz FizzBuzz of 19 is 19 FizzBuzz of 20 is Buzz [jcarouth@fizzlabs] $ php fizzbuzz.php
  23. 43.

    class Fizz { } class Buzz { } class FizzBuzz

    { } class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } }
  24. 44.

    class Fizz { } class Buzz { } class FizzBuzz

    { } class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } } $container = new Container(); $container->fizz = function() { return new Fizz(); }; $container->buzz = function() { return new Buzz(); };
  25. 45.

    $fizzbuzz = new FizzBuzz($container->fizz, $container->buzz); for ($n = 1; $n

    <= 20; $n++) { print "FizzBuzz of {$n} is {$fizzbuzz->of($n)}" . PHP_EOL; } class Fizz { } class Buzz { } class FizzBuzz { } class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } } $container = new Container(); $container->fizz = function() { return new Fizz(); }; $container->buzz = function() { return new Buzz(); };
  26. 46.

    FizzBuzz of 1 is 1 FizzBuzz of 2 is 2

    FizzBuzz of 3 is Fizz FizzBuzz of 4 is 4 FizzBuzz of 5 is Buzz FizzBuzz of 6 is Fizz FizzBuzz of 7 is 7 FizzBuzz of 8 is 8 FizzBuzz of 9 is Fizz FizzBuzz of 10 is Buzz FizzBuzz of 11 is 11 FizzBuzz of 12 is Fizz FizzBuzz of 13 is 13 FizzBuzz of 14 is 14 FizzBuzz of 15 is FizzBuzz FizzBuzz of 16 is 16 FizzBuzz of 17 is 17 FizzBuzz of 18 is Fizz FizzBuzz of 19 is 19 FizzBuzz of 20 is Buzz [jcarouth@fizzlabs] $ php fizzbuzz.php
  27. 47.

    class Database { private $host; public function __construct(array $config) {

    $this->host = $config['host']; } } class OrderMapper { private $db; public function __construct(Database $db) { $this->db = $db; } }
  28. 48.

    class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $orderMapper = new OrderMapper( new Database(['host' => ‘localhost']) );
  29. 49.

    class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container();
  30. 50.

    class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; };
  31. 51.

    class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); };
  32. 52.

    class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); }; $orderMapper = new OrderMapper($di->database); var_dump($orderMapper);
  33. 53.

    class OrderMapper#12 (1) { private $db => class Database#13 (1)

    { private $host => string(9) "localhost" } } [jcarouth@fizzlabs] $ php ordermapperditwittee.php
  34. 54.

    class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); };
  35. 55.

    class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); }; $di->orderMapper = function($c) { return new OrderMapper($c->database); }; var_dump($di->orderMapper);
  36. 56.

    class OrderMapper#3 (1) { private $db => class Database#14 (1)

    { private $host => string(9) "localhost" } } [jcarouth@fizzlabs] $ php ordermapperditwittee.php
  37. 57.

    Aura.di A Dependency Injection Container in a little more than

    a Tweet https://github.com/auraphp/Aura.Di
  38. 58.

    class OrderProcessor { public function __construct() { $this->orderRepository = new

    MySqlOrderRepository(); $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }
  39. 59.

    namespace AwesomeCart; interface OrderRepository { public function save(Order $order); }

    class InMemoryOrderRepository implements OrderRepository {} Refactor: Extract Interface
  40. 60.

    namespace AwesomeCart; interface OrderRepository { public function save(Order $order); }

    class InMemoryOrderRepository implements OrderRepository {} class MySqlOrderRepository implements OrderRepository {} Refactor: Extract Interface
  41. 61.

    namespace AwesomeCart; class OrderProcessor { public function __construct(OrderRepository $orderRepository) {

    $this->orderRepository = $orderRepository; $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }
  42. 62.

    namespace AwesomeCart; class OrderProcessor { public function __construct(OrderRepository $orderRepository) {

    $this->orderRepository = $orderRepository; $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }
  43. 64.

    namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new

    Factory()); $di->params['AwesomeCart\OrderProcessor']['orderRepository'] = $di->lazyNew('AwesomeCart\InMemoryOrderRepository');
  44. 65.

    namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new

    Factory()); $di->params['AwesomeCart\OrderProcessor']['orderRepository'] = $di->lazyNew('AwesomeCard\InMemoryOrderRepository'); $di->set( 'order_processor', $di->lazyNew(‘AwesomeCart\OrderProcessor') );
  45. 66.

    namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new

    Factory()); $di->params['AwesomeCart\OrderProcessor']['orderRepository'] = $di->lazyNew('AwesomeCard\InMemoryOrderRepository'); $di->set( 'order_processor', $di->lazyNew(‘AwesomeCart\OrderProcessor') ); $orderProcessor = $di->get('order_processor');
  46. 67.

    Using a Container In a typical application you will use

    the container from within your “controllers” and use them to inject dependencies into your “models.”
  47. 68.

    <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $app

    = new \Slim\Slim(); $app->get('/', function() { $userRepository = new UserRepository(new PDO(/*…*/)); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  48. 69.

    <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $app

    = new \Slim\Slim(); $app->get('/', function() { $userRepository = new UserRepository(new PDO(/*…*/)); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  49. 70.

    <?php // config/dev.php use Aura\Di\Container; use Aura\Di\Factory; $container = new

    Container(new Factory()); $container->params['PDO'] = [ 'dsn' => 'mysql:dbname=my_app_dev;host=127.0.0.1', 'username' => 'my_app_user', 'passwd' => 'super_secret_passphrase', ]; $container->set('database', $container->lazyNew('PDO')); return $container;
  50. 71.

    <?php // config/dev.php use Aura\Di\Container; use Aura\Di\Factory; $container = new

    Container(new Factory()); $container->params['PDO'] = [ 'dsn' => 'mysql:dbname=my_app_dev;host=127.0.0.1', 'username' => 'my_app_user', 'passwd' => 'super_secret_passphrase', ]; $container->set('database', $container->lazyNew('PDO')); return $container;
  51. 72.

    <?php // config/dev.php use Aura\Di\Container; use Aura\Di\Factory; $container = new

    Container(new Factory()); $container->params['PDO'] = [ 'dsn' => 'mysql:dbname=my_app_dev;host=127.0.0.1', 'username' => 'my_app_user', 'passwd' => 'super_secret_passphrase', ]; $container->set('database', $container->lazyNew('PDO')); return $container;
  52. 73.

    <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $di

    = require_once __DIR__ . '/../config/dev.php'; $app = new \Slim\Slim(); $app->get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  53. 74.

    <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $di

    = require_once __DIR__ . '/../config/dev.php'; $app = new \Slim\Slim(); $app->get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  54. 75.

    <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $di

    = require_once __DIR__ . '/../config/dev.php'; $app = new \Slim\Slim(); $app->get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  55. 76.

    Beware Service Location “…please note that this package is intended

    for use as a dependency injection system, not as a service locator system. If you use it as a service locator, that's bad, and you should feel bad.” – Aura.DI Readme
  56. 77.

    class Foo { } class Bar { protected $foo; public

    function __construct(Container $c) { $this->foo = $c->foo; } } $c = new Container(); $c->foo = function() { return new Foo(); }; $bar = new Bar($c);
  57. 78.
  58. 80.

    Recap • Dependencies in code are unavoidable, but that doesn’t

    mean they need to be unmanageable. • Inverting dependencies is a way to create more flexible software.
  59. 81.

    Recap • Dependencies in code are unavoidable, but that doesn’t

    mean they need to be unmanageable. • Inverting dependencies is a way to create more flexible software. • DI containers are a helpful tool for maintaining single responsibility within objects.
  60. 82.
  61. 83.

    A testing Example This example shows why having dependencies hidden

    becomes problematic when using the code. To illustrate that point we will test a simple piece of functionality.
  62. 85.

    class GuestRepository { public function __construct() { $this->db = new

    \PDO('...'); } public function delete(Guest $guest) { $purchaseRepository = new PurchaseRepository(); $purchasesAssignedToGuest = $purchaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } $deleteQuery = "DELETE FROM guest WHERE id = :id"; $statement = $this->db->prepare($deleteQuery); $statement->execute(array('id' => $guest->id)); return $statement->rowCount() < 1; } }
  63. 86.

    class PurchaseRepository { public function __construct() { $this->db = new

    \PDO('...'); } public function findByGuest($guest) { $query = 'SELECT * FROM purchases WHERE assigned_to = :guest_id'; $statement = $this->db->prepare($query); $statement->execute(); $purchases = $statement->fetchAll(); $purchaseCollection = array(); foreach ($purchases $result) { $purchase = new Purchase(); $purchase = $this->mapDatabaseResultToPurchase($purchase, $result); $purchaseCollection[] = $purchase; } return $purchaseCollection; } }
  64. 88.

    class GuestRepositoryTest extends \PHPUnit_Framework_TestCase { public function testCannotDeleteGuestWhenAssignedToPurchases() { $sql

    = "INSERT INTO guests VALUES(1, 'Test User')"; $this->db->prepare($sql)->execute(); $sql = "INSERT INTO purchases VALUES(1, 1), (2, 1)"; $this->db->prepare($sql)->execute(); $guestToDelete = new Guest(); $guestToDelete->id = 1; $repository = new GuestRepository(); $this->setExpectedException('\Exception'); $repository->delete($guestToDelete); } }
  65. 89.

    class GuestRepository { public function __construct() { $this->db = new

    \PDO('...'); } public function delete(Guest $guest) { $purchaseRepository = new PurchaseRepository(); $purchasesAssignedToGuest = $purhaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } $deleteQuery = "DELETE FROM guest WHERE id = :id"; $statement = $this->db->prepare($deleteQuery); $statement->execute(array('id' => $guest->id)); return $statement->rowCount() < 1; } }
  66. 90.

    class GuestRepository { private $db; public function __construct(\PDO $db) {

    $this->db = $db; } public function delete(Guest $guest) {} }
  67. 91.

    class GuestRepository { private $db; public function __construct(\PDO $db) {

    $this->db = $db; } public function delete(Guest $guest) {} } $guestRepository = new GuestRepository(new \PDO('...')); $guest = new Guest(); $guest->id = 8945221; $guestRepository->delete($guest);
  68. 92.

    class GuestRepository { public function __construct(\PDO $db) { $this->db =

    $db; } public function delete(Guest $guest) { $purchaseRepository = new PurchaseRepository(); $purchasesAssignedToGuest = $purchaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } $deleteQuery = "DELETE FROM guest WHERE id = :id"; $statement = $this->db->prepare($deleteQuery); $statement->execute(array('id' => $guest->id)); return $statement->rowCount() < 1; } }
  69. 93.

    class GuestRepository { private $db; private $purchaseRepository; public function __construct(\PDO

    $db, PurchaseRepository $purchaseRepository) { $this->db = $db; $this->purchaseRepository = $purchaseRepository; } public function delete(Guest $guest) { $purchasesAssignedToGuest = $this->purchaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } //...snip... } }
  70. 94.

    $guestRepository = new GuestRepository(new \PDO('...'), new PurchaseRepository()); $guest = new

    Guest(); $guest->id = 8945221; $guestRepository->delete($guest);
  71. 95.

    class PurchaseRepository { public function __construct() { $this->db = new

    \PDO('...'); } public function findByGuest($guest) { $query = 'SELECT * FROM purchases WHERE assigned_to = :guest_id'; $statement = $this->db->prepare($query); $statement->execute(); $purchases = $statement->fetchAll(); $purchaseCollection = array(); foreach ($purchases $result) { $purchase = new Purchase(); $purchase = $this->mapDatabaseResultToPurchase($purchase, $result); $purchaseCollection[] = $purchase; } return $purchaseCollection; } }
  72. 96.

    class PurchaseRepository { public function __construct(\PDO $db) { $this->db =

    $db; } public function findByGuest($guest) { $query = 'SELECT * FROM purchases WHERE assigned_to = :guest_id'; $statement = $this->db->prepare($query); $statement->execute(); $purchases = $statement->fetchAll(); $purchaseCollection = array(); foreach ($purchases $result) { $purchase = new Purchase(); $purchase = $this->mapDatabaseResultToPurchase($purchase, $result); $purchaseCollection[] = $purchase; } return $purchaseCollection; } }
  73. 97.

    $db = new \PDO('...'); $guestRepository = new GuestRepository($db, new PurchaseRepository($db));

    $guest = new Guest(); $guest->id = 8945221; $guestRepository->delete($guest);
  74. 98.

    class GuestRepositoryTest extends \PHPUnit_Framework_TestCase { public function testCannotDeleteGuestWhenAssignedToPurchases() { $sql

    = "INSERT INTO guests VALUES(1, 'Test User')"; $this->db->prepare($sql)->execute(); $purchaseRepository = $this->getMockBuilder('PurchaseRepository') ->disableOriginalConstructor() ->getMock(); $purchaseRepository->expects($this->once()) ->method('') ->will(This->returnValue(array($this->getMock('Purchase')))); $guestToDelete = new Guest(); $guestToDelete->id = 1; $repository = new GuestRepository($this->db, $purchaseRepository); $this->setExpectedException('\Exception'); $repository->delete($guestToDelete); } }