简体   繁体   English

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

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

I am considering using the factory_boy library for API testing.我正在考虑使用 factory_boy 库进行 API 测试。 An example from the documentation is:文档中的一个例子是:

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

    first_name = "John"
    last_name = "Doe"

For this to work, we need first_name , last_name , etc to be passed as parameters to the __init__() method of the base.User() class .为此,我们需要将first_namelast_name等作为参数传递给base.User() class__init__()方法。 However, if you have many parameters this leads to something like:但是,如果您有许多参数,则会导致类似以下内容:

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

Now the question is, is this construction considered anti-pattern, and if yes, what alternatives do I have?现在的问题是,这种结构是否被认为是反模式,如果是,我有什么替代方案?

Thanks谢谢

In Python 3.7, dataclasses (specified in PEP557 ) were added.在 Python 3.7 中,添加了数据类(在PEP557 中指定)。 This allows you to only write these arguments once and not again in the constructor, since the constructor is made for you:这允许您只在构造函数中写入一次这些参数,而不能再次写入,因为构造函数是为您制作的:

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

It also adds a __repr__ to the class as well as some others.它还向类以及其他一些类添加了__repr__ Note that explicitly inheriting from object is no longer needed in Python 3, since all classes are new-style classes by default.请注意,在 Python 3 中不再需要显式继承自object ,因为默认情况下所有类都是新式类。

There are a few drawbacks, though.不过,也有一些缺点。 It is slightly slower on class definition (since these methods need to be generated).它在类定义上稍慢(因为需要生成这些方法)。 You need to either set a default value or add a type annotation , otherwise you get a name error.您需要设置默认值或添加类型注释,否则会出现名称错误。 If you want to use a mutable object, like a list, as a default argument, you need to use dataclass.field(default_factory=list) (normally it is discouraged to write eg def f(x=[]) , but here it actually raises an exception).如果你想使用一个可变对象,比如一个列表,作为默认参数,你需要使用dataclass.field(default_factory=list) (通常不鼓励写 eg def f(x=[]) ,但在这里它实际上引发了一个异常)。

This is useful where you have to have all those arguments in the constructor, because they all belong to the same object and cannot be extracted to sub-objects, for example.例如,当您必须在构造函数中包含所有这些参数时,这很有用,因为它们都属于同一个对象并且无法提取到子对象。

You could pack the __init__ method's keyword arguments into one dict, and set them dynamically with setattr :您可以将__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)

However, this has the drawback of disallowing you from supplying arguments without naming them - x = User("Mr", "Kevin") works with your original code, but not with this code.但是,这有一个缺点,即不允许您在不命名参数的情况下提供参数 - x = User("Mr", "Kevin")适用于您的原始代码,但不适用于此代码。

Yes, too many arguments is an antipattern (as stated in Clean Code by RObert C. Martin)是的,太多的论点是一种反模式(如 RObert C. Martin 在 Clean Code 中所述)

To avoid this, you have two design approaches:为了避免这种情况,您有两种设计方法:

The essence pattern 本质模式

The fluent interface/builder pattern流畅的界面/构建器模式

These are both similar in intent, in that we slowly build up an intermediate object, and then create our target object in a single step.它们在意图上是相似的,因为我们慢慢地建立一个中间对象,然后一步创建我们的目标对象。

I'd recommend the builder pattern, it makes the code easy to read.我推荐构建器模式,它使代码易于阅读。

The biggest risk would be if you had a large number of positional arguments and then ended up not knowing which is which.. Keyword arguments definitely make this better.最大的风险是,如果您有大量的位置参数,然后最终不知道哪个是哪个.. 关键字参数肯定会使这更好。

As suggested by others, the builder pattern also works quite nicely.正如其他人所建议的那样,构建器模式也很有效。 If you have a very large number of fields, you can also do something more generic, like so:如果您有大量字段,您还可以做一些更通用的事情,如下所示:

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

If overloading were not a problem then every class in python could be reduced to a single method, which we could call doIt (....).如果重载不是问题,那么python中的每个类都可以简化为一个方法,我们可以将其称为doIt(....)。 As with everything, it's best to do things in moderation.与所有事情一样,最好适度做事。 Overloading any method with umpteen arguments is bad practice.用大量参数重载任何方法都是不好的做法。 Instead, allow the user to build up the object in bite-size chunks of related data.相反,允许用户在相关数据的一口大小块中构建对象。 It's more logical.这更合乎逻辑。 In your case, you could split the calls into names, communications, and perhaps other.在您的情况下,您可以将呼叫拆分为姓名、通信,也许还有其他。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM