View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{composition-setup}{composition-setup}]]></ac:plain-text-body></ac:macro>
<h1>RFC: better configuration for components</h1>
<ac:macro ac:name="note"><ac:parameter ac:name="title">RFC Status: For discussion</ac:parameter><ac:rich-text-body>
<p>This RFC is currently under discussion and is not complete.<br />
Please comment on this RFC on the mailing list.</p></ac:rich-text-body></ac:macro>
<ac:macro ac:name="info"><ac:rich-text-body>
<p>This RFC is an alternate approach (counter-RFC) to <a href="http://framework.zend.com/wiki/display/ZFDEV2/RFC+-+Object+instantiation+and+configuration">Ralph's RFC</a>.</p></ac:rich-text-body></ac:macro>

<h2>TOC</h2>
<ac:macro ac:name="toc"><ac:parameter ac:name="minlevel">2</ac:parameter></ac:macro>

<h2>Introduction</h2>
<p>Currently, most ZF1/ZF2 components are instantiated using one of the following styles:</p>

<h4>A constructor that consumes an array of config options </h4>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$f = new Zend\Filter\Compress(array( ... ) );
]]></ac:plain-text-body></ac:macro>
<h4>A constructor that consumes 2 or more arrays for configuration:</h4>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$f = new Zend\Filter\InputFilter(
$filters,
$validators,
$data,
$options
;
]]></ac:plain-text-body></ac:macro>
<h4>A cocktail of scalars, arrays and objects</h4>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$p = new Zend\Mail\Protocol\Smtp(
'127.0.0.1',
null,
array( 'auth' => 'login')
;

Zend/Form/DisplayGroup::__construct(
$name,
PrefixPathMapper $loader,
$options = null
);
]]></ac:plain-text-body></ac:macro>
<h4>Any number of setters and getters for config options</h4>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$application->setOptions(array( ... ) );
$application->hasOption('foo');
$db->setAdapter($adapter);
$db->setParams(array( ... ));
$alnumFilter->setAllowWhiteSpace(true);
]]></ac:plain-text-body></ac:macro>
<h4>&quot;Deaf&quot; constructor (no parameters accepted) + setters</h4>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$amfServer = new Zend\Amf\Server();
$amfServer->setAuthService($auth);
$amfServer->setProduction(1);
$amfServer->setBroker($broker);
]]></ac:plain-text-body></ac:macro>

<p>The purpose of this thread is to try and normalize, simplify and unify the way we components in Zend Framework. This is becoming more important each as more and more components are ported to ZF2. We have also agreed on some form of public invitation for 3rd party developers to contribute additional components. A simplified, unified configuration style will help the framework cope with that.</p>


<h2>Problems with current approach (ZF1)</h2>
<ol>
<li><strong>What config params can I use?</strong></li>
</ol>


<p>An array() is convenient for passing large number of named parameters but because it is weakly-typed (i.e. array can hold any keys and values), the developer does not know what options are accepted, required or optional. What should happen with unrecognized values? Currently, they are usually ignored which introduces confusion for the developer. There are also inconsistencies in how configuration keys are named, and how even their associated getters and setters (should then exist) are named. This increases the confusion by making it much harder to quickly intuit the correct configuration key name.</p>

<ol>
<li><strong>Code duplication and multiple class responsibilities</strong></li>
</ol>


<p>For certain objects, the work is done by a network of helper objects within the same class hierarchy which may themselves be independently usable. Should any of these require bits of the configuration, the approaches can lead to duplication of getters, setters, validation logic and inconsistent methods of transporting configuration across the object boundaries. Where this imposes on one class the responsibility of managing configuration of other classes, it adds another responsibility which is in breach of the Single Responsibility Principle. Using a core Config object centralises all the configuration values, methods and operating logic in a single transportable unit.</p>

<ol>
<li><strong>Array performance</strong></li>
</ol>


<p>Array parsing and validation requires looping over its contents and processing all values. This is performed any number of different ways across components creating another point of inconsistent behaviour.</p>

<ol>
<li><strong>Validation results inconsistencies</strong></li>
</ol>


<p>Sometimes components throw an exception directly form constructor, sometimes during operation (when lazy-loading), sometimes they will just silently fail (return false?). Also - getters do not always cover all configuration parameters. The result is that users have no particular guarantee that configuration is validated. In many cases, a bad configuration value is first communicating to the user when some basic PHP operation fails. This is poor practice. Objects should locate problems BEFORE calling PHP functions and class methods. It ensures rapid debugging of faulty configuration values with a helpful Exception and reduces the change of a faulty configuration value ending up in a Fatal Error scenario unexpectedly.</p>

<ol>
<li><strong>Terminology inconsistencies</strong></li>
</ol>


