Search code examples
pythonoopsoftware-designdesign-principles

python class operations on members vs static methods


I need some help figuring out the right OOP design choice for my python class.

In my example, letters are appended to a string in separate steps. The string is then printed. For this application, it is not really important to the user to get the intermediate results.

So is it better to pass the string to the constructor and then use private methods for the operations and a dothings method calling them all?

class A:
    def __init__(self, string: str()):
        self.string = string

    def _append_a(self):
        self.string += "a"

    def _append_b(self):
        self.string += "b"

    def dothings(self):
        _append_a()
        _append_b()

    def export(self):
        print(self.string)

Or is it better to have the string passed to each method?

class AA:
    @staticmethod
    def append_a(string):
        string += "a"
        return string

    @staticmethod
    def append_b(string):
        string += "b"
        return string

    @staticmethod
    def export(string):
        print(string)

The interface of A looks a bit cleaner to me, one can just call dothings and then export. However, class A would be a bit of a black box, while with class AA the user has some more insights to what is happening.

Is there a 'right' choice for this?


Solution

  • AA is easily dismissed. There is nothing object-oriented about it: it's just three regular functions collected into a single namespace. There's no shared state operated on by a set of methods. There's no suggestion that only the output of one function is a valid input to another. For example, the intention is probably to write something like

    export(append_a(append_b("foo")))  # fooba
    

    but nothing requires this pattern be followed. The functions aren't related to each other in anyway.

    A has some things in common with the builder pattern. Given an initial string, you can append as and bs to it, but nothing else (without violating encapsulation provided by the methods. Eventually, you get the "final" value by calling export, so the work flow it represents is something like:

    a = A("foo")
    a.append_a()
    a.append_a()
    a.append_b()
    a.append_b()
    a.append_a()
    a.export()  # fooaabba
    

    The class as shown is almost trivially simple, but demonstrates how to provide a well defined interface to building a string value from an initial seed. You can't just do anything you like with it: you can't prepend values, you can't remove existing characters, etc.


    To conform more closely to the builder pattern, I would modify A as follows:

    class A:
        def __init__(self, string: str):
            self.string = string
    
        def append_a(self):
            self.string += "a"
    
        def append_b(self):
            self.string += "b"
    
        def append_ab(self):
            self.append_a()
            self.append_b()
    
        def export(self):
            return self.string + "c"  
    

    As long as you don't access the string attribute directly, this class limits the kind of string you can build:

    1. You can start with an arbitrary stem (the argument to __init__)
    2. You can append an a to the string
    3. You can append a b to the string
    4. You can append an ab to the string (but this is just a convenient shortcut for calling append_a followed by append_b, as the implementation implies)
    5. You can end the string with c

    You get your final value by calling export (which I modified just to make the point that you cannot add a c at any point, so there's no way to follow a c with another a, for example).

    In some sense, it's kind of a dual to a regular expression: instead of recognizing whether or not a string matches the regular expression .*(a|b)*c, you create a string that will match the regular expression.