PHP - 使用大量参数和默认值初始化对象的最佳方法

Posted

技术标签:

【中文标题】PHP - 使用大量参数和默认值初始化对象的最佳方法【英文标题】:PHP - best way to initialize an object with a large number of parameters and default values 【发布时间】:2011-08-23 11:10:01 【问题描述】:

我正在设计一个类,该类定义一个高度复杂的对象,其中包含大量(50+)个大多数可选参数,其中许多参数都有默认值(例如:$type = 'foo'; $width = '300'; $interactive = false;)。我正在尝试确定设置构造函数和实例/类变量的最佳方法,以便能够:

让该类易于使用 使自动记录类变得容易(即:使用 phpDocumentor) 优雅地编码

鉴于上述情况,我不想向构造函数传递大量参数。我将传递一个包含初始化值的哈希,例如:$foo = new Foo(array('type'=>'bar', 'width'=>300, 'interactive'=>false));

在类的编码方面,我仍然觉得我宁愿......

class Foo 
    private $_type = 'default_type';
    private $_width = 100;
    private $_interactive = true;

    ...

...因为我相信这将有助于文档生成(您会获得类的属性列表,这让 API 用户知道他们必须使用哪些“选项”),并且它“感觉”是正确的方法。

但是你遇到了将构造函数中的传入参数映射到类变量的问题,并且在不利用符号表的情况下,你进入了一种“蛮力”方法,这对我来说违背了目的(尽管我接受其他意见)。例如:

function __construct($args)
    if(isset($args['type'])) $_type = $args['type']; // yuck!

我考虑过创建一个本身就是关联数组的类变量。那么初始化它会很容易,例如:

private $_instance_params = array(
    'type' => 'default_type',
    'width' => 100,
    'interactive' => true
);

function __construct($args)
    foreach($args as $key=>$value)
        $_instance_params[$key] = $value;
    

但这似乎我没有利用私有类变量等原生特性,而且感觉文档生成不适用于这种方法。

感谢您阅读本文;我可能在这里问了很多,但我是 PHP 新手,我真的只是在寻找惯用/优雅的方式来做这件事。您的最佳做法是什么?


附录(关于这个特定类的详细信息)

这个类很可能试图做太多事情,但它是旧 Perl 库的一个端口,用于创建和处理表单。可能有一种方法可以划分配置选项以利用继承和多态性,但实际上可能会适得其反。

根据要求,这里是一些参数的部分列表(Perl 代码)。您应该看到,它们并不能很好地映射到子类。

这个类当然有很多属性的getter和setter,所以用户可以覆盖它们;这篇文章的目的(以及原始代码做得很好)是提供一种紧凑的方式来实例化这些 Form 对象,并使用已经设置的所需参数。它实际上使代码非常易读。

# Form Behaviour Parameters
        # --------------------------
        $self->id; # the id and the name of the <form> tag
        $self->name = "webform"; # legacy - replaced by id
        $self->user_id = $global->user_id; # used to make sure that all links have the user id encoded in them. Usually this gets returned as the 'i' user input parameter
        $self->no_form; # if set, the <form> tag will be omitted
        $self->readonly; # if set, the entire form will be read-only
        $self->autosave = ''; # when set to true, un-focusing a field causes the field data to be saved immediately
        $self->scrubbed; # if set to "true" or non-null, places a "changed" radio button on far right of row-per-record forms that indicates that a record has been edited. Used to allow users to edit multiple records at the same time and save the results all at once. Very cool.
        $self->add_rowid; # if set, each row in a form will have a hidden "rowid" input field with the row_id of that record (used primarily for scrubbable records). If the 'scrubbed' parameter is set, this parameter is also automatically set. Note that for this to work, the SELECT statement must pull out a unique row id. 
        $self->row_id_prefix = "row_"; # each row gets a unique id of the form id="row_##" where ## corresponds to the record's rowid. In the case of multiple forms, if we need to identify a specific row, we can change the "row_" prefix to something unique. By default it's "row_"

        $self->validate_form; # parses user_input and validates required fields and the like on a form
        $self->target; # adds a target window to the form tag if specified
        $self->focus_on_field; # if supplied, this will add a <script> tag at the end of the form that will set the focus on the named field once the form loads.
        $self->on_submit; # adds the onSubmit event handler to the form tag if supplied
        $self->ctrl_s_button_name; # if supplied with the name of the savebutton, this will add an onKeypress handler to process CTRL-S as a way of saving the form

        # Form Paging Parameters
        # ----------------------
        $self->max_rows_per_page; # when displaying a complete form using printForm() method, determines the number of rows shown on screen at a time. If this is blank or undef, then all rows in the query are shown and no header/footer is produced.
        $self->max_pages_in_nav = 7; # when displaying the navbar above and below list forms, determines how many page links are shown. Should be an odd number
        $self->current_offset; # the current page that we're displaying
        $self->total_records; # the number of records returned by the query
        $self->hide_max_rows_selector = ""; # hide the <select> tag allowing users to choose the max_rows_per_page
        $self->force_selected_row = ""; # if this is set, calls to showPage() will also clear the rowid hidden field on the form, forcing the first record to be displayed if none were selected
        $self->paging_style = "normal"; # Options: "compact"

