2

I am writing a Python system where I have a major class(PlayerModel) that has two another classes as members (StateModel and ActionModel). I was wondering, what is the correct way to initialize such composite class?

Currently. I'm creating the members instances separately, and the class's __init__ method looks like this:

def __init__(self, state_model, action_model):
    self.state_model = state_model
    self.action_model = action_model

However, that means that the members' types are not known to the class in compile-time (and I lose, for example, autocompletion, which is quite annoying).

I can of course instantiate the members inside the major class, but that means I would always have to pass their arguments to the major class constructor, turning it into a method with 5-6 parameters, and I don't like those (and neither does Uncle Bob).

Is there a more elegant solution to this problem than aggregating the member classes' parameters into a named tuple?

2

First of all, be aware that Python is a dynamic language. That “the members' types are not known to the class in compile-time” is pretty much true whatever solution you use, because Python does not feature a static type system that assigns types to variables or class members. Good autocomplete is valuable, but you should not wager your design on the limitations of your editor.

There are a few important tradeoffs when instantiating fields inside or outside the class.

  • if instantiating outside, whoever instantiates our class needs to know about the internal details of our class in order to create the two dependencies. This is essentially a violation of Law of Demeter. On the other hand, these dependencies might play a meaningful role outside of our class.

  • if instantiating outside, we cannot be sure that our class holds the only reference to the inner object. This can lead to bugs of some other code changes the state of our inner objects. Other objects may be safely shared, or even should be shared (e.g. some external connection).

  • if instantiating inside, we cannot easily supply a mock object for testing our class.

For me, the deciding question is whether the inner objects are dependencies that might be selected at runtime, or are so large that they need to be tested in isolation. In your case, the answer to these is probably “yes”. It is much more reasonable to instantiate these classes on the outside

player_model = PlayerModel(
    state_model=StateModel(...),
    action_model=ActionModel(...))

than adding instantiation of the other models to all responsibilities it already has. In particular, the strategy of instantiating everything in the constructor leads to larger and more bloated constructors the further you look up the usage chain. This is not just a matter of counting parameters, it is about separation of concerns.

If it is not reasonable for the caller to take care of instantiating all dependencies, I sometimes create a factory to do this:

class Dependencies(object):
  def __init__(self, global_configuration):
    ...

  def new_player_model(self, ...):
    return PlayerModel(
      state_model=self.new_state_model(...),
      action_model=self.new_action_model(...))

  def new_state_model(self, ...): return StateModel(...)
  def new_action_model(self, ...): return ActionModel(...)

Then:

deps = Dependencies(...)
player_model = deps.new_player_model(...)

Often, many of the constructor arguments are just various dependencies that have to be fulfilled. Our factory methods in the Dependencies object only have to forward the other arguments, since the dependencies can be managed internally. In such a case, centralizing dependency management can be quite useful.

If a large number of arguments is unavoidable, there are a couple of strategies to cope with that. The most important comes built-in with Python: named parameters. When declaring a function def foo(a, b, c): ..., you can invoke it as foo("a", c="c", b="b"). Note that these labels are optional and the order is arbitrary, and that Python will throw an error if any required argument was forgotten. This makes the variable names of your parameters important because they may be used for named parameters. With named parameters, it is reasonable to manage a large number of arguments. Here, the advice against many parameters by “Uncle Bob” and others only applies with limitations, since their advice is primarily targeted at languages like Java that lack named parameters.

However, named parameters do not solve all problems (matplotlib.pyplot is my favourite counterexample). Often, there will be natural groups of parameters. Some parameters may only be used together (e.g. an x and y coordinate). We can then introduce small objects that hold these arguments and do nothing else. Applied to your case, we might have StateModelArgs and ActionModelArgs or some other grouping. The caller then provides these arguments, but the actual objects are instantiated from these arguments inside our class. This strategies has some advantages e.g. that validation can be done more easily in the argument objects, but they might also provide so little value that it would be easier to instantiate the actual objects directly.

So which strategy to use depends a lot on the specific circumstances, and either choice can make sense.

  • Thank you so much for the detailed answer! I was definitely going to use the factory pattern, but it helped me decide how to organize it. I agree with you that large number of arguments is less of a problem in python; however, if the inner module classes are subclass, the inside-initialization solution is still quite awkward. So I guess it's better for me to stay with outside instantiation. Thanks again! – Venomouse Feb 22 '16 at 23:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.