User Tools

Site Tools


component-anatomy

A Dana component provides and requires a set of interfaces. These interfaces are connected to other components to form a complete system. In this section we describe how interfaces and implemented and used, including the use of multiple primary interfaces and secondary interfaces, and how inheritance works in Dana.

Provided interfaces (objects)

Each provided interface declared by a component is a source of instantiable objects (of that interface type) for other components. For example:

component provides App {
   int number
 
   int App:main(AppParam params[])
      {
      return 0
      }
   }

This component offers a single interface of type App. Other components can instantiate objects of type App from this component. Each such instance has its own separate copy of any global instance variables, in this case the integer variable number.

All instance variables in Dana are inherently “private” and can never be accessed from outside the component that declares them.

When providing an interface, a component must implement all functions declared in that interface. An interface function is implemented by declaring a function whose name starts with the interface type name, followed by a colon, and has the same name as one of the functions in the interface definition. This is exemplified by the function App:main in the above code. The parameter list and return type must match those of the corresponding function definition in the interface type.

The objects that are instantiated from a given provided interface can have additional (private) interfaces. This is done using brackets as follows:

component provides App(AdaptEvents) {
   int number
 
   void AdaptEvents:active()
      {
      }
 
   int App:main(AppParam params[])
      {
      return 0
      }
   }

In this example, any objects instantiated from the App interface of this component also have a second interface AdaptEvents. These secondary interfaces can be used by system management processes to inform objects about or query objects for various pieces of information. More than one additional interface can be implemented by an object; the list of such interfaces is separated by commas within the brackets, i.e. App(x, y, z).

Destructors

An object is destroyed when it has no more references. When this happens, a special “destructor” function is invoked on that object if it declares one. A destructor is declared by an object by declaring a secondary interface of type Destructor:

component provides App(Destructor) {
   int number
 
   void Destructor:destroy()
      {
      }
 
   int App:main(AppParam params[])
      {
      return 0
      }
   }

This function can be used to perform any cleanup duties that should be done before the object is destroyed.

Using multiple provided interfaces

A component can declare more than one provided interface. Each provided interface is a separate source of instantiable objects, each with its own set of private instance variables. When using more than one provided interface, a component must use implementation blocks to indicate where the scopes for instance variables start and end. For example:

component provides App, Map {
   implementation App {
      int number
 
      int App:main(AppParam params[])
         {
         return 0
         }
      }
 
   implementation Map {
      void Map:start()
         {
         }
 
      void Map:work(App a)
         {
         }
      }
   }

This component provides two interfaces, App and Map, each of which is its own instantiable source of objects for other components. Each instance of type App has its own private instance variable number, while instances of type Map have no instance variables. Each of these provided interfaces can still have secondary interfaces as described above; these secondary interfaces do not need to be re-stated in the implementation block headers (declaring the primary interface of each provided interface is sufficient).

Objects of different types that have been instantiated from the same component have a special privilege in that they can access the private instance variables of each other. In the above example, if an instance of Map is given a reference to an instance of App via the work function, and that instance of App is from this same component, the instance of Map can then access App's private instance variable number by using the normal dot notation, e.g. a.number. See the net.TCP component in Dana's standard library for a real example use of this.

A component can check if it is the implementer of a given object instance using the implements notation, i.e:

if (implements a)
   {
   //...
   }

Static variables

As well as instance variables, a component can declare “class” variables which are shared between all instances of all objects in the component. These are simply declared as follows:

component provides App {
   static int instanceCount
   int number
 
   int App:main(AppParam params[])
      {
      return 0
      }
   }

In this example, the variable instanceCount is shared between all instances of App from this component.

Required interfaces

A required interface indicates that a component depends on some functionality from another component. Each required interface is a source of instantiable objects, for example:

component provides App requires Orange {
   int number
 
   int App:main(AppParam params[])
      {
      Orange x = new Orange()
      Orange y = new Orange()
      return 0
      }
   }

As a convenience, required interfaces can also be given a “handle” if only a single instance is needed and if the required interface in question does not declare a constructor function. For example:

