vit_r: default (Default)
vit_r ([personal profile] vit_r) wrote2024-01-13 01:51 am

Про ёжика в тумане, зазнайство, языки программирования и вериги

Hilarius 2024

The State Transition Table of the Agile Age


The final version of the following text is going to be shown to some people who do not know about my blog. It is written in English, and it is a bit boring.

Many years ago, I was an experienced expert in software development. I was mature, effective, and knowledgeable. However, unlike the book-smart experts who did not make anything with their own hands, I was unsatisfied with the state of the software development industry.

The methods from wise books did not work in real-life projects. I had set myself the goal of writing a practically applicable book. During some years, I collected, analyzed, refined and extended methods that could ensure the efficient production of reliable software.

Unfortunately, I was too naive. Fortunately, it did not take me too much time and effort to understand that increasing the efficiency of software development turns it into a boring job, and producing reliable software is an economically inefficient business.

This was the best time to start a profitable consulting business by selling castrated methods that could not disturb the overall inefficiency but only introduce pretty insignificant improvements. This approach promised a brilliant career path, but I was unhappy that I would have to sell shit, even if it could be sold for the price of gold.

Today I know what prevents the industry from making potential dramatic improvements, but now I am a wise man, and I also know that sharing this knowledge is a silly idea.

Sometimes my old attempts to improve the world emerge from the darkness of the past. During a recent discussion under my old post (Про зазнайство, жестокосердие и утерянные технологии / 10 kB / 2012-10-26), I had realized that my understanding of the Shlaer-Mellor state transition tables has improved with only one sentence. This is a missed key that did not allow me to open many treasure chests, which I thought was important to investigate.

This rather useless knowledge would be of interest to some software engineers. It would be a fun task for me to write down a short explanation that I could probably read and consider naive after another ten years.

You do not need any specific knowledge to understand the main ideas, but you do need a monitor of a developer-friendly size to comfortably observe all the details. You could read the posts I mention here to get a deeper understanding, but it is also optional.

Here is a slightly modified state transition table (STT) from my old post.