<p>There are several different terms used in various components to describe configuration:</p>
<ul>
<li><em>options</em></li>
<li><em>parameters</em></li>
<li><em>properties</em></li>
<li><em>preferences</em></li>
</ul>


<ol>
<li><strong>Configuration key naming</strong></li>
</ol>


<p>At present, while discussed previously, configuration keys are not always named consistently. A core Config object could implement a simple convention based approach in that it could map configuration keys to getter/setter method names in one consistent fashion. This allows configuration keys to be driven by getter/setter naming and actively discourage random key naming which makes intuitive key selection difficult.</p>


<h2>Goals:</h2>
<p>Main goals of this proposal are:</p>
<ol>
<li>To decouple configuration from component functionality (adheres to separation of concerns, single responsibility principle, law of demeter)</li>
<li>To unify the way we configure all components (common standard).</li>
<li>To decrease learning curve.</li>
<li>To make configuration more robust and explicit.</li>
<li>To make configuration more IDE friendly.</li>
</ol>


<h2>Non-goals</h2>
<p>This proposal is <strong>NOT</strong> meant to :</p>
<ol>
<li>increase performance (in some scenarios it will, but that's not the goal)</li>
<li>decrease code verbosity</li>
<li>force better coding standards per se</li>
<li>deprecate all <code>__constructors</code> and their parameters in all classes</li>
</ol>


<ac:macro ac:name="info"><ac:rich-text-body>
<p>This is a proposal for configuring components' main classes - those directly exposed to the end-user. It is not intended to be applied to all possible classes, though sub units such as adapters/plugins/helpers <strong>should</strong> accept a configuration object, where necessary, rather than altering the transport mechanism needlessly (e.g. calling a list of setters or transferring config to an array).</p></ac:rich-text-body></ac:macro>

<h2>Using an object as a replacement for array</h2>

<p>Let's consider using simple objects as a configuration containers. Here's a very simple example:</p>

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck:id=code1}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=real-world usage}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$config = new RepeaterConfig;
$config->count = 40;
$config->text = 'Hello ';
$repeater = new Repeater($config);
$repeater->doit();
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=RepeaterConfig}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
class RepeaterConfig
{
/**
* @var int Number of times to repeat.
*/
public $count = 10;

/**
* @var string Text to repeat
*/
public $text = '';
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=Repeater}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
class Repeater
{
public $config;

/**
* @param RepeaterConfig $config
*/
public function __construct(RepeaterConfig $config)
{
$this->config = $config;
}

public function doit()
{
for($x=0;$x<$this->config->count;$x++){
echo $this->config->text;
}
}
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck}]]></ac:plain-text-body></ac:macro>

<h4>Pros</h4>
<ul>
<li>very IDE friendly</li>
<li>self-explanatory</li>
<li>all config parameters are named and well known</li>
<li>all config parameters can be given default values</li>
<li>accessors are evil^TM^ and should be avoided if possible (if not absolutely required)</li>
<li>config object is referable, so we do not have to interact with the main component instance to change configuration (separation of concerns)</li>
</ul>



<h2>Using getters and setters for validation</h2>

<p>Now let's add validation for our config object. We are implementing SoC (Separation of Concerns) and SRP (Single Responsibility Principle) by moving the validation part out of the main class and into <code>RepeaterConfig</code> class. The main component should always receive a valid, pre-processed configuration so it can focus on real-world functionality.</p>

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck:id=code2}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=real-world usage}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$config = new RepeaterConfig;

// using named parameters (properties)
$config->count = 40;
$config->text = 'Hello ';
$repeater = new Repeater($config);
$repeater->doit();

// using setters
$config->setCount(50);
$config->setText('Bye ');
$repeater->doit();

]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=RepeaterConfig}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
/**
* @property int $count Number of times to repeat.
* @property string $text Text to repeat
* @throws BadMethodCallException
*/
class RepeaterConfig
{
/**
* @var int Number of times to repeat.
*/
protected $_count = 10;

/**
* @var string Text to repeat
*/
protected $_text = '';

/**
* @throws BadMethodCallException
* @param int $count
* @return void
*/
public function setCount($count)
{
if(!is_numeric($count) || $count < 0){
throw new \BadMethodCallException('The supplied count of "'.$count.'" is invalid');
}
$this->_count = $count;
}

/**
* @throws BadMethodCallException
* @param string $text
* @return void
*/
public function setText($text)
{
if(!is_scalar($text)){
throw new \BadMethodCallException('The supplied text is invalid');
}
$this->_text = $text;
}

public function __set($what,$val)
{
$method = 'set'.ucfirst($what);
if(is_callable(array($this,$method))){
return call_user_func(
array($this,$method),
$val
);
}
throw new \BadMethodCallException('Unknown config property '.$what);;
}

public function __get($what)
{
$method = 'set'.ucfirst($what);
if(is_callable(array($this,$method))){
return call_user_func(
array($this,$method)
);
}
throw new \BadMethodCallException('Unknown config property '.$what);;
}
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=Repeater}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
class Repeater
{
public $config;

/**
* @param RepeaterConfig $config
*/
public function __construct(RepeaterConfig $config)
{
$this->config = $config;
}

public function doit()
{
for($x=0;$x<$this->config->count;$x++){
echo $this->config->text;
}
}
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck}]]></ac:plain-text-body></ac:macro>

