在构造函数中注入所有变量是更好的做法,还是在没有设置setter的情况下使用setter并抛出异常


Is it a better practice to inject all variables in the constructor or to use setters and throw exceptions if they are not set?

想象一下你有这个类

class Ai1ec_Less_Parser_Controller {
    /**
     * @var Ai1ec_Read_Variables_Startegy
     */
    private $read_variable_strategy;
    /**
     * @var Ai1ec_Save_Variables_Strategy
     */
    private $write_variable_strategy;
    /**
     * @var Ai1ec_Less_Variables_Collection
     */
    private $less_variables_collection;
    /**
     * @var Ai1ec_Less_Parser
     */
    private $ai1ec_less_parser;
    /**
     * We set the private variables in the constructor. I feel that there are too many parameters. 
     * Should i use setter instead and throw an exception if something is not set?
     * 
     * @param Ai1ec_Read_Variables_Startegy $read_variable_strategy
     * @param Ai1ec_Save_Variables_Strategy $write_variable_strategy
     * @param Ai1ec_Less_Variables_Collection $less_variables_collection
     * @param Ai1ec_Less_Parser $ai1ec_less_parser
     */
    public function __construct( Ai1ec_Read_Variables_Startegy $read_variable_strategy,
                                 Ai1ec_Save_Variables_Strategy $write_variable_strategy,
                                 Ai1ec_Less_Variables_Collection $less_variables_collection,
                                 Ai1ec_Less_Parser $ai1ec_less_parser ) {
    }
}

我需要设置这些变量,所以我在构造函数中设置了它们(但看起来参数太多了)。另一种选择是使用setter来设置它们,然后在一个方法中,如果所需的变量之一没有像那样设置,则抛出异常

public function do_something_with_parser_and_read_strategy() {
  if( $this->are_paser_and_read_strategy_set === false ) {
     throw new Exception( "You must set them!" );
  }
}  
private function are_paser_and_read_strategy_set () {
  return isset( $this->read_variable_strategy ) && isset( $this->ai1ec_less_parser );
} 

你认为这两种方法中的一种更好吗?为什么?

您的类是不可变的吗?如果是这样的话,那么通过构造函数进行100%的成员填充通常是最好的方法,但我同意,如果你有超过5或6个参数,它可能会开始看起来很难看。

如果你的类是可变的,那么拥有一个具有所需参数的构造函数就没有任何好处。通过访问器/赋值器方法(也称为属性)公开成员。

工厂模式(如@Ray所建议的)可能会有所帮助,但前提是你有各种类似的类——对于一次性的,你可以简单地使用静态方法来实例化对象,但你仍然会遇到"参数太多"的问题。

最后一种选择是接受一个带有字段的对象(每个参数一个字段),但要小心使用这种技术——如果某些值是可选的,那么只需使用方法重载(不幸的是,PHP不支持)。

我会坚持你正在做的事情,只有当它出现问题时,我才会改变它。

类命名控制器在某种程度上反映了MVC,或者一般来说,任何负责处理序列的机制。

在任何情况下,数据对象类往往有许多字段——这是它们的责任。依赖于许多其他对象的规则对象可能会丢失一点。

正如我所看到的,有四个对象:读取、保存、解析和为某个对象提供集合接口。为什么一个人的阅读和写作界面不同?这不能合为一体吗?Parser本身应该是一个库,因此可能没有理由在任何地方将其组合,尽管它可以为自己使用读取器/写入器,并作为回报提供集合。因此,解析器是否可能接受reader的参数并返回一个集合对象?

这更多的是关于具体案例。一般来说,方法有很多参数(或者由不同域的其他对象初始化对象中的许多字段)表明存在某种设计缺陷。

这篇关于构造函数初始化的文章可能是一种主题——它建议在构造函数初始化中使用。只需确保跟进要点:

如果构造函数中有很多合作者要提供呢一个庞大的结构参数列表,就像任何大型参数列表是CodeSmell。

正如Ray所写的那样,有可能使用setter进行初始化,也有关于这方面的文章。就我的观点而言,我认为马丁·福勒确实很好地总结了这些案例。

没有"更好"的方法。但这里有几件事你必须考虑:

  • 构造函数不是继承的
  • 如果类需要太多的对象,它就要承担太多的责任

这可能会影响您选择类实现的接口类型。

一般的经验法则是:

如果参数是类函数所必需的,则应通过构造函数注入这些参数

如果使用工厂初始化实例,则会出现异常。工厂构建实例形式多样的类是很常见的,其中一些实现了相同的接口和/或扩展了相同的父类。然后,通过setter注入共享对象就更容易了。

使用调用setter的工厂而不是使用一组参数的构造函数来创建对象要灵活得多。查看生成器和工厂模式。

为访问未完全构建的对象抛出异常是很好的!

任何有2个以上(有时是3个)参数的函数,我总是传递一个数组,所以它看起来像:

public function __construct(array $options = array()) {
    // Figure out which ones you truly need
    if ((!isset($options['arg1'])) || (mb_strlen($options['arg1']) < 1)) {
        throw new Exception(sprintf('Invalid $options[arg1]: %s', serialize($options)));
    }
    // Optional would look like
    $this->member2 = (isset($options['arg1'])) && ((int) $options['arg2'] > 0)) ? $options['arg2'] : null;
    // Localize required params (already validated above)
    $this->member1 = $options['arg1'];
}

传递一系列选项可以实现未来的增长,而无需更改函数签名。然而,它的缺点是,函数必须本地化数组的所有元素,以确保访问不会引发警告/错误(如果数组中缺少元素)。

在这种情况下,工厂解决方案不是一个好的选择,因为您仍然需要将值传递给工厂,以便它可以用正确的值初始化对象。

"构造函数参数太多"的标准解决方案是生成器模式。控制器类本身仍然有一个长构造函数,但客户端可以在构造函数上使用setter,后者稍后将调用长构造函数。

如果你只在一个或两个地方构造你的控制器对象,那么创建一个生成器就不值得那么麻烦了;在这种情况下,只需使用当前代码即可。