private static final int[][] STATE_TRANSITION_TABLE  = {
    //================================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED | LEVEL_OK  |
    //================================================================================================|
    /*  START        */   { CHECK_PRECOND , _ignore       , INT_ERROR     , INT_ERROR     , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------|
    /*  PRECOND_OK   */  ,{ INIT          , REGISTRATION  , INT_ERROR     , WAIT_ACCEPTED , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------|
    /*  PRECOND_FAIL */  ,{ INIT          , INIT          , INT_ERROR     , INT_ERROR     , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------|
    /*  REG_DONE     */  ,{ _ignore       , CHECK_PRECOND , WAIT_ACCEPTED , INT_ERROR     , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------|
    /*  REG_FAIL     */  ,{ _ignore       , INT_ERROR     , INIT          , INT_ERROR     , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------|
    /*  ACCEPTED     */  ,{ INT_ERROR     , INT_ERROR     , INT_ERROR     , LEVEL_OK      , _ignore   }
    //-------------------++---------------+---------------+---------------+---------------+-----------|
    };


You do not need to decode it because I start with a step-by-step explanation.

Let's say some modern agile software developer, Mr. N, speaks with a user and creates a happy path solution.

           INIT
            |
            |
            V
      CHECK_PRECOND[itions]
            |
            |
            V
       REGISTRATION  
            |
            |
            V
      WAIT_[registration_is_]ACCEPTED
            |
            |
            V
        LEVEL_OK


We have a finite state machine (FCM) with 5 states. It doesn't matter what they mean. This sample is abstract. The state "INIT" is the startinitial state, and the state "LEVEL_OK" is the final state.

Before introducing the happy path state transition table, I'll share here one useful trick: I add colors.

          INIT 
            |
            |
            V
      CHECK_PRECOND 
            |
            |
            V
       REGISTRATION  
            |
            |
            V
      WAIT_ACCEPTED 
            |
            |
            V
        LEVEL_OK 


People who have been reading my blog long enough could remember that I print tables and analyze complex data by applying watercolor. (Анализ данных акварелью / / 2015-06-27) I will use colors here only to guide attention. I have randomly taken some colors from a table with HTML color names instead of a thoughtful selection of matching hues and shades. The correct solution for a satisfactory color composition would demand too much time.

The happy path state transition table is a simple consequence of 4 signals. Each signal switches the state machine to the next state.

private static final int[][] HAPPY_PATH_STT  = {
    //====================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED |
    //====================================================================================|
    /*  START        */   { CHECK_PRECOND ,               ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  PRECOND_OK   */  ,{               , REGISTRATION  ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  REG_DONE     */  ,{               ,               , WAIT_ACCEPTED ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  ACCEPTED     */  ,{               ,               ,               , LEVEL_OK      }
    //-------------------++---------------+---------------+---------------+---------------|
};


Please note, I could use the terms "event" or "message" instead of "signal", but these terms could lead some developers astray because they have specific meanings in some specific areas of software development. Software developers usually like to be specific, and this prevents thinking.

The word "signal" is rarely used.

Of course, this table is useless because it contains holes for many state-signal combinations and the software compiler would report errors, but this example is nothing more than an abstract projection of some source code onto a sequence of state transitions of some finite state machine.

The Shlaer-Mellor method models real-world problems in the form of an ensemble of interacting finite state machines. This is only a toolbox that can be effectively used by an experienced master, but it is pretty useless in the hands of an amateur. You must gather knowledge and experience to select suitable tools and apply them in the right way

The Shlaer-Mellor method was recently renamed to Executable UML, but this rebranding did not help. It is quite unknown in the modern software development industry, which proclaims the slogan: "We are looking for new solutions, not old knowledge!"

The state transition tables were used for designing electromechanical devices even before computers emerged. They are as reliable as a hammer. On the other hand, even a perfect hammer is unproductive if you try to use it instead of a screwdriver or an electric drill.

The Shlaer-Mellor method was created to be used by engineers who can simultaneously balance many real-world project limitations, and this makes it unsuitable for the modern software development industry, which demands not perfect tools for professionals but silver bullets that you can shoot in all directions with your eyes closed.

The Shlaer-Mellor method and the modern software-development industry will not be the subject of my further explanations. My intention is only to introduce one nontrivial feature of the state transition tables specific to the Shlaer-Mellor method.

The meaning of the state transition table above is obvious.

If the state machine is in the state "INIT" and it receives a signal "START", it enters into the state "CHECK_PRECOND".

If the state machine is in the state "CHECK_PRECOND" and receives the signal "PRECOND[itions]_OK", it enters into the state "REGISTRATION".

The next state transitions are initiated by the signals "REG[istration]_DONE" and "[registration_]ACCEPTED".

The state "LEVEL_OK" is a final state. The registration process has been completed successfully, and the finite state machine cannot return to any other state.

There are 2 kinds of finite state machines (Moore and Mealy). The Shlaer-Mellor method uses the version that calls a function by entering a new state.

This can be represented by the following pseudocode:

signal_waiting_cycle {

   new_signal = receive_signal( );

   new_state = STATE_TRANSITION_TABLE [ current_state ] [ new_signal ] ;

   call_entry_function[ new_state ]();

} // wait new signals


A pseudo-code for an entry function can be represented as follows.

entry_function_for_some_state() {

    process_data_that_was_received_with_a_signal();

    read_some_internal_data();
    
    do_something();

    prepare_outputs();

    send_output_signals();

    return;

} // end of entry_function_for_some_state()


Our Mr. N is a modern creative agile software developer. Of course, he does not bother himself with useless knowledge about an obsolete Shlaer-Mellor method — we can assume that he does not even know this name —, he quickly implements the happy path solution, informs his manager that the task is successfully completed, and begins coding something new.

Agile means always being in some kind of movement.

Unfortunately, the real world is unhappy with his optimistic solution. It sends him — through the software test department — two disappointing signals: "PRECOND[ition]_FAIL[ure]" and "REG[istration]_FAIL[ure]".

Mr. N is a true agile developer, and now he is completely immersed in new interesting tasks. He starts the "I have no time" game, but his manager somehow forces him to add the processing of these signals.

Please note, this manager is not agile. An agile manager would escalate this problem to the next level, and there he would start the "We have too many tasks; we need more staff" game, but I let this slide.

Mr. N struggles and adds some source code to handle these situations. Of course, he is agile, which means that he starts coding before he starts thinking, and he obviously does not waste time to draw something, but we can analyze his source code and represent it as a diagram.


  .--> INIT <---------------.
  |     |                   ^
  |     |                   |
  |     V                   |
  | CHECK_PRECOND ---> REGISTRATION  
  |         |               |
  |_________|               |               
                            V
                      WAIT_ACCEPTED
                            |
                            |
                            V
                         LEVEL_OK


The correspondent state transition table gets 2 new rows.

private static final int[][] NOT_VERY_HAPPY_PATHS_STT  = {
    //====================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED |
    //====================================================================================|
    /*  START        */   { CHECK_PRECOND ,               ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  PRECOND_OK   */  ,{               , REGISTRATION  ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  PRECOND_FAIL */  ,{               , INIT          ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  REG_DONE     */  ,{               ,               , WAIT_ACCEPTED ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  REG_FAIL     */  ,{               ,               , INIT          ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  ACCEPTED     */  ,{               ,               ,               , LEVEL_OK      }
    //-------------------++---------------+---------------+---------------+---------------|
    };


Mr. N is proud of his achievement. The software test department also cannot find any problems, and the code goes into production.

After a while, users begin to report that the behavior is incorrect. Mr. N is forced to return to his code. He finds the source of these problems and is annoyed by the stupidity of the users. He explains to his manager that they should patiently wait for the results to appear on the screen instead of pressing the same button twice.

The manager contacts the users and explains to them the wise agile solution. However, users are not satisfied, and after several months of discussions, Mr. N agrees that sometimes users may press a button twice to force the state machine to re-enter the same state again. He adds some conditions and additional processing, which can be reflected in the following state transition table.

private static final int[][] RE_ENTRY_STT  = {
    //====================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED |
    //====================================================================================|
    /*  START        */   { CHECK_PRECOND ,               ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  PRECOND_OK   */  ,{ INIT          , REGISTRATION  ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  PRECOND_FAIL */  ,{ INIT          , INIT          ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  REG_DONE     */  ,{               , CHECK_PRECOND , WAIT_ACCEPTED ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  REG_FAIL     */  ,{               ,               , INIT          ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  ACCEPTED     */  ,{               ,               ,               , LEVEL_OK      }
    //-------------------++---------------+---------------+---------------+---------------|
    };


Mr. N already hates his code, but the real world does not free him from it. Sometimes, in some cases, something abnormal happens within the software.

Mr. N is an experienced software developer. He conducts unit tests, and after only two weeks of investigations finds out that something unexpected can happen in any unexpected state. He reinvents the wheel and adds some code that is equivalent to the "assert()" function routinely used in mission-critical software.

His diagram — which, as we have mentioned, does not exist even in his head — looks now as follows:

*****************************************
*                                       *
* .---> INIT <--------------.           *
* |     |                   ^           *
* |     |                   |           *
* |     V                   |           *
* |  CHECK_PRECOND ---> REGISTRATION    *
* |         |               |           *
* |_________|               |           *
*                           V           *
*                       WAIT_ACCEPTED   *
*                           |           *
*                           |           *
*                           V           *
*                        LEVEL_OK       *
*                                       *
*****************************************
                    |
                    |
                    V
                INT_ERROR 


The correspondent state transition table gets the last row with the new signal: "ASSERT_FAIL[ure]".

private static final int[][] REAL_LIFE_STT  = {
    //====================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED |
    //====================================================================================|
    /*  START        */   { CHECK_PRECOND ,               ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  PRECOND_OK   */  ,{ INIT          , REGISTRATION  ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  PRECOND_FAIL */  ,{ INIT          , INIT          ,               ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  REG_DONE     */  ,{               , CHECK_PRECOND , WAIT_ACCEPTED ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  REG_FAIL     */  ,{               ,               , INIT          ,               }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  ACCEPTED     */  ,{               ,               ,               , LEVEL_OK      }
    //-------------------++---------------+---------------+---------------+---------------|
    /*  ASSERT_FAIL  */  ,{ INT_ERROR     , INT_ERROR     , INT_ERROR     , INT_ERROR     }
    //-------------------++---------------+---------------+---------------+---------------|
    };


Mr. N has completed his role in my explanation. He has reached his limits through some months and several software releases. His code is in production, and nobody dares to touch it.

Sometimes users report some strange behavior, but the software testing department rejects them because the error conditions cannot be reproduced. The software is almost perfect, and users are almost satisfied.

However, the state transition table still cannot be compiled because it contains holes. Now I close them in the Shlaer-Mellor's manner.

I repeat here the final diagram without drawing arrows for re-entry cases.

*****************************************
*                                       *
* .---> INIT <--------------.           *
* |     |                   ^           *
* |     |                   |           *
* |     V                   |           *
* |  CHECK_PRECOND ---> REGISTRATION    *
* |         |               |           *
* |_________|               |           *
*                           V           *
*                       WAIT_ACCEPTED   *
*                           |           *
*                           |           *
*                           V           *
*                        LEVEL_OK       *
*                                       *
*****************************************
                    |
                    |
                    V
                INT_ERROR 


Please note, that the fully correct diagram would contain arrows from each state to the "INT_ERROR" state, but this would make the entire diagram unreadable.

I know how to resolve this contradiction, but that is another topic. We just have to remember that Shlaer-Mellor finite state machines are flat, and this box of stars is not a nested state but nothing more than a simplified graphical representation.

The "INT_ERROR" state is the hidden knowledge of the Shlaer-Mellor method. It is silently hidden behind every state, but it is almost never shown on diagrams. Usually, there is also no signal such as "ASSERT_FAIL".

The "INT_ERROR" state and the "ASSERT_FAIL" signal are specific. They represent a specific behavior that breaks the boundaries of a single finite state machine, which is independent and logically separated from the context.

The entry function for the "INT_ERROR" state can be represented in the following pseudocode:

entry_function_for_INT_ERROR_state() {

    log_error_information();

    preserve_log();

    set_the_whole_system_in_a_guaranteed_stable_state();

} // end of entry_function_for_INT_ERROR_state() 


The functions that save and preserve critical information are also specific and have the highest safety requirements. In some cases, a log entry is sufficient, and the process restarts from a predefined stable state. Sometimes the whole software must stop and restart anew. Sometimes a physical system controlled by this software — for example, a train or a missile — must be stopped or even destroyed.

We could say that the "ASSERT_FAIL" signal is a signal that is sent to the state machine in case of fatal errors, and the state machine first processes this signal, ignoring all signals that could be received early, but such a description would be overly complicated. This behavior is usually implemented as an "assert()" function.

some_assert_function( for some condition X ) {

    if( the condition X is not fullfilled ) {
        
        enter_INT_ERROR_state();

    } // if error

    // else

    return;

} // end of some_assert_function()


Now we can start with the Shlaer-Mellor method. The real-world design process is interactive, but here I assume that after drawing draft diagrams, we have defined all the states and all the signals. Yet, we can create an empty table.

private static final int[][] EMPTY_STT  = {
    //============================================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED | LEVEL_OK  | INT_ERROR |
    //============================================================================================================|
    /*  START        */   {               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_OK   */  ,{               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_FAIL */  ,{               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_DONE     */  ,{               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_FAIL     */  ,{               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ACCEPTED     */  ,{               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ASSERT_FAIL  */  ,{               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    };


The next step is to add the standard error handling. It is banal.

private static final int[][] INT_ERROR_STT  = {
    //============================================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED | LEVEL_OK  | INT_ERROR |
    //============================================================================================================|
    /*  START        */   {               ,               ,               ,               ,           , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_OK   */  ,{               ,               ,               ,               ,           , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_FAIL */  ,{               ,               ,               ,               ,           , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_DONE     */  ,{               ,               ,               ,               ,           , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_FAIL     */  ,{               ,               ,               ,               ,           , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ACCEPTED     */  ,{               ,               ,               ,               ,           , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ASSERT_FAIL  */  ,{ INT_ERROR     , INT_ERROR     , INT_ERROR     , INT_ERROR     , INT_ERROR , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    };


The bottom right corner may represent an interesting problem because you could have an assert() function in the "INT_ERROR" state entry function, and this assert() may fail and generate an "ASSERT_FAIL" signal. This could lead to an infinite cycle of attempts to handle such error.

We assume that the error handling is tested and almost completely reliable.

Usually, the last row and the last column are hidden in a state transition table because they represent obvious standard behavior. Object-oriented design usually derives all finite state machines from a single base class, which contains standard error handling.

This means that the state transition table can be represented in a collapsed state.

private static final int[][] BASE_FSM_CLASS_STT  = {
    //================================================|
    //                                    | INT_ERROR |
    //================================================|
    // /*********************************/            |
    // /*         Derived Table         */, INT_ERROR }
    // /*********************************/            |
    //-------------------++---------------+-----------|
    /*  ASSERT_FAIL  */  ,{ INT_ERROR     , INT_ERROR }
    //-------------------++---------------+-----------|
    };


Almost all the tables that you could see in real-life examples would be derived tables. However, you should always be aware that a derived table can be inserted into a full table.

A typical state transition table also does not contain columns for final states. They are usually implemented in a specific way, and all signals that are received after a state machine enters the final state are usually ignored. Let's say, we need a column for the "LEVEL_OK" state because all signals except "ACCEPTED" should produce an internal error, but the "ACCEPTED" signal must be only ignored.

Now we start with the first column and add the information that we already know.

    //=====================================
    //                   || INIT          |
    //=====================================
    /*  START        */   { CHECK_PRECOND ,
    //-------------------++---------------+
    /*  PRECOND_OK   */  ,{ INIT          ,
    //-------------------++---------------+
    /*  PRECOND_FAIL */  ,{ INIT          ,
    //-------------------++---------------+
    /*  REG_DONE     */  ,{               ,
    //-------------------++---------------+
    /*  REG_FAIL     */  ,{               ,
    //-------------------++---------------+
    /*  ACCEPTED     */  ,{               ,
    //-------------------++---------------+
    /*  ASSERT_FAIL  */  ,{ INT_ERROR     ,
    //-------------------++---------------+


We now have a table column with 3 empty cells.

The standard approach is to forget about them and say: "Let's fill in the next column."

The Shlaer-Mellor approach is to stop at each cell and ask: "What must happen if the state machine is in the state X, and it has received the signal Y?"

Sometimes the answer is simple, and you stop for a few seconds to remember information you already know, but otherwise would not have noticed. Sometimes the answer demands further investigations and long discussions.

This is the disadvantage of such methods. You can close the cell with something you have randomly guessed and go further, but you would know that you have left some shit behind.

Let's say, we have discussed all the possibilities, and found out that the "REG_DONE" and "REG_FAIL" signals can be received under some conditions, but this is not important: both signals can be silently ignored.

The correspondent cells get the value "_ignore" which denotes, that the state machine remains in the same state and nothing should happen.

Please note that this is not the same ignorance as in the agile example above. We know that these signals can be received, and we have decided that these signals should be ignored, and this knowledge is documented.

The last cell corresponds to the "ACCEPTED" signal. It seems impossible that this signal could be received, and we cannot think of conditions under which this assumption could be wrong.

The cell gets the "INT_ERROR" state. The first column is now completely filled.

    //=====================================
    //                   || INIT          |
    //=====================================
    /*  START        */   { CHECK_PRECOND ,
    //-------------------++---------------+
    /*  PRECOND_OK   */  ,{ INIT          ,
    //-------------------++---------------+
    /*  PRECOND_FAIL */  ,{ INIT          ,
    //-------------------++---------------+
    /*  REG_DONE     */  ,{ _ignore       ,
    //-------------------++---------------+
    /*  REG_FAIL     */  ,{ _ignore       ,
    //-------------------++---------------+
    /*  ACCEPTED     */  ,{ INT_ERROR     ,
    //-------------------++---------------+
    /*  ASSERT_FAIL  */  ,{ INT_ERROR     ,
    //-------------------++---------------+


The next column is filled in according to the same principle: We can ignore only what we are allowed to ignore. Anything unexpected initiates a transaction into the "INT_ERROR" state.

I skip other steps and place here the final Shlaer-Mellor solution.

*****************************************
*                                       *
* .---> INIT <--------------.           *
* |     |                   ^           *
* |     |                   |           *
* |     V                   |           *
* |  CHECK_PRECOND ---> REGISTRATION    *
* |         |               |           *
* |_________|               |           *
*                           V           *
*                       WAIT_ACCEPTED   *
*                           |           *
*                           |           *
*                           V           *
*                        LEVEL_OK       *
*                                       *
*****************************************
                    |
                    |
                    V
                INT_ERROR 



private static final int[][] FULL_STATE_TRANSITION_TABLE  = {
    //============================================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED | LEVEL_OK  | INT_ERROR |
    //============================================================================================================|
    /*  START        */   { CHECK_PRECOND , _ignore       , INT_ERROR     , INT_ERROR     , INT_ERROR , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_OK   */  ,{ INIT          , REGISTRATION  , INT_ERROR     , WAIT_ACCEPTED , INT_ERROR , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_FAIL */  ,{ INIT          , INIT          , INT_ERROR     , INT_ERROR     , INT_ERROR , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_DONE     */  ,{ _ignore       , CHECK_PRECOND , WAIT_ACCEPTED , INT_ERROR     , INT_ERROR , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_FAIL     */  ,{ _ignore       , INT_ERROR     , INIT          , INT_ERROR     , INT_ERROR , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ACCEPTED     */  ,{ INT_ERROR     , INT_ERROR     , INT_ERROR     , LEVEL_OK      , _ignore   , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ASSERT_FAIL  */  ,{ INT_ERROR     , INT_ERROR     , INT_ERROR     , INT_ERROR     , INT_ERROR , INT_ERROR }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    };


Let's return to the modern agile world.

Our agile software developer, Mr. N, has left the company for a better position at another company. The old manager has also moved somewhere. A new software developer, Mr. D, starts his job at the company, and he is assigned a new old task: The source code implemented by Mr. N produces too many errors, users complain, and this needs to be taken care of.

Mr. D begins intensive testing, and after some weeks, he discovers that a specific condition for a specific re-entry has not been discovered. We can project this problem onto our state transition table as follows: When the state machine is in the "WAIT_ACCEPTED" state and a "PRECOND_OK" signal is received, the state machine should not report an error but should re-enter the "WAIT_ACCEPTED" state.

Mr. D is happy and proud of himself. He adds the new source code and also performs a small agile refactoring: He removes the old error handling. Unfortunately, these changes affect the whole processing of the "ASSERT_FAIL" signal. Simply speaking, the last row is now empty, and the state machine does not react if something unexpected happens in any state.

Of course, some users still complain, but the software test department cannot reproduce the error conditions. Consequently, testers do not bother developers with error reports. The source code which contains the error logging is now inactive because the "ASSERT_FAIL" signal is never called.

The real-life state transition table in an agile project could be represented as a table full of holes.

private static final int[][] FULL_OF_HOLES_STATE_TRANSITION_TABLE  = {
    //============================================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED | LEVEL_OK  | INT_ERROR |
    //============================================================================================================|
    /*  START        */   { CHECK_PRECOND ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_OK   */  ,{ INIT          , REGISTRATION  ,               , WAIT_ACCEPTED ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_FAIL */  ,{ INIT          , INIT          ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_DONE     */  ,{               , CHECK_PRECOND , WAIT_ACCEPTED ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_FAIL     */  ,{               ,               , INIT          ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ACCEPTED     */  ,{               ,               ,               , LEVEL_OK      ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ASSERT_FAIL  */  ,{               ,               ,               ,               ,           ,           }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    };


Please note, the table above represents the knowledge of the agile developer, Mr. D. We may know more, and we may also disagree with him.

From the point of view of a Shlaer-Mellor expert, the table above does not contain empty cells. These cells are filled with transitions into a specific state.

I had searched for a word in Eastern religious philosophy but could not find suitable candidates. I would use the Russian term "_pokhuy" which means a state of complete arrogant ignorance despite any previous evidence, any experience, and any severity of possible consequences.

The correct representation of a state transition table in an agile project is mostly filled with transitions into this state.

private static final int[][] AGILE_STATE_TRANSITION_TABLE  = {
    //============================================================================================================|
    //                   || INIT          | CHECK_PRECOND | REGISTRATION  | WAIT_ACCEPTED | LEVEL_OK  | INT_ERROR |
    //============================================================================================================|
    /*  START        */   { CHECK_PRECOND , _pokhuy       , _pokhuy       , _pokhuy       , _pokhuy   , _pokhuy   }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_OK   */  ,{ INIT          , REGISTRATION  , _pokhuy       , WAIT_ACCEPTED , _pokhuy   , _pokhuy   }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  PRECOND_FAIL */  ,{ INIT          , INIT          , _pokhuy       , _pokhuy       , _pokhuy   , _pokhuy   }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_DONE     */  ,{ _pokhuy       , CHECK_PRECOND , WAIT_ACCEPTED , _pokhuy       , _pokhuy   , _pokhuy   }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  REG_FAIL     */  ,{ _pokhuy       , _pokhuy       , INIT          , _pokhuy       , _pokhuy   , _pokhuy   }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ACCEPTED     */  ,{ _pokhuy       , _pokhuy       , _pokhuy       , LEVEL_OK      , _pokhuy   , _pokhuy   }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    /*  ASSERT_FAIL  */  ,{ _pokhuy       , _pokhuy       , _pokhuy       , _pokhuy       , _pokhuy   , _pokhuy   }
    //-------------------++---------------+---------------+---------------+---------------+-----------+-----------|
    };


Of course, agile software would work under conditions that are (correctly) considered in the source code. However, any unpredicted combination of external or internal conditions will put this software into a state of unpredictable uncontrolled unknown behavior.

This "_pokhuy" state is also the final state. A transition from a state with unknown behavior into some stable and predictable state is impossible because no one can predict which external and internal data were unpreductably incorrectly changed.

Please note, there is nothing wrong with this table. Unless you are paying for this shit, you must appreciate the modern approach because the "_pokhuy" state is a great source of exciting work for users, testers, managers, and software developers. It generates a nearly infinite amount of work hours and supports the growing profits of the modern agile industry.

Next, I finally move on to the mentioned sentence which has been added to my understanding. I could have written this sentence at the beginning, but it would have prevented me from creating a dramatic storyline.

Let's assume that we consider some mission-critical software. The coding standards for such software contain rules for protecting the source code from unexpected data using "assert()" functions.

We start with an example of student-style pseudocode.

entry_function_for_some_state() {

    process_data_that_was_received_with_a_signal();

    read_some_internal_data();
    
    do_something();

    prepare_outputs();

    send_output_signals();

    return;

} // end of entry_function_for_some_state()


The life of a student is simple: prepare, give away, forget.

Now we turn this example into industrial-style pseudocode by adding logging.

test_friendly_entry_function() {

    log_input_data();

    process_data_that_was_received_with_a_signal();

    read_some_internal_data();
    
    do_something();

    log_internal_state();

    prepare_outputs();

    log_output_data();

    send_output_signals();

    return;

} // end of test_friendly_entry_function()


Please note, logging means that you agree that your code may be imperfect, and you could return to it in the future to correct your errors.

This is an acceptance of imperfection. If something bad happens, it would be logged, the software testing department would (probably) be able to reproduce error conditions, and the software developers would (probably) be able to understand these conditions, find the sources of errors, and correct the source code.

The source code will not be abandoned and forgotten. Logging means the (potential) responsibility of the creator.

Now, we extend this pseudocode with mission-critical-style features.

protected_entry_function() {

    log_input_data();

    assert_preconditions_ok();

    process_data_that_was_received_with_a_signal();

    read_some_internal_data();
    
    do_something();

    log_internal_state();

    assert_invariants_ok();

    prepare_outputs();

    log_output_data();

    send_output_signals();

    assert_postconditions_ok();

    return;

} // end of protected_entry_function()


This is a very boring piece of code. We had a task, and we solved it by producing a student-style solution. All further additions do not solve the task, but ensure that the solution is correct.

A reliable code solves problems that are unknown at the moment this code is created. The "assert()" functions determine that the software does not receive unexpected data values, does not invalidate internal data, and outputs correct data.

Now comes the final sentence: The transitions into the "INT_ERROR" state in a Shlaer-Mellor state transition table are an analog of an "assert()" function but for the software behavior.

This means the behavior of the software is defined, and any unexpected behavior is prevented.

Another old post about motivation (Про мотивацию / / 2009-10-18) that I read recently showed me that in the past I naively believed that gathering knowledge means gathering information.

Unfortunately, knowledge does not mean collecting, but understanding information. This skill comes with time, but it matures when it is no longer useful.

The phenomenon we call motivation is a positive gradient of psychological comfort, and the phenomenon we call demotivation is a negative gradient of psychological comfort. [ itSotWC::2024-01-07_1 ]

This description could have pointed me the right way in my search for motivational methods when I was trying to find them. This is now an interesting but useless piece of knowledge.`

Please note that the example of the state transition table that I use above was taken from a real project from the year 2001. My latest state transition tables are more compact, more readable, and do not contain unnecessary decorations.

They are really effective. But it is useless to write about them.

As I mentioned above, this approach is boring and economically inefficient because it demands thinking and removes the excitement of bug hunting.

I use it now only for my personal projects and for the source code, which I do not share with anyone.

Instead of spending many hours trying to catch a bug, you only read the bottom part of your log, open the correspondent state transition table in your source code, add a comment with the information that has improved your understanding, and change the value of a single cell.

    //=====================================
    //                   || INIT          |
    //=====================================

        ...                 ...
    //-------------------++---------------+
    /*  ACCEPTED     */  ,{ INT_ERROR     ,
    //-------------------++---------------+


Your state machine really can receive an "ACCEPTED" signal when it is in the "INIT" state. This is illogical, but you cannot do anything because this signal is received from some external agile software. Probably they had added this feature to their recent version. The reasons for sending are unknown, and you would better not ask about them.

You must not change the imperfect world. You only change the value in a single cell from "INT_ERROR" to "_ignore".

Flag Counter
juan_gandhi: (Default)

[personal profile] juan_gandhi 2024-01-13 01:19 am (UTC)(link)

Cool. Hilarious. I love reading your stuff. Also, getting familiar with the world of European Industrial Software is also eye-opening.

[personal profile] blue_slonopotam 2024-01-18 05:24 pm (UTC)(link)
https://www.youtube.com/clip/UgkxT4sbu3mFc3rRc7Y8ga1PmCQrEWJOBpD7

You can watch this fragment or the whole thing. Think what would happen to these guys if they made a talk like that in the USA.
juan_gandhi: (Default)

[personal profile] juan_gandhi 2024-01-19 01:32 am (UTC)(link)

FIIK. I did not have enough patience to listen to all this railroad stuff. A TL;DR would be nice to read.

(Anonymous) 2024-01-13 01:40 am (UTC)(link)
Хорошее описание того, как ручками (без поддержки IDE) писать на SDL, https://en.wikipedia.org/wiki/Specification_and_Description_Language

[personal profile] ex0_planet 2024-01-13 10:41 am (UTC)(link)
> The real-life state transition table in an agile project could be represented as a table full of holes.

Ну... как бы да, тут ничего нового. Это собственно и есть идея agile -- мы игнорируем всё что не можем ни контролировать, ни предсказать. А значит, не тратим на это ресурсов.

Не берусь давать оценок насколько это плохо или хорошо (для любого определения "плохо" и "хорошо"), но эту идею очень легко продать бизнесу под соусом "плати потом". Поэтому мы наблюдаем то, что наблюдаем.
uselessextras: (Default)

[personal profile] uselessextras 2024-01-13 05:22 pm (UTC)(link)
-- Mr. D is happy. He adds the new source code and makes another agile improvement: He removes the now obsolete processing of the "ASSERT_FAIL" signal.

Why would he remove it? I must be missing something.

[personal profile] someusersp 2024-01-13 07:39 pm (UTC)(link)
Конечные автоматы - это конечно хорошо, но при условии, что этих состояний не очень много, после определенного кол-ва состояний и переходов между ними - это превращается в труднопонимаемую хрень. На помощь могут прийти иерархические конечные автоматы, но это также до поры до времени.

На помощь могут прийти поведенческие деревья, с повторным использованием части деревьев.
https://www.progamer.ru/dev/utility-ai.htm

Я как-то работал в конторе, где ведущий разраб/архитектор решил делать ядро системы на конечных автоматах, и в итоге это превратилось сложно поддерживаемый и сложно расширяемый код. Этого умника попросили на выход, думал, сейчас хоть придет нормальный архитектор/вед. разраб и сделает по уму, но нет, ничего менять не стали, просто спустя пару лет контору закрыли :)

[personal profile] someusersp 2024-01-13 08:09 pm (UTC)(link)
У вас какая-то святая вера в метод Shlaer-Mellor и как тот молоток во всем начинаете видеть гвозди, т.е. на всё натягивать метод Shlaer-Mellor. Так ригидность мышления начинается, а потом и закрепляется.

[personal profile] someusersp 2024-01-13 08:25 pm (UTC)(link)
У меня как раз есть опыт работы на проектах, где конечные автоматы использовались и как можно было бы сделать относительно нормально, но в виду что компания большая, то на мнение разработчиков начальству насрать.

Относительно конечных автоматов я сделал для себя выводы, что их нужно писать один раз, формально верифицировать и забывать о них навсегда, когда они прошли формальную верификацию.

Если код с конечными автоматами регулярно меняют, то лучше перейти на что-то более легкое в поддержке и расширении.

При росте кол-ва состояний переходить на иерархические конечные автоматы, или поведенческие деревья.

При росте сложности упаковывать в компоненты, а компоненты в модули с четко определенными интерфейсами/API
Edited 2024-01-13 20:27 (UTC)

[identity profile] cross-join.livejournal.com 2024-05-17 01:56 pm (UTC)(link)
Прочел в тексте вместо "конечные автоматы" - коньячные. Пятница...

просто мнение

[personal profile] dedekha 2024-01-13 07:54 pm (UTC)(link)
A mission-critical system should handle all thinkable combinations of events. So, it could assert only on some really really bad internal problem.

[personal profile] dedekha 2024-01-14 02:55 pm (UTC)(link)
Такого не бывает. Любые требования содержат ошибки и любые требования не полны все это разруливается (iron out) в процессе разработки и большого количества тестов. А вот assertов мало а некоторых конторах они вообще запрещены.