component provides App requires io.Output out {
   int number
 
   int App:main(AppParam params[])
      {
      out.println("Some text output")
      return 0
      }
   }

This is a shorthand notation which is essentially the same as declaring the instance as a global static variable. As with provided interfaces, a component may declare more than one required interface, separating each required interface type with a comma, i.e. requires io.Output out, data.IntUtil, data.StringUtil.

In your own projects you will create plenty of components that depend on each other to create a working system, but Dana also features a large standard library of ready-made APIs which can be referenced by including them in your list of required interfaces - see our API pages for the list.

Semantic flavours

Dana is designed to support the existence of multiple implementation variants of the same interface, and adaptation between these variants at runtime. In this case all of the implementations are expected to be equivalent in the task that they perform.

Sometimes, however, different component implementations would naturally have the same interface but perform semantically different tasks to other implementations of that interface - such as image file parsers that read different image formats, or encoding components that encode and decode data into different standards like Base 64 or UTF-16.

For this kind of design pattern, Dana supports semantic flavours of both provided and required interfaces. A component can declare a provided interface with a semantic flavour as follows:

component provides Encoder:base64  {
 
   char[] Encoder:encode(char content[])
      {
      //...
      }
   }

A component can then require such a specific semantic flavour using the same notation on a required interface:

component provides App requires Encoder:base64 encoder  {
 
   }

Here the component will be connected to the default implementation of an Encoder interface with the base64 semantic.

Inheritance

Dana supports a form of behavioural inheritance through its interfaces, effectively allowing objects to be subtypes of other objects. If a component provides a interface that is a subtype of another interface, that component must either implement all functions of both the subtype and supertype, or else must have a required interface of the supertype.

All interfaces automatically inherit from the Object base type, which contains the functions equals, toString, clone and getID. These functions are therefore available on all objects, and the Object type can be used to reference any object instance.

In detail, inheritance works as follows.

Consider the two interface types:

interface GraphicsObject {
   void paint(Canvas c)
   void setPosition(int x, int y)
   Point getPosition()
   }
 
interface Button extends GraphicsObject {
   void setText(char text[])
   char[] getText()
   }

We can then implement the Button interface type as follows:

component provides Button requires GraphicsObject {
   char myText[]
 
   void Button:setText(char text[])
      {
      myText = clone text
      }
 
   char[] Button:getText()
      {
      return myText
      }
   }

In this example we've chosen to only implement the functions declared in the Button interface type, providing no implementations of any functions from the GraphicsObject supertype. Because of this we must declare a required interface of type GraphicsObject, giving access to an implementation of these functions.

We can also provide overriden implementations of some or all of the functions from the supertype. When we do this we still use the Button type as the function prefix, for example providing an overridden implementation of the paint function from GraphicsObject:

component provides Button requires GraphicsObject {
   char myText[]
 
   void Button:setText(char text[])
      {
      myText = clone text
      }
 
   char[] Button:getText()
      {
      return myText
      }
 
   void Button:paint(Canvas c)
      {
 
      }
   }

If we choose to provide an overridden implementation of every function from a supertype it is then no longer necessary to declare the supertype as a required interface.

Using data types defined in other source files

When a component declares provided or required interfaces, the compiler will automatically include all types defined in the corresponding source files of those interfaces, for use locally.

Sometimes it's also necessary to include types defined in source files that are neither provided or required interfaces.

This is done with the uses notation, expressed before the start of the component definition. For example:

uses time.DataTime
 
component provides App {
 
   }

The same search procedure is used to locate the corresponding source file as is used in searching for provided and required interface source files.

The Service interface

The Service interface is a special one which implies that a component wishes to be “started” and “stopped” when it joins and leaves the system. If a component provides this interface, the Service:start() function will be called before anything else, and the Service:stop() function will be called just before the component is unloaded from memory.

These two functions can be used to initialise and destroy parts of the component's state, such as thread pools or native libraries.

component-anatomy.txt · Last modified: 2018/08/16 10:09 by barryfp

Page Tools