当然,我们可以让自己陷入关于编程风格的更长时间的辩论。但我希望避免它,为了所有相关人员的理智!这里(同样是 Perl 代码)是一个使用大量参数实例化此对象的示例。

my $form = new Valz::Webform (
            id                      => "dbForm",
            form_name               => "user_mailbox_recip_list_students",
            user_input              => \%params,
            user_id                 => $paramsi,
            no_form                 => "no_form",
            selectable              => "checkbox",
            selectable_row_prefix   => "student",
            selected_row            => join (",", getRecipientIDsByType('student')),
            this_page               => $paramsc,
            paging_style            => "compact",
            hide_max_rows_selector  => 'true',
            max_pages_in_nav        => 5
        );

【问题讨论】:

这听起来像班级正在做的事情太多了。您能否详细说明这个类应该做什么,并列出 50 个属性中的一些或全部。 制作这些公共成员有什么缺点?您是否需要在构建后对其进行修复并且不提供任何其他方式来更改值? 由于您没有更新您的问题以包含所请求的信息,我只会将您链接到Large Class Code Smell。我鼓励您阅读并应用建议的重构。 @Gordon - 好兄弟,给个家伙时间!您对这里的大类综合症可能是正确的——我希望我在附录地址中的解释(如果不能证明的话)关注这一点。特别是,类提取或子类提取并不能促进我希望 API 用户可以使用的那种“捷径”实例化。希望这是有道理的。谢谢你的链接,我忘记了那个很棒的网站。 @Tom 感谢您的更新。班级肯定做的太多了。这很容易通过表单行为和表单分页中的属性划分来发现。虽然它不是“捷径”,但我仍然会尝试将其重构为更小的部分,以消除各种组件的复杂性。对于实例化,您可以使用 Factory 或 Builder 模式。 【参考方案1】:

我可以想到两种方法。如果你想保留你的实例变量,你可以遍历传递给构造函数的数组并动态设置实例变量:

    <?php

    class Foo 
        private $_type = 'default_type';
        private $_width = 100;
        private $_interactive = true;

        function __construct($args)
            foreach($args as $key => $val) 
                $name = '_' . $key;
                if(isset($this->$name)) 
                    $this->$name = $val;
                
            
        
    

    ?>

使用数组方法时,您实际上不必放弃文档。只需在类主体中使用@property 注释即可:

<?php

/**
 * @property string $type
 * @property integer $width
 * @property boolean $interactive
 */
class Foo 
    private $_instance_params = array(
        'type' => 'default_type',
        'width' => 100,
        'interactive' => true
    );

    function __construct($args)
        $this->_instance_params = array_merge_recursive($this->_instance_params, $args);
    

    public function __get($name)
    
        return $this->_instance_params[$name];
    

    public function __set($name, $value)
    
        $this->_instance_params[$name] = $value;
    


?>

也就是说,一个有 50 个成员变量的类要么仅用于配置(可以拆分),要么只是做的太多,您可能需要考虑重构它。

【讨论】:

我喜欢您的两种方法,但没有真正想到我会使用 $this 访问成员变量,因此可以以编程方式访问它们。如果变量已声明但未分配值,isset() 是否返回 true(我什至不确定这在 PHP 中是否有意义)。我只是在想 - 如果我没有某个值的默认值(例如:private $_foo;)怎么办? 我会将 null 分配给这些。这样你就可以检查 if(isset($this->$name) || $this->$name === null)... 那么要明确一点,如果我要实现上面的方法1,我必须在声明类属性时分配某种值?即:private $foo = null; 并且必须避免private $foo;?我认为如果值为 null,则 isset() 返回 false,并且我认为声明一个成员变量而不为它分配一个分配给它的值 null? private $foo = null;private $foo; 完全相同,isset 将返回 false。如果你想检查你是否在你的类中定义了属性,请使用property_exists。 感谢达夫提供的所有重要信息【参考方案2】:

