[Pkg-owncloud-commits] [owncloud-doc] 56/270: add docs on container

David Prévot taffit at moszumanska.debian.org
Thu Jul 31 03:53:01 UTC 2014


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to branch master
in repository owncloud-doc.

commit d135210d157d7cb2a997edd3716ed21b397e39c3
Author: Bernhard Posselt <dev at bernhard-posselt.com>
Date:   Sun May 11 16:09:24 2014 +0200

    add docs on container
---
 developer_manual/app/container.rst               | 137 ++++++++++++--
 developer_manual/app/controllers.rst             |   2 +-
 developer_manual/app/routes.rst                  |   4 +-
 developer_manual/general/dependencyinjection.rst | 228 -----------------------
 developer_manual/general/index.rst               |   1 -
 5 files changed, 127 insertions(+), 245 deletions(-)

diff --git a/developer_manual/app/container.rst b/developer_manual/app/container.rst
index ccc4c57..5591988 100644
--- a/developer_manual/app/container.rst
+++ b/developer_manual/app/container.rst
@@ -1,14 +1,61 @@
+=========
 Container
 =========
 
 .. sectionauthor:: Bernhard Posselt <dev at bernhard-posselt.com>
 
-The App Framework assembles the application by using an Inversion of Control container which does :doc:`../general/dependencyinjection`. Dependency Injection helps you to create testable and maintainable code. For a very simple and good tutorial, watch the `Dependency Injection and the art of Services and Containers Tutorial on YouTube <http://www.youtube.com/watch?v=DcNtg4_i-2w>`_. A broader overview over how it works and what the benefits are can be seen on `Google's Clean Code Talks < [...]
+The App Framework assembles the application by using a container based on the software pattern `Dependency Injection <https://en.wikipedia.org/wiki/Dependency_injection>`_. This makes the code easier to test and thus easier to maintain.
+
+If you are unfamiliar with this pattern, watch the following videos:
+
+* `Dependency Injection and the art of Services and Containers Tutorial <http://www.youtube.com/watch?v=DcNtg4_i-2w>`_
+* `Google Clean Code Talks <http://www.youtube.com/watch?v=RlfLCWKxHJ0>`_
+
+Dependency Injection
+====================
+Dependency Injection sounds pretty complicated but it just means: Don't new dependencies in your constructor or methods but pass them in. So this:
+
+.. code-block:: php
+
+  <?php
+
+  // without dependency injection
+  class AuthorMapper {
 
-The container is configured in :file:`appinfo/application.php`.
+    private $db;
+  
+    public function __construct() {
+      $this->db = new Db();
+    }
 
+  }
 
-To add your own classes simply open the :file:`appinfo/application.php` and add a line like this to the constructor:
+would turn into this by using Dependency Injection:
+
+.. code-block:: php
+
+  <?php
+
+  // with dependency injection
+  class AuthorMapper {
+
+    private $db;
+  
+    public function __construct($db) {
+      $this->db = $db;
+    }
+
+  }
+
+
+Using a container
+=================
+Passing dependencies into the constructor rather than newing them in the constructor has the following drawback: Every line in the source code where **new AuthorMapper** is being used has to be changed, once a new constructor argument is being added to it. 
+
+The solution for this particular problem is to limit the **new AuthorMapper** to one file, the container. The container contains all the factories for creating these objects and is configured in :file:`appinfo/application.php`.
+
+
+To add the app's classes simply open the :file:`appinfo/application.php` use the **registerService** method on the container object:
 
 .. code-block:: php
 
@@ -18,8 +65,9 @@ To add your own classes simply open the :file:`appinfo/application.php` and add
 
   use \OCP\AppFramework\App;
 
