Rationale - Krystian-L-Lis/Stage GitHub Wiki

#Guide #Rationale

Introduction

The goal of this framework is to set a Stage for your project.

I never had guidance on how to organize my code, so I had to learn on my own from resources I found on the web. This led me to experiment with different approaches, some right, some wrong, and over time, I gradually developed an intuition for what clean code means to me.

I have mixed feelings about OOP, as I’ve yet to see a complex yet clean solution that isn’t cluttered with layers of unnecessary abstractions created “just in case.” In my view, truly clean code should focus on its current purpose, not hypothetical future needs. That said, there should still be room for flexibility, but it must be cost-effective: highly generic and not overly time-consuming for developers. Another problem I have with OOP is the need for boilerplate. Functionality designed solely to support abstraction creates unnecessary mental overhead and diverts attention from the actual logic. In TwinCAT, this is further exacerbated by the fact that methods and properties are stored in separate files in the IDE.

It's common to see significant variation in code organization across different projects and teams. While that isn’t inherently bad, it often takes time to understand the architectural intent. With this framework, I aim to provide a highly generic structure that can be adapted to the coding styles of different teams with varying levels of experience.

The framework is inspired by game engines like Unity, Godot or Bevy. After all, what is a real-time system if not a game? A typical game targeting 60 frames per second operates with an execution window of approximately 16.67 milliseconds per frame. In this time it handles multiple tasks: logic, rendering, physics simulation, and more. In comparison, PLC programs operating on a similar task length do significantly less within this time. Since rendering and simulation are unnecessary, the execution window is entirely dedicated to logic. This leaves room for framework-related logic, which modern IPCs can easily handle without sacrificing much performance.

Memory Management

One of the core principles of the Stage is its reliance on static memory allocation. This by itself mitigates the whole range of bugs related to dynamic memory allocation such as memory leaks, unpredictable execution times and invalid pointers.

Not a single type in Stage library allows for dynamic creation and discourages operations on raw memory and pointers, as such practices can be error-prone and undermine system reliability.

Static allocation does not mean inflexibility. Developers are expected to determine the memory requirements on compile time through parameters.

  • Param.SIGNAL_CAP specifies the maximum number of signals.
  • Param.RECEIVER_CAP defines the maximum number of signal-receiver pairings.

Note: For instructions on modifying library parameters, refer to Beckhoff Infosys.

If these limits are exceeded during execution, the system will return an error value, informing the developer(Debug) that the corresponding parameter needs to be increased. This allow developers to fine-tune the system while maintaining the principles of static allocation.

Components Composition

Inheritance allows one function block to extend another, reusing code while still allowing modifications. Composition, on the other hand, means that instead of extending an FB, you include an instance of it as a component inside another FB.

Inheritance often leads to rigid hierarchies that oftentimes are difficult to refactor without introducing breaking changes. The solution lies in adopting composition over inheritance. Instead of relying on vertical inheritance, program structures should be designed horizontally. Function blocks, or even structs, could be composed of modular behaviours and elements rather than inheriting them. A great external explanation of these concepts can be found in this video, though keep in mind that it focuses on general OOP, not PLC programming.

Example:

FUNCTION_BLOCK composedFb IMPLEMENTS I_Execute, I_Init, I_Callable, I_Custom
    VAR
        _exe: Execute(THIS^);
        _init: Init(THIS^);
        _rec: Receiver(THIS^);
        _custom: Custom(THIS^);

        _logic: SomeLogic;
        _otherLogic: SomeLogic2;
    END_VAR
END_FUNCTION_BLOCK

In this example:

  • The function block composedFb implements multiple behaviours (I_Execute, I_Init, I_Receiver, I_Custom) through interfaces and delegate their execution to corresponding function blocks.
  • The specific behaviours are encapsulated as components (_exe, _init, _rec, _custom), allowing each to be customised or replaced independently.

If a similar function block is needed, with slight variations in logic, it can be achieved effortlessly by composing different components:

FUNCTION_BLOCK composedFb2 IMPLEMENTS I_Execute, I_Init, I_Callable, I_Custom
    VAR
        _exe: Execute(THIS^);
        _init: Init(THIS^);
        _rec: Receiver(THIS^);
        _custom: Custom(THIS^);

        _logic: SomeLogic;
        _otherLogic: SomeLogic2;
        _yetAnother: SomeLogic3;
    END_VAR
END_FUNCTION_BLOCK

In this example:

  • The composedFb2 function block retains its core behaviours (Execute, Init, etc.), but introduces an additional logic component (_yetAnother).
  • This modular approach makes it easy to extend functionality without altering the base behaviours.

The components like Execute and Init provide base behaviours akin to using SUPER^ in inheritance-based designs. However, the methods required by the corresponding interfaces (I_Execute, I_Init) allow for fully customised implementations.

While composition offers numerous advantages over inheritance, it is not without its drawbacks. Among the most notable are performance overhead and verbosity. Let’s address these:

  • Verbosity: Composition often requires some boilerplate code, especially in the context of TwinCAT XAE, where developers may find themselves navigating through multiple files. While some developers appreciate the clarity this brings, others may find it cumbersome. Stage strives to minimise boilerplate code wherever possible.
  • Performance Overhead: As mentioned earlier in the Introduction section, the performance impact of composition in most applications is negligible. The Stage framework has been designed with performance in mind, ensuring that all operations aim for O(1) time complexity. While there may be some overhead in framework-related logic, modern PLC hardware should handle these operations without any noticeable performance loss.

Wherever possible Stage implements composition, but there may be use-cases where inheritance is preferable(see Job, State).

Syntactic Sugar

At the framework level the provided functionality should use minimal boilerplate, and writing it should feel natural. The flow of the code should be as close as possible to the raw logic of the problem. By "raw logic flow," I mean the way code looks when the problem is simple and doesn't involve any form of abstraction. It is acceptable to encapsulate some boilerplate in methods or functions for developer convenience, as long as there remains a way to access the original methods when needed.

< Previous | Home | Next >

⚠️ **GitHub.com Fallback** ⚠️