'What's the best way to design a class that calls sequences of its methods?

I have a class similar to the one below where it goes through a series of methods using variables in the class. The code used to be a massive series of functions, passing variables around and this felt more structured and easy to work with/test. However, it still feels as though there's a better way.

  1. Is there a better design pattern or approach for situations like this? Or is going the object route a mistake?

  2. In terms of testing process() and other methods, I can just mock the methods called and assert_called_once. But, ultimately, it leads to ugly testing code with tons of mocks. So, it makes me wonder about question #1 again.

class Analyzer:

    def __init__(self):
        self.a = None
        self.b = None

    def process(self):
        self.gather_data()
        self.build_analysis()
        self.calc_a()
        self.calc_b()
        self.build_output()
        self.export_data()
        ...

    def gather_data(self):
        self.get_a()
        self.get_b()
        self.get_c()
        ...
        
    def build_analysis(self):
        self.do_d()
        self.do_e()
        self.do_f
        ...

As for testing, and I know this code isn't technically right, but I just wanted to illustrate how it gets hard to read/sloppy.

        
class TestAnalyzer: 

        
    @patch.object(Analyzer, 'gather_data')
    @patch.object(Analyzer, 'build_analysis')
    @patch.object(Analyzer, 'calc_a')
    @patch.object(Analyzer, 'calc_b')
    @patch.object(Analyzer, 'build_output')
    @patch.object(Analyzer, 'export_data')
    def test_process(self, m_gather_data, m_build_analysis, m_calc_a,
                     m_calc_b, m_build_output, m_export_data):

        analyzer.process()
        m_gather_data.assert_called_once()
        m_build_analysis.assert_called_once()
        m_calc_a.assert_called_once()
        ...

Any insight or thoughts would be appreciated. Thank you!



Solution 1:[1]

Maybe the information expert design principle can help you

Assign responsibility to the class that has the information needed to fulfill it

In your example it seems like you can split your class into different ones with better defined responsibilities. Assuming that you have to perform the following functions in order:

  1. Gather data
  2. Preprocess it
  3. Analyse it

I would create a class for each of them. Here you have some example code that generates some data and performs some basic calculations:

from random import randint
from dataclasses import dataclass

@dataclass
class DataGatherer:
    path: str
    def get_data(self):
        """Generate fake x, y data"""
        return randint(0, 1), randint(0, 1)

@dataclass
class Preprocessing:
    x: int
    y: int
    def prep_x(self):
        return self.x + 1
    def prep_y(self):
        return self.y + 2

@dataclass
class Calculator:
    x: int
    y: int
    def calc(self):
        return self.x + self.y

All I have done is to divide your code into blocks, and assigned methods and attributes to fullfill the functions of those blocks. Finally, you can create an Analysis class whose only responsibility is to put the whole process together:

@dataclass
class Analysis:
    data_path: str
    def process(self, save_path: str):
        x, y = DataGatherer(self.data_path).get_data()
        print("Data gathered x =", x, "y =", y)
        prep = Preprocessing(x, y)
        x, y = prep.prep_x(), prep.prep_y()
        print("After preprocessing x =", x, "y =", y)
        calc = Calculator(x, y)
        result = calc.calc()
        print("Result =", result)
        self.save(result, save_path)
    def save(self, result, save_path: str):
        print(f"saving result to {save_path}")

In the end this is all you have to do:

>>> Analysis("data_path_here").process("save_path_here")
Data gathered x = 0 y = 1
After preprocessing x = 1 y = 3
Result = 4
saving result to save_path_here

When it comes to testing I use pytest. You can create a test file for each of your classes (eg test_datagatherer.py, test_preprocessing.py ...) and have a unit test function for each of your methods, for example:

from your_module import Preprocessing

def test_prep_x():
    prep = Preprocessing(1, 2)
    assert type(prep.prep_x()) is int

def test_prep_y():
    prep = Preprocessing(1, 2)
    assert type(prep.prep_y()) is int

I will leave you to pytest documentation for more details.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 edd313