CODESYS Β· Track 04 Β· OOP in depth

Methods, Actions & OOP β€” the deep cut

You've met methods, EXTENDS and interfaces. Now we open the hood: the full anatomy of a method, properties that disguise getters and setters, real polymorphism across an array of devices, the ABSTRACT / FINAL controls that decide who may override what, and the honest question every engineer should ask β€” when NOT to use any of this.

🧩 method anatomyπŸŽ›οΈ propertiesπŸ”€ polymorphism🚦 ABSTRACT Β· FINAL
01 Β· Anatomy of a method

Every part of the signature, dissected

A method is a tiny program bolted to an FB. Its header declares exactly what flows in, what flows out, what it keeps to itself, and who's allowed to call it. Click any coloured token below to see what it does.

PUBLIC METHOD Move : BOOL VAR_INPUT target : REAL; // where to go VAR_OUTPUT done : BOOL; // arrived? VAR_IN_OUT log : STRING; // caller's buffer, edited in place VAR delta : REAL; // scratch, gone after the call END_VAR delta := target - Position; // Position is a MEMBER var (THIS^) Move := ABS(delta) < 0.1; // assign the method name = its return
β–² click a token
Each colour is a different kind of variable scope. Tap one to learn what it means and how it behaves at the call site.

Notice the return: you assign to the method's own name (Move := …), exactly like a FUNCTION. And Position isn't declared anywhere here β€” it's a member variable of the FB, reached through the implicit THIS^ every method carries.

02 Β· Properties

A field on the outside, two methods on the inside

A property looks like a plain variable to the caller β€” motor.Speed := 80; β€” but each access secretly runs code: a get accessor when you read it, a set accessor when you write it. That's the perfect place to validate, clamp, scale or log without the caller ever knowing. Try writing an out-of-range speed:

PROPERTY Speed : REAL  Β·  the setter clamps to 0…50 Hz

the caller never sees the guard β€” it just thinks it set a field
PROPERTY Speed : REAL           // looks like a variable to the outside

(* get accessor β€” runs when someone READS motor.Speed *)
Speed := fInternal;

(* set accessor β€” runs when someone WRITES motor.Speed := x *)
fInternal := LIMIT(0, Speed, 50);   // clamp; 'Speed' here is the incoming value
03 Β· Polymorphism, for real

One loop. Three devices. Three behaviours.

This is the payoff that makes OOP worth it. An ARRAY OF I_Device holds a pump, a valve and a heater β€” totally unrelated FBs that each implement the same interface. The line code loops once and calls Stop() on each. No CASE, no type checks: every object runs its own override. Add a fourth device type next year and this loop never changes.

New to ARRAY[..] OF, STRUCT or ENUM? They're the data shapes underneath all this β€” see the Data Unit Types deep dive.

FOR i := 0 TO 2 DO devices[i].Stop(); β†’

the dispatch log

// press "Stop all" β€” watch each index dispatch to a different FB
VAR
    devices : ARRAY[0..2] OF I_Device;   // interface references
END_VAR
devices[0] := pump;  devices[1] := valve;  devices[2] := heater;

FOR i := 0 TO 2 DO
    devices[i].Stop();   // SAME line β€” each runs its own Stop()
END_FOR
04 Β· ABSTRACT & FINAL

Who is allowed to override what

Inheritance is powerful, so IEC gives you brakes. These keywords let a base author say "you must fill this in" or "you may never change this" β€” turning a loose hierarchy into a contract the compiler enforces.

keywordonwhat it means
ABSTRACTmethodNo body here β€” just a signature. Any non-abstract child must override it. The compiler stops you forgetting.
ABSTRACTFBCan't be instantiated directly; it only exists to be extended. A blueprint, not a building.
FINALmethodThis override is the last word β€” no child may override it again. Locks safety-critical logic.
FINALFBNothing may EXTEND it. The leaf of the tree.
PUBLIC / PROTECTED / PRIVATE / INTERNALmethod Β· propertyWho may call it: anyone Β· this FB & its children Β· only this FB Β· only this library.
FUNCTION_BLOCK ABSTRACT FB_Device   // can't be instantiated
    METHOD ABSTRACT Stop : BOOL      // no body β€” children MUST provide one
// ────────────────────────────────────
FUNCTION_BLOCK FB_Estop EXTENDS FB_Device
    METHOD FINAL Stop : BOOL        // nobody may ever re-override this
        Energise := FALSE;            // the one true safe state
05 Β· And what about actions?

Where actions still earn their keep

Methods got all the new powers, so when is a plain action still the right call? Two honest cases β€” and one refactor you'll reach for constantly.

πŸͺœ Inside SFC β€” Sequential Function Chart steps fire actions on entry, while-active and on exit. That's their native home; methods don't slot into a step the same way.

βœ‚οΈ "Extract block" β€” when one giant FB body is unreadable, splitting it into a few named actions that share the same variables makes it scannable, with zero new scope to reason about.

⚠️ Not for reuse across FBs β€” an action can't take parameters, return a value, or join an interface. The moment you want any of those, it must be a method.

🧭 Rule of thumb β€” action = organise this FB's own code; method = expose a reusable, testable, override-able capability to the outside world.

06 Β· The honest part

When NOT to reach for OOP

OOP is a tool, not a religion. On a PLC it shines for device libraries, fleets of similar equipment, and code you'll ship to other people. It actively hurts when you over-apply it.

βœ“ Reach for it

Many variants of one thing (10 drives, 30 valves). A reusable library. A device that must be swappable behind an interface. Logic worth unit-testing in isolation.

βœ• Leave it alone

A one-off interlock. A simple latch. Code a maintenance tech must read at 3am. Deep inheritance chains "because we can" β€” every SUPER^ hop is one more place to look.

The best automation code mixes paradigms without ceremony: ladder for the interlocks an electrician will debug, a clean OOP library for the motion devices, plain ST for the maths. Use the abstraction that makes the next person faster β€” and nothing more.

07 Β· Check yourself

Did the deep cut land?

You declare FUNCTION_BLOCK ABSTRACT FB_Device with an ABSTRACT method Stop. A colleague writes pump : FB_Device; and the compiler refuses. Why?

A loop calls devices[i].Stop() over an ARRAY OF I_Device. You add a brand-new FB_Damper that implements I_Device. What happens to the loop?