Tackling Temporal Dependency in Classes
Tags: softwaredesign, solid, softwareengineering
Published: 2024-03-06 12:29:14

My definition of Temporal Dependency is when a part of the system/ecosystem/class needs to be aware about the order/timing of other operations. 

It exists on every category of systems in which subsystems communicate with each other. However, in this article I want to mainly focus on a very common setting: Object oriented classes. It is absolutely crazy how underrated this topic is given the mental load that it causes.

In an object-oriented class, there isn't a clear sense of in what order are the methods allowed to be executed. Except for the constructor and deconstructor/destructor, other methods do not have any constraints in the language level to be called in any specific order. 

The explained problem might not seem a significant in a small class but if you have a large number of classes that each have a large number of methods, it would be hard to control the temporal dependency. Both when you want to learn and edit a class, and also when you want to use a class's method elsewhere.

Of course there are many ways to tackle this issue. I think they fall into one of these 3 levels:
- Code Level
- Language Level
- Paradigm Level

### Code level 
Essentially, you can write code to handle the issue:

One way to tackle it is using a combination of assertions and enums/flags that contain internal state of the class. This means that for the classes that need extra care, you can define enum indicating the operational state of the class and for certain methods assert the wrong states. 
```cpp
enum class_state { s1, s2, s3 }

class a
{
    class_state state;

    void some_method()
    {
        if (state != class_state::s1) 
            return;
    }

    void some_other_method()
    {
        if (state != class_state::s2) 
            return;
    }

    void some_other_method_too()
    {
        state = class_state::s3;
    }

};
```

Another similar way would be to have represent the state of the class by bits. For example 0x00000000 is the class state that supports 8 different conditions. If the state value is 0x00000101, it means that the conditions 1 and 3 are met. Then for a method, we can check that the conditions are met via bitwise & operation:
```cpp
void some_method()
{
    // changing the current state
    state = state | 0x00100001;
}
void some_other_method()
{
    // using the current state
    if (state & 0x00011101 != 0x00011101)
        return;
}
```
This indicates that the method needs the condition of 0x00011101 to be met in order for it to be run.

Of course there are countless other ways that you can come up with. 

### Language Level
Using language's built-in features. 

For example,
- Language's class constructor is ensured to be called as the first method when an object is created. 
- Deconstructor/destructor is ensured to be called as the last method before the death of an object. 

Of course, you can choose not to use them. You can write your own "Initialize" and "Deinitialize" or kill methods. The only difference will be that the programmer needs to logically make sure to call them directly after the object creation and before the death, whereas constructor/deconstrutor is called automatically in the language's runtime.

This level's features are not in the programmers hands usually. Programmers can only use what the languages offer.

### Paradigm Level
This concerns the way you approach architecting the code. When you write procedural code, you naturally count in the temporal dependency as opposed to when you write object oriented code. 

To deal with the issue of temporal dependency, there can be middle grounds between paradigms. You can use object-oriented but mix in some procedural goodness; write classes that are traditionally "dirtier," with less number of methods and care less about "single responsibility," but instead have more procedural codes in those methods. That can be a helpful way to solve this issue to a degree. [The notion of clean and dirty is too relative, fragile, and context-dependent. It's not worth religiously obsessing over. But that's for another post.]

## What's the "best" way?
I think the "best" way to tackle the temporal dependency is a combination of ideas on the 3 levels, given what makes sense for you and your project. The paradigm level decisions provide higher chunk of the value because they affect the way you approach writing code more. And of course, there is no singular best way.

## Contexts are important
It's helpful to mention two general points:
1) Of course, this topic isn't always a problem. It depends on the context of the code itself and the development environment. The deadline, the team size, the code size, etc.
2) Temporal dependency isn't a "bad" thing. It's just an attribute that naturally exists when you have multiple instances entities that communicate with each each other. My main point is that if the temporal dependency is making the flow too hard to understand, then it might have room for being stated more clearly.