How can I change method behaviour on a class without subclassing?

← Getting Started · Ref: Q57

In EK9, you can change a class's method behaviour without subclassing by using function delegate fields. Supply a function as a constructor parameter, store it as a field, and call it from the method. Different instances of the same class can have different behaviour.

THE PATTERN

Define an abstract function for the behaviour contract:

  Processor() as abstract
    -> text as String
    <- result as String?

Define a class with a function delegate field:

  TextHandler
    processor as Processor?
    TextHandler()
      -> handler as Processor
      this.processor :=: handler
    process()
      -> text as String
      <- result as String: processor(text)

Now create instances with different behaviour:

  upper <- TextHandler(UpperProcessor)
  lower <- TextHandler(LowerProcessor)
  upper.process("hello")    //Returns "HELLO"
  lower.process("hello")    //Returns "hello"

Same class, same method signature, different behaviour per instance. No subclassing, no inheritance hierarchy, no override boilerplate.

WHY IS THIS BETTER THAN SUBCLASSING?

1. NO CLASS EXPLOSION — Instead of UpperTextHandler, LowerTextHandler, TrimTextHandler, you have one TextHandler class with different function delegates.
2. RUNTIME FLEXIBILITY — Change behaviour after construction by reassigning the delegate field.
3. COMPOSITION OVER INHERITANCE — Functions are composed, not inherited. This is the strategy pattern made trivial.
4. TESTABILITY — Pass a test function delegate in unit tests.

DYNAMIC FUNCTIONS AS DELEGATES

You can use dynamic functions with captures for custom behaviour:

  prefix <- ">>>"
  custom <- TextHandler((prefix) is Processor as function
    result:=? prefix + " " + text)
  custom.process("hello")    //Returns ">>> hello"

This creates a one-off behaviour with captured context. In Java, this requires an anonymous inner class or lambda with a functional interface. In EK9, it is natural.

In Java, the strategy pattern requires defining an interface, implementing multiple classes, and wiring dependencies. In Go, you use function fields but without type hierarchies. In Python, you monkey-patch or use first-class functions but without type safety. EK9 makes the strategy pattern a first-class, type-safe, compile-time-verified pattern.

See Q51 for abstract functions. See Q52 for dynamic functions. See Q53 for variable capture. See Q55 for function delegates. See Q106 for traits. See Q109 for composition over inheritance. See Q214 for strategy pattern.

Example

defines module qa.strategypattern

  defines function

    Processor() as abstract
      -> text as String
      <- result as String?

    UpperProcessor() extends Processor
      -> text as String
      <- result as String: text.upperCase()

    LowerProcessor() extends Processor
      -> text as String
      <- result as String: text.lowerCase()

  defines class

    TextHandler
      processor as Processor?

      default private TextHandler()

      TextHandler()
        -> handler as Processor
        this.processor: handler

      process()
        -> text as String
        <- result as String: processor(text)

      default operator ?

  defines program

    StrategyDemo()
      stdout <- Stdout()

      // Same class, different behaviour per instance
      upper <- TextHandler(UpperProcessor)
      lower <- TextHandler(LowerProcessor)

      stdout.println(`upper: ${upper.process("hello")}`)
      stdout.println(`lower: ${lower.process("HELLO")}`)

      // Dynamic function with capture as strategy
      prefix <- ">>>"
      customProcessor <- (prefix) is Processor as function
        result:=? `${prefix} ${text}`
      custom <- TextHandler(customProcessor)

      stdout.println(`custom: ${custom.process("hello")}`)

Common mistakes

E07090 — TextHandler is not declared as 'as open' so it cannot be extended. Use composition with function delegate fields instead of creating subclasses. See ek9 -h E07090 for details.

Incorrect:

UpperTextHandler extends TextHandler

Correct:

TextHandler
      processor as Processor?

E08180 — Class fields must be initialised at declaration. The '?' suffix creates the field in an uninitialised state that can be assigned later in the constructor. Without it, the field has no initial value. See ek9 -h E08180 for details.

Incorrect:

processor as Processor

Correct:

processor as Processor?

E02080 — If TextHandler has a function delegate field 'processor' and also defines a method called 'processor()', the compiler reports E02080 DELEGATE_AND_METHOD_NAMES_CLASH. Calling processor() would be ambiguous — is it the method or the delegate invocation? Rename either the field or the method to avoid the clash. See ek9 -h E02080 for details.

Incorrect:

processor()
        -> text as String
        <- result as String: processor(text)

Correct:

process()
        -> text as String
        <- result as String: processor(text)
Other ways to ask this
  • How do I use the strategy pattern with function delegates in EK9?
  • How do I inject behaviour into a class using functions in EK9?
  • How do I customise class behaviour per instance in EK9?

Coming from another language?

Java: strategy pattern requires defining an interface, implementing it in separate classes, injecting via constructor, verbose boilerplate, anonymous inner classes before Java 8, lambdas require functional interface. Python: duck typing allows passing any callable, no type safety, monkey-patching can change behaviour but is fragile, no compile-time verification. JavaScript: pass function as property, no type safety, prototype manipulation for behaviour changes, 'this' binding confusion. Rust: trait objects (dyn Trait) for runtime polymorphism, Box<dyn Fn> for stored closures, complex lifetime management, no simple field-level strategy pattern. Go: function fields on structs, simple but no type hierarchies, no interface guarantee on the function type. C#: delegate fields, strategy pattern well-supported but requires explicit delegate type declarations. Kotlin: function type fields ((String) -> String), SAM conversion, but structural not nominal typing. Swift: closure properties, @escaping required, but structural not nominal. EK9: function delegate field typed as abstract function gives compile-time contract, per-instance behaviour without subclassing, dynamic functions with captures for one-off strategies, natural strategy pattern with type safety.

Keywords: instance, function, class, change, composition, start, migrate, inject, beginner, field, inheritance, strategy, intro, first, constructor, delegate, behaviour, subclass, pattern