4

I sometimes find myself writing classes (in Python, in my case) that are used like this:

MyClass(some_args).go()

In other words, there's only one method in the class that any external code would call, and it will only ever get called once. There might be a return value from go() but often there isn't, and when there isn't, I could even call go from the class's __init__.

So why even bother with the class, instead of just writing go() as a standalone function? Because what goes on inside go is complicated. There may be several levels of nested function calls, and there are certain pieces of information that are needed in different spots. If I try to write all this without a class, I end up with a many of the functions returning multiple values. Sometimes the argument lists also get uncomfortably long. If I wrap everything up inside MyClass, then I can put all this information in member variables of the MyClass instance. These become not-quite-global variables, and they're generally stuff that feels like it make sense to store in a class.

Does this practice have a name? Is it a pattern, or an anti-pattern? And if the latter, how should I structure my code instead?

  • 1
    My first thought is, if the implementation is that complicated, then it should be in its own file, and the decision between class and standalone function should still be based solely on the public interface. Is there a reason why simply moving the complicated stuff to its own file wouldn't solve the problem? – Ixrec Jun 13 '15 at 18:16
  • @Ixrec, so I'd use actual global variables instead of what I'm calling "not-quite-global variables"? I'm sure the code could be structured that way, although I guess if I wanted to use it more than once I'd have to be sure to reset all the global variables properly, while creating a new MyClass instance handles that for me. – kuzzooroo Jun 13 '15 at 18:22
  • This sounds like the command pattern actually. This is common when the thing you do needs to be decoupled from the calling code, and is not the easiest, most straight forward thing to do. – Greg Burghardt Jun 13 '15 at 20:21
  • 2
    @GregBurghardt: the command pattern typically involves having different command objects, all implementing the same interface, and a client who executes the commands utilizing the interface. Moreover it does not enforce the command objects to have only a single, public method (though command objects often have that structure, indeed). I think the case of the OP is simpler. – Doc Brown Jun 14 '15 at 7:21
  • What happens if you call .go twice? – user2313838 Jun 14 '15 at 18:32
1

This is a natural progress of encapsulation, abstraction, and general code organization. As you extract pieces of code from a function into their own separate functions, you may notice that they share a lot of state that requires passing numerous parameters around. Promoting this state to fields of a class -- and said functions to methods of the class -- helps to reduce clutter and improve readability.

Note that in language like Python, where standalone functions are allowed, you may want to hide the existence of your class behind a function that both instantiates the object and calls its go method. This way, the class becomes an implementation detail and your API remains the same, even if e.g. the logic becomes complicated enough to warrant a whole subpackage.

4

Like Ixrec said - put the implementation in another file. But since you need a state that you don't want to make global - put that state in a class!

OK, that came up a bit confusing. What I mean is to keep the class, but hide it from the user. The user will only see a global function that internally creates the class and call it's go method. Actually - ditch the go. Since all it does is calling other functions, you probably should do that from that external function.

So, instead of

class MyClass:
    def __init__(self):
        self.x = 1
        self.y = 2

    def op1(self):
        self.x += self.y

    def op2(self):
        self.y += self.x

    def go(self):
        self.op1()
        self.op2()
        return self.x * self.y

Write this:

class __MyClass:
    def __init__(self):
        self.x = 1
        self.y = 2

    def op1(self):
        self.x += self.y

    def op2(self):
        self.y += self.x

    def calc_result(self):
        return self.x * self.y

def my_function():
    my_object = __MyClass()
    my_object.op1()
    my_object.op2()
    return my_object.calc_result()

That way, you still have a class that holds all the data, but the user sees a single function.

  • +1 for not letting the abstraction leak any more than it really must. Which coincidentally also makes misuse considerably harder. – Deduplicator Jun 14 '15 at 15:22
  • if using Python 3 another option would be nested functions and nonlocal variables. – Kelly Thomas Jun 15 '15 at 3:31
3

I do not think this practice has a special name, I would call it "encapsulation", "creating an abstraction by utilizing a class", "class design", or simply "OO programming". And it is definitely not an anti-pattern.

  • I would add that this sounds an awful lot like a function object or lambda. One public method that hides underlying complexity is a common theme in several frameworks and languages. – user22815 Jun 14 '15 at 9:40
  • Well, "leaky encapsulatio" "java-esque OO programming (forced class)" and the like seem to fit better. – Deduplicator Jun 14 '15 at 15:24
2

Uncomfortably long argument lists and multiple return values can usually be addressed by grouping values into structured data types or encapsulating them in classes. If there are no clear groupings, hiding that by sweeping it into object state with other unrelated values is only furtherly detrimental.

Methods (even methods intended to be private) should leave objects in a consistent state when they return. That becomes more difficult to achieve as the object's state gets more complex. Often it means factoring out some code into external/static functions so that you have: 1) a method that transitions the object from one valid state to another, which is accomplished by 2) functions that implement well-defined procedures, which may be passed a subset of values from the object state, but are separately testable.

Lastly, a function should be exposed as a function. That means clients should be calling my_function(some_args) and not MyClass(some_args).go(). If my_function is implemented as the latter, you are at least isolating client code from its ambiguities.

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.