The client/supplier dichotomy
EM revolves around the concept of a concrete module
, a programmatic construct that plays a seminal role within the language comparable to the position held by a class within C++ or Java. Not unlike classes, each EM module defines a programmatic boundary between its clients – users of the module – and the supplier of the module itself.
To minimize direct coupling between clients and suppliers – and therefore to increase software re-use – EM also supports module abstraction through an interface
construct which clients in turn can leverage through a module proxy
.
Source-code structure
Reflecting a basic dichotomy between module clients and module suppliers, consider the overall organization of a sample EM module named Mod1
, whose source code will reside in a file named Mod1.em
:
Starting from the top, each EM module lives within the logical scope of a package
which physically corresponds to a file-system directory of the same name. As in Java or Python, an EM package will generally bear a globally-unique qualified name that identifies its supplier – bob.pkg
, bob.pkg.test
, dave.pkg
, and so forth. By extension, all EM modules have a (globally-unique) fully-qualified canonical name – bob.pkg/Mod1
in the current example – though in practice you'll invariably refer to modules using simple names like Mod1
.
Flat directory structure
But unlike Java or Python, where the contents of a package named bob.pkg
would actually reside in a nested directory structure with the path bob/pkg
, EM employs a flat organization in which the logical package-name and physical directory-name must match exactly.
Moving on, the declarations beginning at line 3 comprise the public specification of this module – a coherent collection of constants, types, and functions (what others might term an "API" ) available for direct use by clients of Mod1
. Taken together, these externally-visible features of the module constitute a programmatic contract in which future changes on the part of the supplier should (hopefully!!! ) not violate prior client assumptions.
By contrast, features of Mod1
declared beginning at line 9 along with all definitions of its public and private functions beginning at line 15 remain hidden from any clients of this module. The supplier of Mod1
can consequently change the internal implementation of this module – optimizing performance or improving robustness – while maintaining a measure of "plug-compatibility" from its clients' perspective.
Separation of concerns
By enforcing crisp boundaries between clients and suppliers, EM modules encourage a software "best-practice" known as separation of concerns – organizing application functionality into discrete programmatic elements that encapsulate specific implementation decisions. Besides helping us digest software-rich systems in "bite-sized" chunks (where each EM module becomes a small world unto itself), our ability to manage change throughout the software life-cycle emerges as the most enduring benefit of modularity in general.
Importing modules
To gain access to public features of bob.pkg/Mod1
, clients must explicitly import
this module within their own .em
files prior to any direct usage. As an illustration, consider a sample module named dave.pkg/Mod2
:
dave.pkg/Mod2.em | |
---|---|
The directive at line 3 effectively adds the identifier Mod1
to this file's top-level namespace, which already includes all public / private feature names of Mod2
; an optional trailing as
clause can resolve name conflicts, should they arise. Through their import
directives, EM modules organize themselves into a static hierarchy of clients and suppliers. As a rule, this client-supplier relation must remain acyclic; an EM module can neither directly nor indirectly import itself.
After importing Mod1
, the example accesses this module's public type T
at line 9 followed by its public function f
at line 13 using a qualified name of the form Mod1.feature
. This syntax enables client modules using Mod1
to (coincidentally) declare their own features with identical names, such as the function f
defined here within the scope of Mod2
; unqualified identifiers always refer to features defined within the current module.
Main programs
By implementing a special (intrinsic) function named em$run
, any EM module can potentially serve as the entry-point for an executable application – a common pattern in many modern languages. Said another way, EM has no inherent notion of a "main-program"; instead, application developers will designate a particular module as the top of a client-supplier hierarchy defined via import
directives.
Abstracting suppliers
At the end of the day, EM application programs comprise a set of concrete modules – each contributing some measure of encapsulated code and data to the final executable image. In the example above, where we directly imported bob.pkg/Mod1
, this module will automatically and unconditionally become an element of any application program that directly or indirectly uses dave.pkg/Mod2
.
As an alternative to this mode of direct coupling between modules, EM introduces a language construct known as a proxy
that adds a level of indirection between clients and suppliers – abstracting a particular supplier's identity from the client's perspective. Akin to polymorphism within object-oriented programming, EM proxies can enhance application flexibility and improve software re-use by further decoupling clients from suppliers; the same client module, as you'll soon see, can effectively (re-)use different supplier implementations of otherwise common functionality.
And even more important than re-use, we can better manage change !!!
By not having to modify client modules that employ proxies, our software becomes more resilient and malleable when – and not if – application requirements evolve over time.
To capture commonality amongst a family of "plug-compatible" modules, EM enables us to separately publish a set of client-visible features as an interface
– a public specification independent of any particular supplier implementation. As an illustration, let's refactor the bob.pkg/Mod1
module presented earlier.
bob.pkg/ModI.em | |
---|---|
bob.pkg/Mod1.em | |
---|---|
The declarations beginning at line 3 mimic the earlier public specification of bob.pkg/Mod1
; but unlike a concrete module, an abstract EM interface cannot have an internal implementation. Our new rendition of Mod1
now inherits its public specification from the interface ModI
at line 3, while declaring any additional (public) features unique to this module; the private portion of Mod1
beginning at line 8 remains unchanged from before.
More on interfaces
In practice, abstract interfaces and implementing modules will often reside in different packages: for instance, the EM runtime contains a package named em.hal holding ConsoleUartI
, GpioI
, WakeupTimerI
, and other interfaces; packages such as ti.mcu.cc23xx then hold concrete implementations of these abstract interfaces targeting a particular MCU architecture.
Returning to the matter at hand – abstracting suppliers – we'll now refactor our original dave.pkg/Mod2
client module to remove its direct dependence on bob.pkg/Mod1
and instead leverage a local proxy
implementing the ModI
interface.
dave.pkg/Mod2.em | |
---|---|
The syntax at line 6 adds the name ModX
to the top-level scope of Mod2
, as well as declares that the proxy ModX
provides all client features specified within the interface ModI
; access to the public type T
and the public function f
at lines 10 and 14 respectively mirror earlier direct usage of bob.pkg/Mod1
.
Once again, we should emphasize that client Mod2
has no overt coupling to the concrete module Mod1
; instead, Mod2
only knows about the abstract interface ModI
– which would admit an unbounded number of alternate implementations beyond that provided by Mod1
.
The proxy – interface pattern
The pattern exemplified here occurs extensively within the EM runtime, and largely holds the key to maintaining platform portability. As a case in point, the package em.utils contains only portable modules such as AlarmMgr
which declares a local proxy implementing the em.hal/WakeupTimerI interface. Client application modules likewise desiring hardware independence can simply follow suit, using the proxy
– interface
pattern at each point of potential variability.
Moving on to Chapter 2, we'll now explore the process of binding the Mod2
.
ModX
proxy to the Mod1
module.