I work on a survey that has a lot of questions. This means we have a lot of columns/variables to work with. This translates to a lot of conditionals that have to be done a certain way according to a specification. The implementation in the code can be different, but it has to map to the spec. We are moving toward Python, and I've tried to think of ways to make the spaghetti code easier to wrangle and change when needed. At the same time, I need to point to the spec and say here it is in the code.
I've considered the strategy patterm or "chain of responsibility", and I was introduced to the specification pattern. But anything that requires creating a class for a condition or for a type of person, for example, feels like it won't work. I feel like it would result in unmanageable complexity. It's already tough because the data model is complex, and has so many columns to work with.
Current code can have points where we have if statements nested 10 deep. To me that's really tough to look at and make changes to, but a lot of people find that easier than what I like to do.
One solution I'm considering is to create a function for each check in the spec.
For example, I can give a simple example of a piece of spec.
Check1 (Age < 18) -True->
Check2 (Sex = Male) -True ->
Set description = "Male Under 18"
One possibility is to flatten that arrow anti-pattern, which speeds up the Python program. For example:
universe = (interviewed_person == "yes")
check1 = (age < 18)
check2 = (sex = "Male")
if (universe & check1 & check2):
set description = "Male Under 18"
Functions would allow us to do unit testing for each check and more easily track program edit flow. I had a mistake in my real example that I found when I did this way. With these complicated specs, I know there's only one or two ways to get to a certain point in the spec. The only way to get to Check1 is if the person is in universe. The only way to get to Check2 is if Check1 is true. The below program is an example of that, just imagine if it's 200 methods x 200 edits. The below prints the following to the terminal or command line: stop True Male Under 18
class Person:
def __init__(self):
self.job= -9
self.age = 17
self.sex = "Male"
self.interviewed_person = "yes" #for the example, let's say everyone's in universe
class Logic:
def __init__(self):
#track state value with another class?
state, condition = self.universe(p.interviewed_person)
state, condition = self.check1(state, condition)
state, condition = self.check2(state, condition)
state, condition = self.set_description(state, condition)
print(state, condition, p.job)
def stop(self):
return "stop", True
def universe(self, state):
if (state == "yes"):
return "universe", True
#putting else in here to show that we may have False conditions that lead elsewhere
def check1(self, state, condition):
if ((state == "universe") & (condition == True)):
if (p.age < 18):
return "check1", True
else:
return "check1", False
def check2(self, state, condition):
if ((state == "check1") & (condition == True)):
if (p.sex == "Male"):
return "check2", True
else:
return "check2", False
def set_description(self, state, condition):
if ((state == "check2") & (condition == True)):
#I've worked this with my data in objects, but I may go with dataframes
p.job="Male Under 18"
return self.stop()
return state, condition
if __name__ == "__main__":
p = Person()
l = Logic()
I didn't find the following link helpful for my situation: Style for control flow with validation checks
I think the specification pattern would end up with too many classes and be unmanageable. If I have hundreds of checks, like Check1 - Check200 times 200, I think it would add a lot of lines of code. Is there another way?
chain of responsibilities
pattern. A CoR pattern consisting of 1-200 small classes is (and will always be) better (or more convenient) than 200 functions (with temporal coupling) coupled here and there causing hundreds or thousands of different execution paths you won't be able to test properly by unit tests. Not to mention that the links of any CoR can be configured in runtime. The path is not static, something you can't achieve with 200 functions and temporal coupling.