foam3

FOAM3 Dependencies: requires vs imports

This document explains the differences between requires and imports in the FOAM3 framework and when each should be used in the codebase.

Overview

FOAM3 provides multiple mechanisms for managing dependencies between classes and accessing runtime context. The two primary mechanisms are:

Cross-Language Design Philosophy

FOAM is designed to be cross-language compatible, which means it can’t easily adopt conventions from any single language. This leads to some naming that might seem counterintuitive at first:

requires: - Class Dependencies

Purpose

Declares class dependencies so you can refer to classes by their short name rather than their fully qualified name. requires requires classes.

Conceptual Model

When you requires: 'com.acme.Foo', you can refer to it as just Foo rather than its fully qualified name com.acme.Foo. This is similar to:

Syntax

foam.CLASS({
  name: 'MyClass',
  requires: [
    'foam.u2.DetailView',                           // Available as this.DetailView
    'foam.dao.MDAO',                               // Available as this.MDAO
    'foam.net.web.XMLHTTPRequest as HTTPRequest'   // Available as this.HTTPRequest
  ]
});

How It Works

Examples from Codebase

Basic Usage:

foam.CLASS({
  package: 'com.example.models',
  name: 'Transaction',
  
  requires: [
    'com.example.views.TransactionFormDetailView'
  ]
  // Instead of: com.example.views.TransactionFormDetailView.create()
  // You can use: this.TransactionFormDetailView.create()
});

Complex View with Multiple Dependencies:

// File: foam3/src/foam/u2/wizard/IncrementalStepWizardView.js
foam.CLASS({
  package: 'foam.u2.wizard',
  name: 'IncrementalStepWizardView',
  extends: 'foam.u2.View',
  
  requires: [
    'foam.lang.Action',
    'foam.log.LogLevel',
    'foam.u2.detail.SectionView',
    'foam.u2.dialog.Popup',
    'foam.u2.dialog.SimpleActionDialog'
  ]
  // All classes available without full package names
});

When to Use requires

imports: - Dependency Injection

Purpose

Declares that you want a live instance of a service or value for use in your class. This is dependency injection or inversion of control.

You get runtime objects/services, not classes.

Conceptual Model

Instead of your class going fishing around the system looking for services (which leads to complexity and tight-coupling), you simply declare what services you want to import and they’re provided to you - even possibly from across the network.

This follows the dependency injection pattern:

Syntax

foam.CLASS({
  name: 'MyView',
  imports: [
    'currentUser',              // A user object instance
    'userDAO',                  // A live DAO instance
    'log',                      // A logging service instance
    'optionalService?',         // Optional service (no warning if missing)
    'someDAO as myDAO'          // Aliased service
  ]
});

How It Works

Examples from Codebase

Basic Context Injection:

foam.CLASS({
  package: 'com.example.views',
  name: 'MenuView',
  extends: 'foam.core.menu.VerticalMenu',

  imports: [
    'currentProgram',    // Live program object
    'isMenuOpen'        // Boolean state value
  ]
  // Access: this.currentProgram (the actual object)
  //         this.isMenuOpen (the actual boolean value)
});

Service Injection:

// File: foam3/src/foam/u2/wizard/debug/WizardInspector.js
foam.CLASS({
  package: 'foam.u2.wizard.debug',
  name: 'WizardInspector',
  extends: 'foam.u2.Controller',

  imports: [
    'wizardController',    // Live controller instance
    'crunchController'     // Live controller instance
  ]
  // These are actual running controller objects, not classes
});

When to Use imports

exports: - Making Services Available

Purpose

The flip side of imports: - declares what objects or values you want to make available to descendants in the context chain.

Syntax

foam.CLASS({
  name: 'MyService',
  
  exports: [
    'userDAO',           // Export userDAO property to children
    'as myService',      // Export entire instance
    'log as logger'      // Export with alias
  ]
});

How It Works

Key Differences Summary

Aspect requires imports
Purpose Class name resolution Service injection
Result Factory methods Direct property access
Type Classes/constructors Instances/values/services
Source Class looked up in the object’s context on first use Runtime execution context
Analogy import in Java, require() in JS Dependency injection frameworks
Creation this.ClassName.create() this.serviceName
Coupling Class dependencies, replaceable through the context Runtime loose coupling
Testability Register a replacement class in a sub-context Easy to inject test doubles
Network Classes are resolved locally Can import across network
Optional No Yes (with ?)

Dependency Injection Benefits

Without Dependency Injection (Tight Coupling)

// BAD: Tight coupling - knowing application architecture explicitly
foam.CLASS({
  name: 'BadExample',
  methods: [
    function init() {
      // Tight coupling - requires knowing the exact architecture path
      this.userDAO = this.frame.controller.session.daoAccessor.userDAO;
      // If the structure changes, all client code breaks
    }
  ]
});

