diff --git a/doc/Developer-Domain-Driven-Design.md b/doc/Developer-Domain-Driven-Design.md new file mode 100644 index 000000000..b8d886aa0 --- /dev/null +++ b/doc/Developer-Domain-Driven-Design.md @@ -0,0 +1,239 @@ +Domain-Driven-Design +============== + +* [Home](help) + * [Developer Intro](help/Developers-Intro) + +Friendica uses class structures inspired by Domain-Driven-Design programming patterns. +This page is meant to explain what it means in practical terms for Friendica development. + +## Inspiration + +- https://designpatternsphp.readthedocs.io/en/latest/Structural/DependencyInjection/README.html +- https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html +- https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html +- https://designpatternsphp.readthedocs.io/en/latest/Creational/FactoryMethod/README.html +- https://designpatternsphp.readthedocs.io/en/latest/Creational/Prototype/README.html + +## Core concepts + +### Models and Collections + +Instead of anonymous arrays of arrays of database field values, we have Models and collections to take full advantage of PHP type hints. + +Before: +```php +function doSomething(array $intros) +{ + foreach ($intros as $intro) { + $introId = $intro['id']; + } +} + +$intros = \Friendica\Database\DBA::selectToArray('intros', [], ['uid' => local_user()]); + +doSomething($intros); +``` + +After: +```php +function doSomething(\Friendica\Collection\Introductions $intros) +{ + foreach ($intros as $intro) { + /** @var $intro \Friendica\Model\Introduction */ + $introId = $intro->id; + } +} + +/** @var $intros \Friendica\Collection\Introductions */ +$intros = \Friendica\DI::intro()->select(['uid' => local_user()]); + +doSomething($intros); +``` + +### Dependency Injection + +Under this concept, we want class objects to carry with them the dependencies they will use. +Instead of calling global/static function/methods, objects use their own class members. + +Before: +```php +class Model +{ + public $id; + + function save() + { + return \Friendica\Database\DBA::update('table', get_object_vars($this), ['id' => $this->id]); + } +} +``` + +After: +```php +class Model +{ + /** + * @var \Friendica\Database\Database + */ + protected $dba; + + public $id; + + function __construct(\Friendica\Database\Database $dba) + { + $this->dba = $dba; + } + + function save() + { + return $this->dba->update('table', get_object_vars($this), ['id' => $this->id]); + } +} +``` + +The main advantage is testability. +Another one is avoiding dependency circles and avoid implicit initializing. +In the first example the method `save()` has to be tested with the `DBA::update()` method, which may or may not have dependencies itself. + +In the second example we can mock `\Friendica\Database\Database`, e.g. overload the class by replacing its methods by placeholders, which allows us to test only `Model::save()` and nothing else implicitly. + +The main drawback is lengthy constructors for dependency-heavy classes. +To alleviate this issue we are using [DiCe](https://r.je/dice) to simplify the instantiation of the higher level objects Friendica uses. + +We also added a convenience factory named `\Friendica\DI` that creates some of the most common objects used in modules. + +### Factories + +Since we added a bunch of parameters to class constructors, instantiating objects has become cumbersome. +To keep it simple, we are using Factories. +Factories are classes used to generate other objects, centralizing the dependencies required in their constructor. +Factories encapsulate more or less complex creation of objects and create them redundancy free. + +Before: +```php +$model = new Model(\Friendica\DI::dba()); +$model->id = 1; +$model->key = 'value'; + +$model->save(); +``` + +After: +```php +class Factory +{ + /** + * @var \Friendica\Database\Database + */ + protected $dba; + + function __construct(\Friendica\Database\Database $dba) + { + $this->dba; + } + + public function create() + { + return new Model($this->dba); + } +} + +$model = \Friendica\DI::factory()->create(); +$model->id = 1; +$model->key = 'value'; + +$model->save(); +``` + +Here, `DI::factory()` returns an instance of `Factory` that can then be used to create a `Model` object without having to care about its dependencies. + +### Repositories + +Last building block of our code architecture, repositories are meant as the interface between models and how they are stored. +In Friendica they are stored in a relational database but repositories allow models not to have to care about it. +Repositories also act as factories for the Model they are managing. + +Before: +```php +class Model +{ + /** + * @var \Friendica\Database\Database + */ + protected $dba; + + public $id; + + function __construct(\Friendica\Database\Database $dba) + { + $this->dba = $dba; + } + + function save() + { + return $this->dba->update('table', get_object_vars($this), ['id' => $this->id]); + } +} + +class Factory +{ + /** + * @var \Friendica\Database\Database + */ + protected $dba; + + function __construct(\Friendica\Database\Database $dba) + { + $this->dba; + } + + public function create() + { + return new Model($this->dba); + } +} + + +$model = \Friendica\DI::factory()->create(); +$model->id = 1; +$model->key = 'value'; + +$model->save(); +``` + +After: +```php +class Model { + public $id; +} + +class Repository extends Factory +{ + /** + * @var \Friendica\Database\Database + */ + protected $dba; + + function __construct(\Friendica\Database\Database $dba) + { + $this->dba; + } + + public function create() + { + return new Model($this->dba); + } + + public function save(Model $model) + { + return $this->dba->update('table', get_object_vars($model), ['id' => $model->id]); + } +} + +$model = \Friendica\DI::repository()->create(); +$model->id = 1; +$model->key = 'value'; + +\Friendica\DI::repository()->save($model); +``` diff --git a/doc/Developers-Intro.md b/doc/Developers-Intro.md index ae9a856b8..f72ff4abe 100644 --- a/doc/Developers-Intro.md +++ b/doc/Developers-Intro.md @@ -41,6 +41,8 @@ If you have seen Friendica you probably have ideas to improve it, haven't you? ## Programming +Friendica uses an implementation of [Domain-Driven-Design](help/Developer-Domain-Driven-Design), please make sure to check out the provided links for hints at the expected code architecture. + ### Composer Friendica uses [Composer](https://getcomposer.org) to manage dependencies libraries and the class autoloader both for libraries and namespaced Friendica classes. diff --git a/doc/Home.md b/doc/Home.md index d58cfeca6..9ed552bd3 100644 --- a/doc/Home.md +++ b/doc/Home.md @@ -45,6 +45,7 @@ Friendica Documentation and Resources * [Help on Vagrant](help/Vagrant) * [Bugs and Issues](help/Bugs-and-Issues) * Code structure + * [Domain-Driven-Design](help/Developer-Domain-Driven-Design) * [Addon Development](help/Addons) * [Theme Development](help/themes) * [Smarty 3 Templates](help/smarty3-templates)