Translate workflow into state machine with respect to SOLID and OOP

108 views Asked by At

Lets say you start with a method that accepts two complex objects. This method is some sort handler and it needs to process workflow (see workflow flowchart)

public Task Handler(Object A, List<object> lisftOfObjectsB)
{
    //execute workflow steps here 
}

You can do this by using statements like 'if', 'if-else', 'switch' etc. But if you program it that way you will end up with messy code and you will probably violate at least one of SOLID principles (open-close principle for example). How can you program a workflow with respect to SOLID principles and using OOP instead of using many different if, if-else, switch ect statements ? enter image description here

public Task Handler(Object A, List<object> lisftOfObjectsB)
{
    bool inDb = IsAInDatabase();
    if(inDb == false)
    {
         //Add to DB
    }
    else
    {
         bool hasLastState = CheckForLastState(A);
         if(hasLastState == false)
         {
             //Add laststate
         }
    }
    ....
 }

If you do it this way you will end up with many different if/else/for/for-each statements and imagine if workflow will have much more steps and YES/NO decisions.

1

There are 1 answers

0
Morten Bork On BEST ANSWER

Think of your code as a scientific article, in a paper:

You have a "main" purpose of the contents, you need the reader to understand that you have some sort of message you need to convey to the reader.

So you have a "main" method, that is your starting point. Now you need to create the first abstract layer -> What do I want to tell the reader of my code, that it is doing?

In your workflow, you have two classes A and B. Now there doesn't appear to be a connection between A and B, so the first mistake, is to link the two. You do not need to wait for A to finish before you do B. There is no dependency on handling object A before B.

So you should first of all, split your workflow in two. One for A, one for B. (If there is one (or more), okay, but then we will need to know the dependency between A and B)

Then you make your abstractions: (This is pseudo code, and assumes you are using DI)

   public interface IObjectAVerifier{
      bool VerifyObjectAInPersistence(Object a);
    } 

    public interface IPersistenceLayerVerifier(object a)
    {
       IInsertAIntoPersistence VerifyIsInPersistence(a);
    }

    public interface ILastStateChecker(Object a){
       
    }

    public interface IListObjectValidator{
      ValidateListOfObjectBInPersistence(List<Object> objectList);
    }

    public interface IInsertAIntoPersistence {

    }

Now you Main method reads:

public static void Main(string[] args) {
   object a = InitializeA();
   List<object> list = InitializeList();

   IObjectAVerifier objectAVerifier = new ObjectAVerifier();
   IListObjectValidator listValidator = new ListObjectValidator();

   ojbectAVerifier.VerifyObjectAInPersistence(a);
   listValidator.ValidateListOfObjectBInPersistence(b)
}

You then implement:

public class ObjectAVerifier : IObjectAVerifier {
   private readonly IPersistenceVerifier _persistenceVerifier;
   private readonly ILastStateChecker _lastStateChecker;
   

   public ObjectAVerifier(IVerifyAIsInPersistenceLayer verifyAIsInPersistenceLayer, ILastStateChecker lastStateChecker, DbContext context)
{
    _persistenceVerifier = verifyAIsInPersistenceLayer;
    _lastStateChecker = lastStateChecker;
    

}

  public bool VerifyObjectAInPersistence(object a) {
      IInsertAction action = IPersistenceLayerVerifier.VerifyIsInPersistence(a);
    

  }

}

public class PersistenceLayerVerifier : IPersistenceLayerVerifier {
   private DbContext _context;

   public PersistenceLayerVerifier(DbContext context)
{
_context = context;
}

   public IInsertAIntoPersistence VerifyIsInPersistence(object a)
   {
       if(_context.Set<A>().contains(a))
       {
          return new NullObjectInserter(); // this implementation does nothing.
       }
       return new InsertAIntoPersistence(a); //this implementation does an actual insert


   }
}

This is just a super simplistic and partial example of what you could do, to convert your workflow into OOP and SOLID programming. I hope this is enough to get the ball rolling? Other ask, and I will extend my answer.

The trick is to move all actual if/else checks into factory methods, or similar concepts, or remove them entirely where possible.

Whenever you feel like a name to your process could contain an "And" Like ProcessAandB => DO NOT USE SUCH A NAME. Find a way to split the two actions up into to methods, at a lower abstraction layer.

A method with a return type, can only have a return type, NO OTHER ACTIONS ARE ALLOWED. no side effects, no effects, no nothing.

When you require a (side) effect, make sure it happens in a void method. This will help you split up your methods into smaller methods. (This applies to leaf methods in your execution tree. Not higher abstract layers, where you are merely displaying top level abstract concepts.)

SOLID is not a "how to" map a workflow into code. It is a set of principles to follow for the code itself, when your brain creates the code from a workflow.

Every decision you see => some sort of conceptual factory. Every process on the chart => some sort of method.

It becomes easier to spot if you follow the naming convention:

Variables and Types must be nouns. Methods must be Verbs.

So you could have a Class Coin. Interface ICoin, Variable coin.

And a method on it called Flip()

That is fine. But you couldn't have a class called CoinFlip.Flip()

Just look at it? Action.Action? wtf? Is the method flipping? Is the class flipping? Where is the responsibility for the actual flipping being done?

Make you code readable. So that you can read it, like you would an instruction manual, or article.

Then your code becomes self-documenting.

Also, you start thinking about, if your code is handling multiple things, that might confuse the reader.

For example: Would you count coin flips on a coin? I could make sense in a certain context, but perhaps the coin results, need to be recorded together with other coins?

Is it then relevant to store the individual result on the individual coin? Or in a Class called "ScoreKeeper" That is an instance on a Class called "FlipHandler", that can accept a list of Coins to flip.

It's all about how the program should handle the responsibility of the required actions the program must be responsible for, and how to implement the code in a way, where you can extend your code, instead of modifying it.

This requires a lot of thought as to HOW you write your code, more than what your workflow diagram will require of you.

Your question is basically ->

How do I translate the IKEA instructions I received into the carpentry knowledge required to make the components of the provided diagram.

The two are connected, but not directly. The Carpentry bit, in the analogy is the coding itself, IKEA instructions your workflow.