<h4>Pros</h4>
<ul>
<li>all of the previous pros</li>
<li>still very IDE friendly (we are using standard <code>@parameter</code> to handle dynamic properties)</li>
<li>validation is handled by <code>RepeaterConfig</code> instead of the functional, main component's class (separation of concerns)</li>
<li>we can use either configuration style: properties or setters</li>
</ul>



<h2>Backward compatibility</h2>

<p>Let's implement constructor into our config object and main class, so if we wish to construct an instance from an array (knowing what config options are accepted) we can do that.</p>

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck:id=code3}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=real-world usage}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
// using config array
$repeater = new Repeater(array(
'count' => 1000,
'text' => 'aBc'
));

// using Zend\Config
$zconfig = new Zend\Config\Config(array(
'count' => 500,
'text' => 'foo '
));
$repeater = new Repeater($zconfig);

// using array with config object
$config = new RepeaterConfig(array(
'count' => 500,
'text' => 'foo '
));
$repeater = new Repeater($config);
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=RepeaterConfig}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
/**
* @property int $count Number of times to repeat.
* @property string $text Text to repeat
* @throws BadMethodCallException
*/
class RepeaterConfig
{
/**
* @var int Number of times to repeat.
*/
protected $_count = 10;

/**
* @var string Text to repeat
*/
protected $_text = '';


/**
* @throws BadFunctionCallException
* @param null|array|Iterator $config
*/
public function __construct($config = null)
{
if($config === null)
return;

if(is_object($config) && !($config instanceof \Iterator)){
throw new \BadFunctionCallException('Cannot use object of class '.get_class($config).' as config');
}elseif(is_array($config)){
foreach($config as $key=>$val){
$this->__set($key,$val);
}
}else{
throw new \BadFunctionCallException('Cannot use '.gettype($config).' as config');
}
}
/**
* @throws BadMethodCallException
* @param int $count
* @return void
*/
public function setCount($count)
{
if(!is_numeric($count) || $count < 0){
throw new \BadMethodCallException('The supplied count of "'.$count.'" is invalid');
}
$this->_count = $count;
}

/**
* @throws BadMethodCallException
* @param string $text
* @return void
*/
public function setText($text)
{
if(!is_scalar($text)){
throw new \BadMethodCallException('The supplied text is invalid');
}
$this->_text = $text;
}

public function __set($what,$val)
{
$method = 'set'.ucfirst($what);
if(is_callable(array($this,$method))){
return call_user_func(
array($this,$method),
$val
);
}
throw new \BadMethodCallException('Unknown config property '.$what);;
}

public function __get($what)
{
$method = 'set'.ucfirst($what);
if(is_callable(array($this,$method))){
return call_user_func(
array($this,$method)
);
}
throw new \BadMethodCallException('Unknown config property '.$what);;
}
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=Repeater}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
class Repeater
{
/**
* @var RepeaterConfig
*/
public $config;

/**
* @throws BadFunctionCallException
* @param array|RepeaterConfig|Iterator $config
*/
public function __construct($config)
{
if(is_object($config)){
if($config instanceof RepeaterConfig){
$this->config = $config;
}elseif($config instanceof \Iterator){
$this->config = new RepeaterConfig();
foreach($config as $key=>$val){
$this->config->__set($key,$val);
}
}else{
throw new \BadFunctionCallException('Cannot use object of class '.get_class($config).' as config');
}
}elseif(is_array($config)){
$this->config = new RepeaterConfig();
foreach($config as $key=>$val){
$this->config->__set($key,$val);
}
}else{
throw new \BadFunctionCallException('Cannot use '.gettype($config).' as config');
}
}

public function doit()
{
for($x=0;$x<$this->config->count;$x++){
echo $this->config->text;
}
}
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck}]]></ac:plain-text-body></ac:macro>

<h4>Pros</h4>
<ul>
<li>all before</li>
<li>we can use array and any Iterable object for configuration (including Zend\Config)</li>
<li>we can use any of the 3 config styles, fully BC.</li>
</ul>



