【问题标题】:Is it a better practice to inject all variables in the constructor or to use setters and throw exceptions if they are not set?在构造函数中注入所有变量或使用设置器并在未设置时抛出异常是更好的做法吗?
【发布时间】:2012-07-17 00:06:03
【问题描述】:

假设你有这门课

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 );
} 

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

【问题讨论】:

    标签: php oop class


    【解决方案1】:

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

    如果您的类是可变的,那么使用带有必需参数的构造函数没有任何好处。通过访问器/修改器方法(也称为属性)公开成员。

    工厂模式(如@Ray 所建议的那样)可以提供帮助,但前提是您有各种类似的类 - 一次性然后您可以简单地使用静态方法来实例化对象,但您仍然可以“参数过多”的问题。

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

    我会坚持你正在做的事情,只有在出现问题时才将其更改为其他内容。

    【讨论】:

      【解决方案2】:

      类命名控制器以某种方式反映了 MVC,或者一般而言 - 任何负责处理序列的机制。

      无论如何,数据对象类往往有很多字段——这是他们的责任。 依赖于许多其他对象的常规对象可能会丢失一点。

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

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

      主题可能是this article on Constructor Initialization - 它建议使用构造函数内初始化。请务必跟进重点:

      如果在构造函数中有很多协作者需要提供呢? 一个大的构造参数列表,就像任何大的 参数列表,是一个CodeSmell

      正如Ray 所写 - 有可能使用设置器和there is article on that too 进行初始化。在我看来——我认为 Martin Fowler 确实很好地总结了这些案例。

      【讨论】:

      • 我认为读者和作家也是如此,我可以将它们组合成一个对象。我仍然需要它们是分离的对象,你可能想从数据库中读取并写入文件:)
      • @NicolaPeluchetti - 您可以向下移动阅读器/作者。该接口可以公开最终的接口方法,然后使用适当注入的流进行输出。 ;-)
      【解决方案3】:

      没有“更好”的方法。但您需要考虑以下几点:

      • 构造函数不被继承
      • 如果类需要太多的对象,它负责太多

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

      一般的经验法则是这样的:

      如果参数对于类的功能是强制性的,则应该通过构造函数注入。

      例外情况是,如果您使用工厂初始化实例。工厂从不同的类构建实例是很常见的,其中一些实现相同的接口和/或扩展相同的父类。那么通过setter注入共享对象就更容易了。

      【讨论】:

        【解决方案4】:

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

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

        【讨论】:

          【解决方案5】:

          任何有超过 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'];
          }
          

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

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

          【讨论】:

          • 这个问题是我会丢失类型提示:)
          • 是的,并且会影响“智能”ide,但是你在类型提示中失去了什么,你不必通过圣诞节列表长函数签名,你总是可以做 if ($object instanceof SomeClass) {} 条件作为一部分验证。
          【解决方案6】:

          “构造函数参数过多”的标准解决方案是builder pattern。您的控制器类本身仍然有一个长构造函数,但客户端可以在构建器上使用 setter,而后者随后将调用长构造函数。

          如果你只在一个或两个地方构建你的控制器对象,那么创建一个构建器甚至都不值得。在这种情况下,请坚持使用您当前的代码。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-01-19
            • 1970-01-01
            • 2015-07-10
            • 1970-01-01
            • 1970-01-01
            • 2019-02-22
            • 1970-01-01
            • 2011-03-31
            相关资源
            最近更新 更多