Why must injected components be abstract types?

← DI Validation · Ref: Q667

EK9 requires injected component types to be abstract (E08150). This enforces the Dependency Inversion Principle: depend on abstractions, not implementations.

WHY ABSTRACT ONLY

Concrete injection creates tight coupling. Abstract injection allows:

  1. Different applications wire different implementations
  2. Test applications inject mocks
  3. Production applications inject real services

REGISTRATION MUST USE 'AS'

Concrete components must be registered with 'as AbstractType':

  register ConsoleLogger() as Logger    // Correct
  register ConsoleLogger()              // ERROR: E50020

Without 'as', the registration genus is incompatible.

INJECTION MUST USE ABSTRACT TYPE

  logger as Logger!                     // Correct: abstract
  logger as ConsoleLogger!              // ERROR: E08150

PATTERN

  defines component
    Logger as abstract
      log() as abstract
        -> message as String
    ConsoleLogger extends Logger
      override log()
        -> message as String
        stdout <- Stdout()
        stdout.println(message)
  defines application
    ProdApp
      register ConsoleLogger() as Logger
  defines program
    MyProgram with application of ProdApp
      logger as Logger!       // Inject abstract

See Q111 for component basics. See Q666 for pure context. See Q668 for injectable contexts. See Q324 for Autowired equivalent. See Q325 for Component equivalent.

Example

defines module qa.divalidation.abstractonly

  defines component

    <?-
      Abstract logger contract.
      Concrete implementations are registered in applications.
    -?>
    Logger as abstract

      logMessage() as abstract
        -> logEntry as String

      default operator ?

    <?-
      Console logger implementation.
    -?>
    ConsoleLogger extends Logger

      override logMessage()
        -> logEntry as String
        stdout <- Stdout()
        stdout.println(logEntry)

      default operator ?

    <?-
      Abstract repository contract.
    -?>
    UserRepository as abstract

      findByName() as abstract
        -> userName as String
        <- result as String?

      default operator ?

    <?-
      In-memory repository implementation.
    -?>
    InMemoryUserRepo extends UserRepository

      override findByName()
        -> userName as String
        <- result as String: ""

        result: "Found: " + userName

      default operator ?

  defines application

    ProductionApp
      register ConsoleLogger() as Logger
      register InMemoryUserRepo() as UserRepository

  defines program

    AbstractInjectionDemo() with application of ProductionApp
      stdout <- Stdout()

      logger as Logger!
      userRepo as UserRepository!

      logger.logMessage("Starting application")

      foundUser <- userRepo.findByName("Alice")
      stdout.println(foundUser)

      logger.logMessage("Application complete")

Common mistakes

E08150 — Only abstract component types can be injected. Injecting a concrete type like ConsoleLogger creates tight coupling and defeats the purpose of dependency injection. Depend on the abstract type Logger and let the application wiring choose the implementation. See ek9 -h E08150 for details.

Incorrect:

logger as ConsoleLogger!

Correct:

logger as Logger!
Other ways to ask this
  • What is E08150 injecting concrete component?
  • Why can't I inject a concrete component class?
  • How does abstract injection enable testing?

Coming from another language?

Java: Spring injects concrete types (bad practice). Python: duck typing, no enforcement. Go: interfaces encouraged but not required. Rust: trait objects for abstraction. EK9: compiler enforces abstract injection, concrete types cannot be injected.

Keywords: interface, component, E08150, inject, DI, registration, injection, concrete, validate, abstract