// STILL LOOSE but more work:
foam.CLASS({
  name: 'LooseCouplingExample',
  methods: [
    function init() {
      // Context lookup is still loose-coupling, just more work
      this.userDAO = this.__context__.userDAO;
      this.logger = this.__context__.log;
      // What if they're not there? What if they move?
    }
  ]
});

With Dependency Injection (Loose Coupling)

// GOOD: Dependencies are injected
foam.CLASS({
  name: 'GoodExample',
  
  imports: [
    'userDAO',    // I need a user DAO - don't care where it comes from
    'log'         // I need logging - don't care how it's implemented
  ],
  
  methods: [
    function someMethod() {
      // Just use the injected dependencies
      this.userDAO.find(id).then(user => {
        this.log.info('Found user:', user.name);
      });
    }
  ]
});

Cross-Language Comparison

Language Class Import Service Injection
FOAM requires: imports:
Java import com.acme.Foo @Inject annotations
JavaScript require('./Foo') Manual DI or frameworks
Python from acme import Foo DI frameworks
C# using Acme [Inject] attributes

Additional Import Mechanisms

javaImports: - Java Code Generation

Used when generating Java code from FOAM classes:

foam.CLASS({
  package: 'com.example.services',
  name: 'DataProcessor',

  javaImports: [
    'foam.dao.AbstractSink',
    'java.util.ArrayList',
    'java.util.List'
  ]
  // Generates: import foam.dao.AbstractSink; etc.
});

Traditional Node.js require()

For CommonJS modules in Node.js environments:

// File: foam3/tools/build.js
const fs = require('fs');           // Node.js module
const path = require('path');       // Node.js module

Advanced Features

Optional Imports

Use ? suffix for optional context values:

imports: [
  'requiredService',        // Missing: debug builds report 'Missing required import', and access logs a warning
  'optionalService?'        // Missing: no warning, the value is undefined
]

Aliasing

Both requires and imports support aliasing:

requires: [
  'foam.u2.DetailView as DetailView',
  'very.long.package.name.SomeClass as ShortName'
],
imports: [
  'userDAO as dao',
  'currentUser as user'
]

Network Services

imports: can work across networks:

foam.CLASS({
  name: 'ClientView',
  
  imports: [
    'userService'  // Could be a remote service accessed over HTTP
  ],
  
  methods: [
    function loadUser() {
      // userService might be running on a different server
      return this.userService.getCurrentUser();
    }
  ]
});

Best Practices

Use requires when:

Use imports when:

Use exports when:

General Guidelines:

  1. Prefer imports for services and runtime data - it creates more flexible, testable code
  2. Use requires for class instantiation and when you need to create multiple instances
  3. Mark optional dependencies with ? in imports
  4. Use aliasing to avoid naming conflicts
  5. Keep dependencies minimal - only import what you need
  6. Design for testability - prefer injection over direct lookups
  7. Think about the context chain - exports flow down to imports

Common Patterns

Service Layer Pattern

foam.CLASS({
  name: 'UserService',
  
  imports: [
    'userDAO',      // DAO injected from context
    'log'          // Logging service
  ],
  
  exports: [
    'as userService'       // Export self for other components
  ],
  
  methods: [
    function getCurrentUser() {
      return this.userDAO.find(this.currentUserId);
    }
  ]
});

Controller Pattern

foam.CLASS({
  name: 'UserController',
  
  imports: [
    'userService',   // Service injected (loose coupling)
    'notify'        // Notification service
  ],
  
  requires: [
    'foam.u2.DetailView',  // View class for creating forms
    'foam.core.auth.User'    // Model class for data validation
  ],
  
  methods: [
    function createUserForm() {
      // Use required class to create view
      return this.DetailView.create({
        data: this.User.create()  // Use required model
      });
    },
    
    function saveUser(user) {
      // Use injected service
      return this.userService.save(user).then(() => {
        this.notify.success('User saved!');
      });
    }
  ]
});

View Pattern

foam.CLASS({
  name: 'UserView',
  extends: 'foam.u2.View',
  
  imports: [
    'theme',         // Theming service
    'userService'    // Business logic service
  ],
  
  requires: [
    'foam.u2.tag.Button',  // UI components
    'foam.u2.TextField'
  ],
  
  methods: [
    function render() {
      this.add(this.TextField.create({
        data$: this.data.username$
      }));
      
      this.start(this.Button, { label: 'Save' })
        .on('click', () => this.userService.save(this.data))
      .end();
    }
  ]
});

Summary

Understanding the difference between requires and imports is crucial for effective FOAM development:

This design enables loose coupling, testability, and the ability to work across networks - core principles of the FOAM architecture.