-  use \OCA\MyApp\Controller\PageController;
-
+  use \OCA\MyApp\Controller\AuthorController;
+  use \OCA\MyApp\Service\AuthorService;
+  use \OCA\MyApp\Db\AuthorMapper;
 
   class MyApp extends App {
 
@@ -35,20 +83,83 @@ To add your own classes simply open the :file:`appinfo/application.php` and add
       /**
        * Controllers
        */
-      $container->registerService('PageController', function($c){
-        return new PageController(
+      $container->registerService('AuthorController', function($c){
+        return new AuthorController(
           $c->query('AppName'),
-          $c->query('ServerContainer')->getRequest()
+          $c->query('ServerContainer')->getRequest(),
+          $c->query('AuthorService')
+        );
+      });
+
+      /**
+       * Services
+       */
+      $container->registerService('AuthorService', function($c){
+        return new AuthorService(
+          $c->query('AuthorMapper')
+        );
+      });
+
+      /**
+       * Services
+       */
+      $container->registerService('AuthorMappers', function($c){
+        return new AuthorService(
+          $c->query('ServerContainer')->getDb()
         );
       });
     }
   }
 
+How the container works
+=======================
+
+The container works in the following way:
+
+* :doc:`A request comes in and is matched against a route <request>` (for the AuthorController in this case)
+* The matched route queries **AuthorController** service form the container::
+        
+    return new AuthorController(
+      $c->query('AppName'),
+      $c->query('ServerContainer')->getRequest(),
+      $c->query('AuthorService')
+    );
+
+* The **AppName** is queried and returned from the baseclass
+* The **Request** is queried and returned from the server container
+* **AuthorService** is queried::
+
+    $container->registerService('AuthorService', function($c){
+      return new AuthorService(
+        $c->query('AuthorMapper')
+      );
+    });
+
+* **AuthorMapper** is queried::
+
+    $container->registerService('AuthorMappers', function($c){
+      return new AuthorService(
+        $c->query('ServerContainer')->getDb()
+      );
+    });
+
+* The **database connection** is returned from the server container
+* Now **AuthorMapper** has all of its dependencies and the object is being returned
+* **AuthorService** gets the **AuthorMapper** and returns the object
+* **AuthorController** gets the **AuthorService** and finally the controller can be newed and the object is being returned
+
+So basically the container is used as a giant factory to build all the classes that are needed for the application. Because it centralizes all the creation of objects (the **new Class()** lines), it is very easy to add new constructor parameters without breaking existing code: only the **__construct** method and the container line where the **new** is being called need to be changed.
+
+Which classes should be added
+=============================
+In general all of the app's controllers need to be registered inside the container. Then following question is: What goes into the constructor of the controller? Pass everything into the controller constructor that is responsible matches one of the following criteria:
 
-Service provided by core
-========================
-Core provides some predefined services that can be injected into your app. Every service is available from querying the **ServerContainer**::
+* It does I/O (database, write/read to files)
+* It is a global (eg. $_POST, etc. This is in the request class btw)
+* The output does not depend on the input variables (also called `impure function <http://en.wikipedia.org/wiki/Pure_function>`_), e.g. time, random number generator
+* It is a service, basically it would make sense to swap it out for a different object
 
-  $server = $c->query('ServerContainer')
-  $server->getRequest()  // get the request
+What not to inject:
 
+* It is pure data and has methods that only act upon it (arrays, data objects)
+* It is a `pure function <http://en.wikipedia.org/wiki/Pure_function>`_
\ No newline at end of file
diff --git a/developer_manual/app/controllers.rst b/developer_manual/app/controllers.rst
index 334ded5..f88877e 100644
--- a/developer_manual/app/controllers.rst
+++ b/developer_manual/app/controllers.rst
@@ -52,7 +52,7 @@ To connect a controller and a route the controller has to be registered in the :
             $container->registerService('AuthorApiController', function($c) {
                 return new AuthorApiController(
                     $c->query('AppName'), 
-                    $c->query('Request')
+                    $c->query('ServerContainer')->getRequest()
                 );
             });
         }
diff --git a/developer_manual/app/routes.rst b/developer_manual/app/routes.rst
index 8df1beb..f387ee6 100644
--- a/developer_manual/app/routes.rst
+++ b/developer_manual/app/routes.rst
@@ -47,7 +47,7 @@ The route array contains the following parts:
                 $container->registerService('PageController', function($c) {
                     return new PageController(
                         $c->query('AppName'), 
-                        $c->query('Request')
+                        $c->query('ServerContainer')->getRequest()
                     );
                 });
             }
