0

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?

user1981275
  • 13,002
  • 8
  • 72
  • 101
  • 2
    `AA` is just procedural programming dressed up as OOP. The class doesn't serve any real purpose. – chepner Nov 04 '21 at 01:20
  • `A` is roughly an implementation of the [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) (or at least has a bit in common with it). – chepner Nov 04 '21 at 01:22
  • My problem with `A` is that it would it would not even matter if all functionality was done in the constructor... – user1981275 Nov 04 '21 at 01:28
  • My problem with `AA` is that is in fact not OOP and if I do all the steps in e.g. a __main__ method, it would look a bit messy with passing the string in and out... – user1981275 Nov 04 '21 at 01:29
  • 1
    The point of `A` is that you are deliberately hiding the fact that it wraps an ordinary string. The interface "restricts" the user from doing anything to the initial argument other than appending an `a` or a `b`, until it's time to get the final result. (If the only thing you intend to do is create an instance, call `dothing`, then call `export`, then yes, this class is also an overengineered version of a regular function that would take a string and return the same string with `ab` appended to it.) – chepner Nov 04 '21 at 01:30
  • 1
    Whatever you do, don't write a class with all static methods, that's something that shouldn't be a class, but just a bunch of functions – juanpa.arrivillaga Nov 04 '21 at 03:03

1 Answers1

1

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.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • My problem with both `A` and `AA` is that they both kind of look like I stuff some code into classes for the sake of having classes. – user1981275 Nov 04 '21 at 01:41
  • So in my real application, I query some stuff from an API, get a JSON response, convert the JSON to a pandas table, do some cleaning of the table, and save the table to file. The raw response is not really of interest for the user of this class. So I guess I would then more tend to `A`? – user1981275 Nov 04 '21 at 01:44
  • So with the above application there would be actually a well-defined sequence of functions to call. – user1981275 Nov 04 '21 at 01:46
  • 1
    Yes, `AA` is a classic example of how *not* to use classes. But whether you successfully implement your workflow as a class depends on what you do, exactly. Not everything fits naturally into an OOP pattern. (I'm looking at you, Java.) In your example, you are passing some data from one method to another, but the order in which you can is quite prescribed: you have to query the API, then get a response, the convert that JSON response, etc. You can't skip steps, you might not need to repeat any steps, etc. – chepner Nov 04 '21 at 01:47
  • Ok, I think I will in this case opt for `A`, but keeping in mind that I stuff some procedural pattern into a class, in order to make my code more readable and easier testable. I agree that some problems do not naturally fit into OOP! – user1981275 Nov 04 '21 at 02:02