问题
I am considering using the factory_boy library for API testing. 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
. 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
回答1:
You could pack the __init__
method's keyword arguments into one dict, and set them dynamically with 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.
回答2:
In Python 3.7, dataclasses (specified in PEP557) were added. 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. Note that explicitly inheriting from object
is no longer needed in Python 3, since all classes are new-style classes by default.
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 e.g. def f(x=[])
, but here it actually raises an exception).
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.
回答3:
Yes, too many arguments is an antipattern (as stated in Clean Code by RObert C. Martin)
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.
回答4:
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
来源:https://stackoverflow.com/questions/30599478/is-passing-too-many-arguments-to-the-constructor-considered-an-anti-pattern