Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
OTJ Primer/Role Containment
Roles are grouped into a team where emphasis is on the interaction among roles, using the team as an encapsulation boundary. Technically, role containment is a superior version of class containment in Java.
Just like role playing, role containment is a relation that is declared between classes but essentially affects run time instances:
public team class Company { public class Employee { /* role details omitted */ } /* team details omitted */ }
Just as with inner classes in Java, such nesting of classes implies a connection between instances:
- creating an inner instance requires an outer instance to be in scope. Two situations are possible:
- Normally roles are created within a non-static method of Company (or of a sibling role). In these cases writing
new Employee()
implicitly connects the new inner instance to the currentthis
of type Company. - Creating a role outside its team is actually discouraged in OT/J. However, if it should be done one may use the Java syntax
thatCompany.new Employee()
, or the OT/J syntaxnew Employee<@thatCompany>()
(OTJLD §9). The two forms are semantically equivalent and reasons for the second form are outside the scope of this text.
- Normally roles are created within a non-static method of Company (or of a sibling role). In these cases writing
- as a result of the rules for role instantiation, each role instance has an immutable non-null reference to its enclosing team, accessible using the Java syntax
Company.this
within the role
Contents
Role Visibility
So far, roles behave just like inner classes. Next the developer of a role class must decide whether a role should be public
or protected
(there are no other choices).
Protected roles
It should be the normal case that roles are protected (OTJLD §1.2.3). This gives a team full control over the role. Only the team and sibling roles will be able to communicate with a protected role. To be more precise, it is the team instance that owns a role and acts as a fassade to it. So, the following method call, even when it occurs in a method of Company, is illegal if Employee is protected:
Company otherCompany = ... Employee leader = otherCompany.getTeamLeader();
The compiler will complain about an illegal attempt to "externalize" (OTJLD §1.2.2) a protected role. Specifically, if getTeamLeader()
returns an object of type Employee, this method can only be called with
this
as the receiver, ensuring that nobody outside the owning team instance will ever get a reference to the team's role instances.
- Further reading: with just the rules above an Employee instance could still leak out off the team by upcasting the role to some non-role supertype, ultimately to
Object
. The new top-level superclassTeam.Confined
provides means to prohibit upcasting even toObject
(see OTJLD §7.2). The full story can be found in the book chapter "Confined Roles and Decapsulation in Object Teams — Contradiction or Synergy?"
The focus on protected roles emphasizes that roles are intended to interact with other roles, not with arbitrary objects outside the team.
Public roles
By declaring a role public a pre-condition is met that instances of this type can actually be seen outside the enclosing team instance. Howeverm the type rules for "externalized roles" (OTJLD §1.2.2) still distinguish between, e.g., the types Employee<@microsoft>
and Employee<@canonical>
, and consider both types as incompatible (in this example, microsoft
and canonical
are assumed to be instances of Company).
The usage of "externalized roles" using type syntax like Employee<@canonical>
is actually not recommended for the novice. If, despite all recommendations, a role shall still be accessed from the outside of the team, it might be a good idea to simply declare an interface outside the team, let the role implement the interface and access the role via its non-role interface:
public interface IEmployee { String getName(); } public team class Company { protected class Employee implements IEmployee { getName -> getName; // declaration for this method is inherited from IEmployee } private Employee leader; public IEmployee getTeamLeader() { return this.leader; } } ... Company thatCompany = ... IEmployee leader = thatCompany.getTeamLeader(); System.out.println(leader.getName());
With this design we clearly distinguish which properties of a role should be visible from the outside.
- Further reading: a severe restriction of inner classes in Java is the direct coupling of logical nesting and physical file structure. This simply doesn't scale for many tens of inner classes and perhaps nesting at more than two levels. To overcome these issues, OT/J supports an alternative file structure using "team packages" and "role files" (OTJLD §1.2.5).
Data Flows
Given that roles are normally protected, i.e., confined within their enclosing team instance, the question arises how information can be exchanged between a team and the outside. This is when the playedBy relation comes back into focus.
Role-to-base
Given that a role and its base are two sides of the same coin, the compiler can easily help with a little translation:
public team class Company { protected class Employee playedBy Person {} private Employee leader; public Person getTeamLeader() { return this.leader; } } ... Company thatCompany = ... Person leader = thatCompany.getTeamLeader();
Only one thing has changed wrt the previous example: getTeamLeader()
now declares to return a Person. Well, the actual returned value this.leader
isn't exactly of type Person, and Employee actually is not a subtype of Person, but the compiler can easily extract the underlying Person from any Employee instance and return this instead of the role.
This translation from a role instance to its base instance is called lowering (OTJLD §2.2). It is applied implicitly in all program locations where a role instance is provided which does not conform to the required type of the expression, but where extracting the base instance actually yields a value of the required type.
Base-to-role
The opposite direction is a bit more challenging: assume you have a base instance of type Person and want to use this as the handle refering to a role. The team could have multiple role types for the same base type, like when we introduce a second role Client playedBy Person
to our running example.
In order to help the compiler here, the developer can use the following kind of method signature (OTJLD §2.3.2):
public class Company { protected class Employee playedBy Person { protected void acceptRequest(String request) { /* */ } } public void speakToClark(Person as Employee someone, String request) { someone.acceptRequest(request); } }
Here the argument someone
has a two-sided type: the client has to provide an instance of type Person, but inside the method we want to speak to the Employee role of the person, since method acceptRequest
is only defined in Employee. The translation from a base (Person) to a matching role (Employee) is called lifting, which has the following properties:
- lifting requires three inputs
- a base instance (explicitly provided by the client)
- a team instance (implicit in non-static team methods)
- a role type (here declared after as)
- if all three inputs are identical across multiple invocations, lifting will always find the same role instance.
- if lifting finds no existing role matching all three inputs a new role will be created on-demand and internally cached for later use.
- lifting does not interfere with garbage collection.
Translations in method bindings
The most convenient way to use lifting and lowering is in method bindings.
public class Person { public Person getSpouse() { /* */ } } public class Company { protected class EmployeeSpouse playedBy Person { } protected class Employee playedBy Person { EmployeeSpouse getSpouse() -> Person getSpouse(); } }
Here the callout binding interprets the return value from Person.getSpouse()
as an EmployeeSpouse, performing the necessary lifting fully automatic behind the scenes.
This is a summary of how lifting/lowering are supported in method bindings (OTJLD §3.3, OTJLD §4.5):
kind | return | argument |
---|---|---|
callout | lifting | lowering |
callin | lowering | lifting |
- Further reading: If implicit role creation due to lifting is not intended, you should consider using the Object Registration idiom.
Team Activation
Given that method call interception (by means of callin) is a significant intervention into an existing class, it is important to precisely control when a callin binding should have an effect.
Consider the Person joe being asked for his phone number. Answering the office phone number only makes sense when joe is actually in the office so he will hear the office phone ringing. In Object Teams we use team activation to represent a given context in which the program should perform its behavior (OTJLD §5).
Team activation is a feature of team instances. So the easiest way to activate a context is
Person joe = ... String joesOfficePhone; Company canonical = ... canonical.activate(); joesOfficePhone = joe.getPhoneNo(); canonical.deactivate();
The methods activate
and deactivate
are declared in the implicit super-type of all teams, org.objectteams.ITeam
.
Variants of team activation include
- per-thread vs. global (all-threads) activation OTJLD §5.1.1
- method call (activate) vs. control block (within OTJLD §5.2.a)
- explicit vs. implicit activation (whenever the controlflow enters a team, OTJLD §5.3).
- in-program activation vs. deployment-time activation OTJLD §5.5
Callin-enablement can be further restricted by guard predicates OTJLD §5.4. Guard predicates are particularly useful for the Object Registration pattern, which ensures that only existing roles will be considered for callin dispatch.
Interpretations of team activation
Using implicit team activation, a team is automatically active whenever some of its code is being executed. Thus, implicit team activation ensures that the current context of execution controls which method overrides (callin bindings) are enabled. This style can be used to implement use cases as teams: the main course of action is within the team which may invoke (using callout) methods of any bound base classes. Only during such callout executions the overriding defined by callin bindings is effective.
When using explicit team activation, this mechanism can be used to switch between different program modes with slightly different behavior. In these cases the program can be viewed as different layers of execution: a base layer that provides the main behavior and additional team layers that are superimposed on the base layer in order to provide additional behavior or adapt the behavior of the base layer.
Extra-program activation, finally, can be used to almost statically compose a system from different layers.