向构造函数传递太多参数是否被视为反模式?
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_name
、last_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 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 (....)。与所有事情一样,最好适度做事。用无数参数重载任何方法是不好的做法。相反,允许用户以一口大小的相关数据块构建对象。这更符合逻辑。在您的情况下,您可以将呼叫拆分为名称、通信,也许还有其他。
我正在考虑使用 factory_boy 库进行 API 测试。文档中的一个示例是:
class UserFactory(factory.Factory):
class Meta:
model = base.User
first_name = "John"
last_name = "Doe"
为此,我们需要将 first_name
、last_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 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 (....)。与所有事情一样,最好适度做事。用无数参数重载任何方法是不好的做法。相反,允许用户以一口大小的相关数据块构建对象。这更符合逻辑。在您的情况下,您可以将呼叫拆分为名称、通信,也许还有其他。