向构造函数传递太多参数是否被视为反模式?

Is passing too many arguments to the constructor considered an anti-pattern?

我正在考虑使用 factory_boy 库进行 API 测试。文档中的一个示例是:

class UserFactory(factory.Factory):
    class Meta:
        model = base.User

    first_name = "John"
    last_name = "Doe"

为此,我们需要将 first_namelast_name 等作为参数传递给 base.User() class__init__() 方法。然而,如果你有很多参数,这会导致类似:

class User(object):

    GENDER_MALE = 'mr'
    GENDER_FEMALE = 'ms'

    def __init__(self, title=None, first_name=None, last_name=None, is_guest=None,
             company_name=None, mobile=None, landline=None, email=None, password=None,
             fax=None, wants_sms_notification=None, wants_email_notification=None,
             wants_newsletter=None, street_address=None):

        self. title = title
        self.first_name = first_name
        self.last_name = last_name
        self.company_name = company_name
        self.mobile = mobile
        self.landline = landline
        self.email = email
        self.password = password
        self.fax = fax
        self.is_guest = is_guest
        self.wants_sms_notification = wants_sms_notification
        self.wants_email_notification = wants_email_notification
        self.wants_newsletter = wants_newsletter
        self.company_name = company_name
        self.street_address = street_address

现在的问题是,这种构造是否被视为反模式,如果是,我有哪些替代方案?

谢谢

是的,参数太多是一种反模式(如 RObert C. Martin 在 Clean Code 中所述)

为避免这种情况,您有两种设计方法:

The essence pattern

The fluent interface/builder pattern

这两个在意图上是相似的,因为我们慢慢地建立一个中间对象,然后一步创建我们的目标对象。

我推荐构建器模式,它使代码易于阅读。

您可以将 __init__ 方法的关键字参数打包到一个字典中,并使用 setattr:

动态设置它们
class User(object):
    GENDER_MALE = 'mr'
    GENDER_FEMALE = 'ms'
    def __init__(self, **kwargs):
        valid_keys = ["title", "first_name", "last_name", "is_guest", "company_name", "mobile", "landline", "email", "password", "fax", "wants_sms_notification", "wants_email_notification", "wants_newsletter","street_address"]
        for key in valid_keys:
            setattr(self, key, kwargs.get(key))

x = User(first_name="Kevin", password="hunter2")
print(x.first_name, x.password, x.mobile)

但是,这样做的缺点是不允许您在不命名的情况下提供参数 - x = User("Mr", "Kevin") 适用于您的原始代码,但不适用于此代码。

最大的风险是如果你有大量的位置参数然后最终不知道哪个是哪个..关键字参数肯定会让这更好。

正如其他人所建议的那样,构建器模式也能很好地工作。 如果你有非常多的字段,你也可以做一些更通用的事情,像这样:

class Builder(object):

    def __init__(self, cls):
        self.attrs = {}
        self.cls = cls

    def __getattr__(self, name):
        if name[0:3] == 'set':
            def setter(x):
                field_name = name[3].lower() + name[4:]
                self.attrs[field_name] = x
                return self
            return setter
        else:
            return super(UserBuilder, self).__getattribute__(name)

    def build(self):
        return self.cls(**self.attrs)

class User(object):

    def __str__(self):
        return "%s %s" % (self.firstName, self.lastName)

    def __init__(self, **kwargs):
        # TODO: validate fields
        for key in kwargs:
            setattr(self, key, kwargs[key])

    @classmethod
    def builder(cls):
        return Builder(cls)

print (User.builder()
  .setFirstName('John')
  .setLastName('Doe')
  .build()) # prints John Doe

在 Python 3.7 中添加了 dataclasses (specified in PEP557)。这允许您只在构造函数中写入这些参数一次,而不是再次写入,因为构造函数是为您制作的:

from dataclasses import dataclass

@dataclass
class User:
    title: str = None
    first_name: str = None
    last_name: str = None
    company_name: str = None
    mobile: str = None
    landline: str = None
    email: str = None
    password: str = None
    fax: str = None
    is_guest: bool = True
    wants_sms_notification: bool = False
    wants_email_notification: bool = False
    wants_newsletter: bool = False
    street_address: str = None

它还在 class 以及其他一些内容中添加了 __repr__。请注意,在 Python 3 中不再需要显式继承 object,因为默认情况下所有 classes 都是新式 classes。

不过也有一些缺点。 class 定义稍慢(因为需要生成这些方法)。您需要设置默认值或添加 type annotation,否则会出现名称错误。如果你想使用一个可变对象,比如一个列表,作为默认参数,你需要使用 dataclass.field(default_factory=list) (通常不鼓励写 def f(x=[]),但在这里它实际上引发了一个异常)。

这在您 在构造函数中拥有所有这些参数的情况下很有用,因为它们都属于同一个对象,不能提取到子对象,例如。

如果重载不是问题,那么 python 中的每个 class 都可以简化为一个方法,我们可以将其称为 doIt (....)。与所有事情一样,最好适度做事。用无数参数重载任何方法是不好的做法。相反,允许用户以一口大小的相关数据块构建对象。这更符合逻辑。在您的情况下,您可以将呼叫拆分为名称、通信,也许还有其他。