@@ -177,7 +177,7 @@ Sometimes its useful to turn a route into a URL to make the code independent fro
             $container->registerService('PageController', function($c) {
                 return new PageController(
                     $c->query('AppName'), 
-                    $c->query('Request'),
+                    $c->query('ServerContainer')->getRequest(),
 
                     // inject the URLGenerator into the page controller
                     $c->query('ServerContainer')->getURLGenerator()
diff --git a/developer_manual/general/dependencyinjection.rst b/developer_manual/general/dependencyinjection.rst
deleted file mode 100644
index 316c094..0000000
--- a/developer_manual/general/dependencyinjection.rst
+++ /dev/null
@@ -1,228 +0,0 @@
-Dependency Injection
-====================
-
-.. sectionauthor:: Bernhard Posselt <dev at bernhard-posselt.com>
-
-`Dependency Injection <http://en.wikipedia.org/wiki/Dependency_injection>`_ is a programming pattern that helps you decouple dependencies between classes. The result is cleaner and more testable code. A good overview over how it works and what the benefits are can be read on `Google's Clean Code Talks <http://www.youtube.com/watch?v=RlfLCWKxHJ0>`_
-
-Basic Problem
--------------
-
-Consider the following class:
-
-.. code-block:: php
-
-  <?php
-
-  class PersonController {
-
-      public function listNames(){
-          $sql = "SELECT `prename` FROM `persons`";
-          $query = \OCP\DB::prepare($sql);
-          $result = $query->execute();
-
-          while($row = $result->fetchRow()){
-              echo '<p>' . htmlentities($row['prename']) . '</p>';
-          }
-      }
-
-  }
-
-This class prints out all prenames of a person and is called by using:
-
-.. code-block:: php
-
-  <?php
-  $controller = new PersonController();
-  $controller->listNames();
-
-
-This looks like clean code until the first tests are written. **Tests are absolutely necessary in every application! Do not think that your app is too small to require them.** The code will eventuell grow bigger and will have to be refactored.
-
-If code is refactored code will be written. If code is being written there will be bugs. If there will be bugs then every possible failure must be tested. This is tiresome and must be automated.
-
-If the code already comes with tests, this is not a problem. The unittests ensure that the changes did not introduce regressions.
-
-**Back to the above example**: How would a test for the SQL query look like?
-
-* ``\OCP\DB`` needs to be `Monkey Patched <http://en.wikipedia.org/wiki/Monkey_patch>`_ to make the query accessible
-* The monkey patching must not conflict with other tests which use the same class
-* There must be a database connection or the test will fail
-* If data is inserted into the database, it needs to be deleted afterwards
-
-This is a significant amount of work that needs to be done for every test. If something is hard to do, people tend to not do it that often or even won't do it at all. Because tests are necessary they need to be written and therefore they must be as easy as possible.
-
-Furthermore Monkey Patching is not a good solution, so most static classes need to be built with testing methods built in. These methods are only for debugging purposes and do not add any value to the running product.
-
-Inject Dependencies
--------------------
-The reason why the example class is hard to test is because it depends on ``\OCP\DB``. To be able to test it, the class needs to be replaced with a `mock object <http://en.wikipedia.org/wiki/Mock_object>`_.
-
-This mock object is passed to the class either using a `Setter <http://stackoverflow.com/questions/4478661/getter-and-setter>`_ or using an additional parameter in the constructor. The most common one is constructor injection.
-
-Using constructor injection, the example can be refactored by simply passing the object which executes the database request. At this point the first problem arises: ``\OCP\DB`` uses static methods and can not be instatiated as an object. This is because ownCloud uses static incorrectly.
-
-.. note:: Static methods and attributes should only be used for sharing information that needs to be available in all classes of this instance. **Do not use static methods for utility functions!** Instantiating an object is only one line in your code and can limitted to one place by putting it into a container.
-
-.. note:: Because of constructor injection, the constructor should not contain anything else than initializing attributes. **Never open a connection in a constructor**. Provide a seperate method that handles resourceintensive initialization.
-
-To get around the the static method call, which is actually more like a function call, the method needs to be wrapped in an object. This object can  be passed into the class. The refactored class would look like this:
-
-.. code-block:: php
-
-  <?php
-
-  class API {
-    public function prepareQuery($sql){
-      return \OCP\DB::prepare($sql);
-    }
-  }
-
-  class PersonController {
-
-    private $api;
-
-    public function __construct($api){
-      $this->api = $api;
-    }
-
-    public function listNames(){
-      $sql = "SELECT `prename` FROM `persons`";
-      $query = $this->api->prepareQuery($sql);
-      $result = $query->execute();
-
-      while($row = $result->fetchRow()){
-        echo '<p>' . htmlentities($row['prename']) . '</p>';
-      }
-    }
-
-  }
-
-  // run controller
-  $api = new API();
-  $controller = new PersonController($api);
-  $controller->listNames();
-
-
-Now a first, simple test can be written:
-
-.. note:: The other methods that are called on the mock object need to be implemented too, but for the sake of simplicity this is not done in this example
-
-.. code-block:: php
-
-  <?php
-  class PersonControllerTest extends \PHPUnit_Framework_TestCase {
-
-    private $api;
-
-    public function setUp(){
-      $this->api = $this->getMock('API', array('prepareQuery'));
-      $this->controller = new PersonController($this->api);
-    }
-
-
-    public function testListNamesQuery(){
-      $sql = "SELECT `prename` FROM `persons`";
-
-      $this->api->expects($this->once())
-          ->method('prepareQuery')
-          ->with($this->equalTo($sql));
-
-      $this->controller->listNames();
-
-    }
-
-  }
-
-Limit input and output to one place
------------------------------------
-The code also depends on another function: **echo**. Because this is usually hard to test, it is better to limit the use of input and output functions to one place. Remember that ownCloud uses PHP which likes to do output in functions like ``header`` or ``session_start``. The refactored code would look like this:
-
-.. code-block:: php
-
-  <?php
-
-  class API {
-    public function prepareQuery($sql){
-      return \OCP\DB::prepare($sql);
-    }
-  }
-
-  class PersonController {
-
-    private $api;
-
-    public function __construct($api){
-      $this->api = $api;
-    }
-
-    public function listNames(){
-      $sql = "SELECT `prename` FROM `persons`";
-      $query = $this->api->prepareQuery($sql);
-      $result = $query->execute();
-
-      $output = '';
-      while($row = $result->fetchRow()){
-        $output .= '<p>' . htmlentities($row['prename']) . '</p>';
-      }
-
-      return $output;
-    }
-
-  }
-
-  // run controller
-  $api = new API();
-  $controller = new PersonController($api);
-  echo $controller->listNames();
-
-The output test can now be implemented as a simple string comparison.
-
-
-Use a container
----------------
-The above example works fine in small cases, but if the class depends on four other classes that each depend on two other classes there will be **eight** instantiations. Also if one constructor parameter changes, every line that instantiates the class will have to change too.
-
-The solution is to define the injected classes as dependencies and let the system handle the rest.
-
-Pimple is a simple container implementation. The documentation on how to use it can be read on the `Pimple Homepage <http://pimple.sensiolabs.org/>`_
-
-The dependencies can now be defined like this:
-
-.. code-block:: php
-
-  <?php
-
-  class DIContainer extends \Pimple {
-
-    public function __construct(){
-
-      $this['API'] = $this->share(function($c){
-        return new API();
-      });
-
-
-      $this['PersonController'] = function($c){
-        return new PersonController($c['API']);
-      };
-  }
-
-The output could look like this:
-
-.. code-block:: php
-
-  <?php
-
-  $container = new DIContainer();
-  echo $container['PersonController']->listNames();
-
-
-The container figures out all dependencies and instantiates the objects accordingly. Also by using the **share** method, the `anti-pattern Singleton <http://en.wikipedia.org/wiki/Singleton_pattern>`_ can be avoided. From the Pimple Tutorial::
-
-  By default, each time you get an object, Pimple returns a new instance of it. If you want the same instance to be returned for all calls, wrap your anonymous function with the share() method
-
-Do not inject the container
----------------------------
-Injecting the container as a dependency is known as the `Service Locator Pattern <http://en.wikipedia.org/wiki/Service_locator_pattern>`_ which is widely regarded as an anti-pattern.
-
-It makes your code dependant on the container and hides the class' real dependencies. This makes testing and maintaining harder.
diff --git a/developer_manual/general/index.rst b/developer_manual/general/index.rst
index 250ecc0..27d679d 100644
--- a/developer_manual/general/index.rst
+++ b/developer_manual/general/index.rst
@@ -9,4 +9,3 @@ General
    security
    codingguidelines
    debugging
-   dependencyinjection

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-owncloud/owncloud-doc.git



More information about the Pkg-owncloud-commits mailing list