Home > front end >  Best way to apply versioning to State object-oriented design pattern?
Best way to apply versioning to State object-oriented design pattern?

Time:09-13

Main question

What are some good ways to manage state machine versions (including states themselves, state transition methods, etc) in object-oriented design, particularly during deployments when multiple versions are available?

Context/application

My team is working on re-architecting a back-end microservice with better OOP design, and we've opted to use the State pattern to model the various parts of the workflow. Execution of work in different states can often be separated by long periods of time as workflow execution is event-driven and sometimes can involve calling external services -- in other wrods: in one state, we'll call a dependency and wait until we get an asynchronous response/notification before moving to and executing work in the next state.

As such, there is a chance that we can run into a "mixed fleet" issue during software deployments when we update the state machine, the transition methods, etc. For example, suppose the "v1" state machine transitions looks like:

Start -> File Retrieved -> Hashing Complete -> File Published   -> Finished

and then suppose I want to add a new state to remove personally identifiable information (PII), which would go between File Retrieved and Hashing Complete and change the state transitions out of/into those states, respectively. The "v2" state machine transitions would now look like this:

Start -> File Retrieved -> PII Removed      -> Hashing Complete -> File Published -> Finished

During deployment of this change, I want to make sure that any requests that start with the OLD/"v1" state machine workflow continue to use that workflow, rather than transitioning to the NEW/"v2" state machine workflow as it rolls out to the fleet.

My current approach is to store a "version" property/field in the database records with each request, so that it's easy to figure out with which state machine version the request was initiated. Right now, I am stuck figuring out the best way to model the versions in code. Some ideas I had:

  • Use separate Java packages for different versions. Use a Factory pattern or something that can instantiate state objects based on whatever version number is provided. Keeps state machine implementations fully isolated, but at the cost of an explosion of classes.
  • Integrate a "version" property/field into the state classes directly. Not sure how hard this would be to manage over time, and code might get ugly.
  • Something else?

Open to any suggestions. We've also considered some alternative design patterns (Pipeline, CoR, etc) so open to exploring any of those as well.

For additional context: We're using the State pattern to make the workflows idempotent, abstract the business logic from orchestration, and (most importantly) make it extremely easy for future developers to see exactly where business logic SHOULD live and how to add to/modify the workflow going forward.

Thanks!

CodePudding user response:

I think you're making a mistake. A lot of times, this "mixed fleet" issue is going to be a feature, not a bug. In addition, this notion of "version" that you want to add creates complexity and restrictions, but adds very little value.

I think you just need to change your idea of what a state is. It's not "a step in the workflow". It's the entire remainder workflow process. That remaining process may involve multiple steps or it may not, but it's up to the state implementation to decide.

In a typical implementation of this kind of workflow system, there is a persisted "current state". When an event occurs, the current state is read from the DB and hydrated somehow into an implementation object. A handler in that object is then called to perform the appropriate action, and optionally return a new current state definition to be persisted.

The key to making a system like this comprehensible and maintainable is the design of the persisted state representation. This representation is the language that a state implementation uses to define what happens next. That language should be expressive enough to define any remaining process that might be necessary, and also simple, straightforward, and unambiguous, so that there is no confusion about how the serialized definition of "what happens next" turns into an implementation.

Your version idea ruins the "simple, straightforward" part, making it more difficult to figure out what's going to happen next in any workflow.

To make this concrete, lets consider your example...

In version 1, the start state transitions to the "RetrieveAndPublishFile" state. That state then transitions to the "PublishFileData" state. See how the name reflects the whole remainder of the process?

In version 2, you change the "RetrieveAndPublishFile" implementation so that it transitions to "CleanPiiAndPublish" instead of "PublishFileData". The "CleanPiiAndPublish" state cleans out the PII and then transitions to the "PublishFileData" state that you already have.

CodePudding user response:

State pattern is not about to add new step into behaviour, it is about change the whole behaviour without adding new steps. As wiki says about State pattern:

The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines.

If you want to add new step or behavior, then Decorator pattern can be used. As wiki says about decorator pattern:

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern. Decorator use can be more efficient than subclassing, because an object's behavior can be augmented without defining an entirely new object.

So it looks like Decorator pattern is better fit for your requirements.

Keeps state machine implementations fully isolated, but at the cost of an explosion of classes.

yeah, if you add new step, then it is better to add new class. Yeah, the cost is an explosion of classes. On the other hand, your classes will have:

  • Related