另一种方法是使用FooOptions对象实例化类,仅作为选项容器,即:

<?php
class Foo 

    /*
     * @var FooOptions
     */
    private $_options;

    public function __construct(FooOptions $options) 
    
        $this->_options = $options;
    



class FooOptions

    private $_type = 'default_type';
    private $_width = 100;
    private $_interactive = true;

    public function setType($type);
    public function getType();

    public function setWidth($width);
    public function getWidth();

    // ...

您的选项有据可查,您可以轻松地设置/检索它们。这甚至有助于您的测试,因为您可以创建和设置不同的选项对象。

我不记得这个模式的确切名称,但我认为它是 BuilderOption 模式。

【讨论】:

这实际上听起来更像是一个模型模式。我不确定您的建议是否真的有效,除非您定义一个接口(PHP 可以这样做吗?) IFooOptions 然后让 API 用户在 MyOptions 类中实现该接口,或扩展该类(例如:MyFooOptions extends FooOptions)并通过Foo 构造函数的一个实例。如果用户只实例化 Foo 几次,这可能会起作用。在用户创建这个类的许多实例的情况下,可能需要动态设置参数,这变得非常尴尬。 是的,PHP 可以像定义类一样定义接口:interface IFooInterface。当然,这种方法有它的弱点,会让用户的生活更加艰难,但是你会得到一个带有类选项的定义良好的 API。也许对于您的情况,这不是最佳选择... :)【参考方案3】:

根据Daff's 解决方案之一跟进我是如何实现的:

    function __construct($args = array())
        // build all args into their corresponding class properties
        foreach($args as $key => $val)                 
            // only accept keys that have explicitly been defined as class member variables
            if(property_exists($this, $key)) 
                $this->$key = $val;
            
        
    

欢迎提出改进建议!

【讨论】:

【参考方案4】:

你也可以创建一个父类。

在那个类中你只定义变量。

protected function _SetVarName( $arg )

   $this->varName=$arg;

然后将该类扩展为一个新文件,并在该文件中创建所有进程。

所以你得到了

classname.vars.php
classname.php

classname extends classnameVars 


因为大多数都是默认设置,所以您只需设置/重置您需要的那些。

$cn=new classname();
$cn->setVar($arg);    
//do your functions..

【讨论】:

如果我要强制 API 用户这样做,为什么不直接在类中使用公共变量,让用户直接设置呢?【参考方案5】:

我在我的一些课程中使用它。便于复制和粘贴以实现快速开发。

private $CCNumber, $ExpMonth, $ExpYear, $CV3, $CardType;
function __construct($CCNumber, $ExpMonth, $ExpYear, $CV3, $CardType)
    $varsValues = array($CCNumber, $ExpMonth, $ExpYear, $CV3, $CardType);
    $varNames = array('CCNumber', 'ExpMonth', 'ExpYear', 'CV3', 'CardType');
    $varCombined = array_combine($varNames, $varsValues);
    foreach ($varCombined as $varName => $varValue) $this->$varName = $varValue;

使用步骤:

    粘贴并从当前的 __construct 函数中获取变量列表,删除所有可选参数值 如果尚未粘贴,请使用您选择的范围将其粘贴到您的类中声明变量 将同一行粘贴到 $varValues 和 $varNames 行中。 在 ", $" 上为 "', '" 进行文本替换。除了您必须手动更改的第一个和最后一个之外,这将得到所有内容 享受吧!

【讨论】:

【参考方案6】:

对 Daff 的第一个解决方案进行了一点改进,以支持可能具有 null 默认值并将 FALSE 返回到 isset() 条件的对象属性:

<?php

class Foo 
    private $_type = 'default_type';
    private $_width = 100;
    private $_interactive = true;
    private $_nullable_par = null;

    function __construct($args)
        foreach($args as $key => $val) 
            $name = '_' . $key;
            if(property_exists(get_called_class(),$name))
                $this->$name = $val;
            
        
    


?>

【讨论】:

以上是关于PHP - 使用大量参数和默认值初始化对象的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有参数的情况下快速将类保存到用户默认值

构造方法

php面向对象构造函数,析构函数

PHP 5.4:禁用警告“从空值创建默认对象”

PHP面向对象--构造函数与析构函数

getForm() symfony2.8 后更改表单对象默认参数的最佳实践