<h2>DI, lazy-loading and instantiating depending classes</h2>
<p>Because we are internally using setters, we can instantiate depending objects using any method imaginable.<br />
Let's modify our configuration to digest a <code>Zend\Filter</code>. Let's also try to use DI to set up some defaults.</p>

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck:id=code4}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=real-world usage}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
// config using DI
$di = new Zend\Di\DependencyInjector;
$di->getInstanceManager()->setParameters('RepeaterConfig', array(array(
'count' => 1,
'text' => 'default text',
)));
$repeater = $di->newInstance('Repeater');


/*
* If DI supported config objects (or sub-instances) in the future, we
* could probably use something like this:
*/
$di->getInstanceManager()->setParameters('RepeaterConfig', array(
'count' => 1,
'text' => 'default text',
'filter' => new Zend\Di\InstanceParameter(
'Zend\Filter\StripTags', // <- lazy load this class
array(
'commentsAllowed' => true
)
)
));

]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=RepeaterConfig}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
/**
* @property int $count Number of times to repeat.
* @property string $text Text to repeat
* @property Zend\Filter\Filter $filter Filter to use for cleaning up text
* @throws BadMethodCallException
*/
class RepeaterConfig
{
/**
* @var int Number of times to repeat.
*/
protected $_count = 10;

/**
* @var string Text to repeat
*/
protected $_text = '';

/**
* @var Zend\Filter\Filter
*/
protected $_filter;

/**
* @throws BadFunctionCallException
* @param null|array|Iterator $config
*/
public function __construct($config = null)
{
if($config === null)
return;

if(is_object($config) && !($config instanceof \Iterator)){
throw new \BadFunctionCallException('Cannot use object of class '.get_class($config).' as config');
}elseif(is_array($config)){
foreach($config as $key=>$val){
$this->__set($key,$val);
}
}else{
throw new \BadFunctionCallException('Cannot use '.gettype($config).' as config');
}
}
/**
* @throws BadMethodCallException
* @param int $count
* @return void
*/
public function setCount($count)
{
if(!is_numeric($count) || $count < 0){
throw new \BadMethodCallException('The supplied count of "'.$count.'" is invalid');
}
$this->_count = $count;
}

/**
* @throws BadMethodCallException
* @param string $text
* @return void
*/
public function setText($text)
{
if(!is_scalar($text)){
throw new \BadMethodCallException('The supplied text is invalid');
}
$this->_text = $text;
}

/**
* @param Zend\Filter\Filter $filter
* @return void
*/
public function setFilter(Zend\Filter\Filter $filter){
$this->_filter = $filter;
}

/**
* @return Zend\Filter\Filter $filter
*/
public function getFilter(){
if($this->_filter === null){
$this->_filter = new Zend\Filter\Alnum(true);
}
return $this->_filter;
}

public function __set($what,$val)
{
$method = 'set'.ucfirst($what);
if(is_callable(array($this,$method))){
return call_user_func(
array($this,$method),
$val
);
}
throw new \BadMethodCallException('Unknown config property '.$what);;
}

public function __get($what)
{
$method = 'set'.ucfirst($what);
if(is_callable(array($this,$method))){
return call_user_func(
array($this,$method)
);
}
throw new \BadMethodCallException('Unknown config property '.$what);;
}
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card:label=Repeater}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
class Repeater
{
/**
* @var RepeaterConfig
*/
public $config;

/**
* @throws BadFunctionCallException
* @param array|RepeaterConfig|Iterator $config
*/
public function __construct($config)
{
if(is_object($config)){
if($config instanceof RepeaterConfig){
$this->config = $config;
}elseif($config instanceof \Iterator){
$this->config = new RepeaterConfig();
foreach($config as $key=>$val){
$this->config->__set($key,$val);
}
}else{
throw new \BadFunctionCallException('Cannot use object of class '.get_class($config).' as config');
}
}elseif(is_array($config)){
$this->config = new RepeaterConfig();
foreach($config as $key=>$val){
$this->config->__set($key,$val);
}
}else{
throw new \BadFunctionCallException('Cannot use '.gettype($config).' as config');
}
}

public function doit()
{
$result = '';
for($x=0;$x<$this->config->count;$x++){
$result .= $this->config->text;
}
echo $this->config->filter->filter($result);
}
}
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{card}]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{deck}]]></ac:plain-text-body></ac:macro>



<h4>Pros</h4>
<ul>
<li>DI is a great candidate to handle sub-instances, loading, both run-time and global configuration (defaults)</li>
<li>DI could be modified to &quot;understand&quot; config objects and read their parameters</li>
<li>it is easy to implement things like optional/required parameters</li>
<li>lazy-loading</li>
</ul>