05. Associations in OOP (Part 2)

Associations with attributes, qualified associations, composition vs. aggregation, and recursive associations.

Educational outcomes

Student:

  • explains how to implement constructs such as: association and link, cardinalities and association roles, directed and recursive association, attribute association and qualifed in natural language (understanding);
  • identifies and corrects errors in the source code, which is an implementation of the above structure;
  • lists constructs implemented in the source code (understanding);
  • implements the above structures using the selected programming language (application);
  • indicates functional differences between two selected programs based on code snippets source (analysis);
  • creates a program that implements specific business requirements using the constructs learned from the lectures (synthesis);

Justification

Modern programming languages ​​are mostly based on the object paradigm. Therefore, its practical understanding is a necessary basis for working in this profession. Moreover, programmers often make systems based on analytical materials. And these largely use object-oriented graphical notations (such as UML). During this and the next lessons, you will deepen your theoretical bond, but first of all you will learn how to implement different concepts from object-orientness. This will allow you to design and implement modern business applications.

Aggregation and composition

Definition

Aggregation is a special association emphasizing the relationship part - whole.

Definition

Composition is a stronger form of aggregation modeling the part-to-whole relationship and additionally imposes certain constraints:

  • Part can not be shared,
  • Part can not exist without a whole,
  • Removing the whole also means removing all parts. Removing a part does not necessarily mean deleting the whole.

The following figure shows an example of the composition between a building and its rooms.

Composition

The aggregation implementation is exactly the same as the association. This is because aggregation does not impose any specific conditions on the relationship.

The issue with the implementation of the composition is different. We have some problems to resolve:

  • Block creating unconnected parts - because part can not exist without the whole,
  • Prohibition of sharing parts,
  • Deleting a part when deleting a whole - again: because part can not exist without the whole.

It seems that we can use at least two general approaches:

  • Modification of existing solution implemented on the basis of identifiers or native references,
  • Take advantage of a completely new construction, so far unannounced - see the next section.

Let's start by modifying the solution with native references. How do we block "alone" parts? Let's start by answering the following question: what to do in order to block creating an object at all? You need to make their conctructor private. Thus, it will not be available for code outside the class. Why give us a class for which objects can not be created (we can do it as abstract as well). It can not be outside, but we will add a special method - a class one (because we want to use it to create objects). It should have the following features:

  • Get a reference to the whole. In addition, it must also check if the reference is valid. Someone could give, for example null.
  • Create a part object and possibly return it. In some situations, sharing references to object-parts may not be desirable, because with such a reference, the programmer can do with the object what he/she likes (for example, combine it with another whole, and this is against the semantics of the composition). In this situation, we create methods that work on parts from the whole.
  • Add returning information - to keep all association features.

Let's look at the sample code:

Java
public class Part {
	public String name;	// public for simplicity
	private Whole whole;

	private Part(Whole whole, String name) {
		this.name = name;
		this.whole = whole;
	}

	public static Part createPart(Whole whole, String name) throws Exception {
		if(whole == null) {
			throw new Exception("The given whole does not exist!");
		}

		// Create a new part
		Part part = new Part(whole, name);

		// Add to the whole
		whole.addPart(part);

		return part;
	}
}

public class Whole {
	private List<Part> parts = new ArrayList<>();

	private String name;

	public Whole(String name) {
		this.name = name;
	}

	public void addPart(Part part) throws Exception {
		if(!parts.contains(part)) {
			parts.add(part);
		}
	}

	@Override
	public String toString() {
		String info = "Whole: " + name + "\n";
		for(Part part : parts) {
			info += "   " + part.name + "\n";
		}

		return info;
	}
}

And traditionally the discussion of the most important parts:

  • (1) Class describing part.
  • (3) Because a part can only be connected to one entity, we use a single reference instead of a container.
  • (5) A constructor. Please note that it is private. This allows only methods of the same class to create instances of that class.
  • (10) Method creating a part based on the given whole.
  • (11) First we check whether the whole exists. If not, then we throw an exception. Instead, you can eg return null or display a message.
  • (16) The whole is correct, so we create a part.
  • (19) We add a part to the whole. For this purpose we call a special method from the Whole class.
  • (21) We return the reference to the newly created part
  • (25) Class describing the whole.
  • (26) A container storing links (references) to parts. Here we can not use a single reference, because there could be many parts.
  • (34) A method adding a part to the whole. Checks whether a part is no longer connected to the whole. If not, it adds; If there is alsready a link, it does nothing.
  • (41) A special version of the method for displaying a textual description of an object.

Another problem that we have to solve when implementing the composition is to prohibit the sharing of parts. How we do it? We will modify the method that adds a part:

  • It will check if a part is no longer added somewhere,
  • In addition to adding information related to a given section, it remembers (globally - that is, for the whole extent) the fact that a part is already associated with the whole. For this we will need a class attribute that stores information about all parts associated with the whole.

Modified snippets of the previous code and some comments:

Java
public class Whole {
	private List<Part> parts = new ArrayList<>();

	private static Set<Part> allParts = new HashSet<>();

	// [...]

	public void addPart(Part part) throws Exception {
		if(!parts.contains(part)) {
			// Check if the part has been already added to any wholes
			if(allParts.contains(part)) {
				throw new Exception("The part is already connected with a whole!");
			}

			parts.add(part);

			// Store on the list of all parts
			allParts.add(part);
		}
	}
}
  • (2) A container that holds references to object-parts for a particular object-whole.
  • (4) A class attribute that is a container that holds references to all parts (connected to all object-bodies). Thanks to it, we are able to see if any part is already connected to any object-all.
  • (8) Modified version of part adding method.
  • (9) First we check whether the given part is no longer connected to us. If so, this method ends its operation.
  • (11) If not, then we check whether the part is connected to any object of the whole. If so, we throw an exception with the appropriate message.
  • (15) We add a part to our whole.
  • (18) We store the connection of a given part on the global list of all parts.

The last problem to solve is to remove the part when removing the whole. Unfortunately, in languages ​​such as Java or C#, we do not manualy delete objects from memory. As a result, we are unable to obtain the exact functionality required. For our implementation, it is useful to create a class method that removes the whole from an extent along with the associated parts (remember to remove the information from the global part list).

Knowledge Check
Select all that apply

Which statements are true?

Composition using inner classes

Another approach to composition implementation is to use Java inner (nested) classes. This mechanism is characterized by the following features:

  • An inner class object can not exist without the (enclosing) outer class object.
  • The inner class object has a direct access to the invariants of the outer class object.

Because not all of the features of the composition are provided by the inner class mechanism, some of them must be implemented manualy. Let's look at the code and comments:

Java
public class Whole {
	private String wholeName;

	private List<Part> parts = new ArrayList<>();

	public Whole(String wholeName) {
		this.wholeName = wholeName;
	}

	public Part createPart(String partName) {
		Part part = new Part(partName);
		parts.add(part);

		return part;
	}

	@Override
	public String toString() {
		return wholeName;
	}

	// The inner class (the part).
	public class Part {
		private String partName;
		// Because of Java inner class properties, we do not need a reference pointing at the whole.

		public Part(String partName) {
			this.partName = partName;
		}

		@Override
		public String toString() {
			return "Part: " + partName;
		}
	}
}
  • (4) A container that holds references to object-parts. We need to add it because the connection to this side is not provided by the inner class semantics.
  • (10) A method that creates a part and returns a reference to it. It is worth noting that it is not a class one and it isinside the whole, not as before, in the part.
  • (11) Physical creation of a part-object as an inner class instance.
  • (12) Remember reference to it.
  • (23). Inner class (note that it is actually inside the main class - see the brackets). Because each inner class instance automatically has access to its surrounding object, we do not need to create a manual linking (as we did before).
Knowledge Check
Select all that apply

Which statements are true?

The approach proposed so far required manually adding return link information. So to connect two objects (such as an actor and a movie) you need to separately add references showing from the movie to the actor and from the actor to the movie. It is worth to automate it so that we do not have to do it manually. We will create a method that adds association information:

  • in the main class,
  • in the class associated with it.

The whole must be designed so that it does not create an endless loop (more on that later).

In other words, it is enough, for example, to add information about the actor to the movie, and the actor's link to the movie will be created automatically. Sample code implementing this functionality:

Java
public class Actor {
	public String name;	// public for simplicity

	// For standard associations
	private List<Movie> movies = new ArrayList<>(); // implementation of the association, cardinality *

	public Actor(String name) {
		this.name = name;
	}

	public void addMovie(Movie newMovie) {
		// Check if we already have the info
		if(!movies.contains(newMovie)) {
			movies.add(newMovie);

			// Add the reverse connection
			newMovie.addActor(this);
		}
	}

	@Override
	public String toString() {
		var info = "Actor: " + name + "\n";

		// Add info about his/her movies
		for(Movie movie : movies) {
			info += "   " + movie.title + "\n";
		}

		return info;
	}
}

public class Movie {
	public String title; // public for simplicity

	private List<Actor> actors = new ArrayList<>(); // implementation of the association, cardinality: *

	public Movie(String title) {
		this.title = title;
	}

	public void addActor(Actor newActor) {
		// Check if we have the information already
		if(!actors.contains(newActor)) {
			actors.add(newActor);

			// Add the reverse connection
			newActor.addMovie(this);
		}
	}

	@Override
	public String toString() {
		var info = "Movie: " + title + "\n";

		// Add info about titles of his/her movies
		for(Actor actor : actors) {
			info += "   " + actor.name + "\n";
		}

		return info;
	}
}

Comments:

  • (1) Class describing an actor.
  • (5) Private container storing movie information associated with specific actors.
  • (11). Method to add a movie information to specific actors. This is where "automation" is implemented, which we talked about earlier.
  • (13). First, we check to see if the movie collection already contains a reference to the specified object describing the movie. In other words, we want to find out if there is already a link between this actor and the movie. Why are we doing this? Imagine that we do not check this condition. What is going on?
    • We are adding information about this movie (12),
    • Then we call the method from the object on the other side (15), which adds return info (38). Its code is almost identical to the method we are analyzing. That is, at some point, the method will be invoked from the object on the other side; from the inside of this method again will be called the method on the other side, etc. - we have endless looping, resulting in the "hang" of the program.
  • As you can see, checking this condition works as a fuse - breaks the chain of reciprocal calls.
  • (22). Override the method responsible for returning a textual description of an object. What's happening here? We first construct a single string containing names of actors (20). Then we add information about the movies in which he/she played. We do this by iterating through the movie collection (23) and reading their titles, which are separated by the end of the line.

And how can we use the code we just created? Here is an example, comment and the result:

Java
// Create new business objects (without connections)
var movie1 = new mt.mas.associationsref.Movie("T1");
var movie2 = new mt.mas.associationsref.Movie("T3");

var actor1 = new mt.mas.associationsref.Actor("AS");
var actor2 = new mt.mas.associationsref.Actor("MB");
var actor3 = new mt.mas.associationsref.Actor("KL");

// Add info about connections
movie1.addActor(actor1);
movie1.addActor(actor2);
movie2.addActor(actor1);
movie2.addActor(actor3);

actor1.addMovieQualif(movie1);

// Show info about movies
System.out.println(movie1);
System.out.println(movie2);

// Show info about actors
System.out.println(actor1);
System.out.println(actor2);
System.out.println(actor3);
  • (1) We create objects that describe the movies. It is worth noting that for the time being we have no information about links with actors.
  • (5) We create objects describing actors. As in the above case, we have no links yet.
  • (10) We add actors to the movies. Equally good you can do the opposite: actors assign to movies. Because of automatically creating links, the effect will be exactly the same.
  • (18) Show info about movies.
  • (22) Show info about actors.
Plain Text
Movie: T1
	AS
	MB

Movie: T3
	AS
	KL

Actor: AS
	T1
	T3

Actor: MB
	T1

Actor: KL
	T3
Commentary

Does anyone know what business case can not be realized this way? Well, we will not be able to store many links between the same objects. Why? Because when you try to add the same object, our method will notice that it is already in the collection and will not do anything.

Knowledge Check
Select all that apply

Which statements are true?

Deleting association information is accomplished on similar terms as the above two-way linking. Let's look at the code of the dedicated methods and discuss them:

Java
public class Actor {
	public String name;	// public for simplicity

	// [...]

	private List<Movie> movies = new ArrayList<>(); // implementation of the association, cardinality *

	public void removeMovie(Movie movieToRemove) {
		if(movies.contains(movieToRemove)) {
			movies.remove(movieToRemove);

			// Remove the reverse link
			movieToRemove.removeActor(this);
		}
	}
}

public class Movie {
	public String title; // public for simplicity

	// [...]

	private List<Actor> actors = new ArrayList<>(); // implementation of the association, cardinality: *

	public void removeActor(Actor actorToRemove) {
		if(actors.contains(actorToRemove)) {
			actors.remove(actorToRemove);
			// Remove the reverse link
			actorToRemove.removeMovie(this);
		}
	}
}
  • (1). Fragment of the Actor class.
  • (6). Collection keeping links.
  • (8). Mthod removing the given movie.
  • (9). First we check whether we even have information about the given object. If not, we do not do anything. It is also protection against endless looping.
  • (10). If so, we remove it locally and call the method that will do it on the other side (13).
  • Similarly, a method is used to remove actors from a movie.

Sample code is below (line 25).

Java
// Create new business objects (without connections)
var movie1 = new mt.mas.associationsref.Movie("T1");
var movie2 = new mt.mas.associationsref.Movie("T3");

var actor1 = new mt.mas.associationsref.Actor("AS");
var actor2 = new mt.mas.associationsref.Actor("MB");
var actor3 = new mt.mas.associationsref.Actor("KL");

// Add info about connections
movie1.addActor(actor1);
movie1.addActor(actor2);
movie2.addActor(actor1);
movie2.addActor(actor3);

actor1.addMovieQualif(movie1);

// Show info about movies
System.out.println(movie1);
System.out.println(movie2);

// Show info about actors
System.out.println(actor1);
System.out.println(actor2);
System.out.println(actor3);

System.out.println("Remove info about actor: " + actor1.name);
movie1.removeActor(actor1);

System.out.println(movie1);
System.out.println(actor1);

Here is the effect of the use. Notice that calling the remove method causes the deletion of information from both the movie (line 20) and actors (line 23).

Plain Text
Movie: T1
	AS
	MB

Movie: T3
	AS
	KL

Actor: AS
	T1
	T3

Actor: MB
	T1

Actor: KL
	T3

Remove info about actor: AS
Movie: T1
	MB

Actor: AS
	T3
Knowledge Check
Select all that apply

Which statements are true?

As in the case of the class extent, let's make an observation. So the way to implement association management will be (almost) the same for every business class in the system. Moreover, they will (almost) the same for each association even in the same class. In other words, many times we will have to duplicate the same code. Therefore, the question arises: can this be unified?

Similarly to the management of the extent, we will create a special class that groups functionality for association management. What's more, our new class, let's call it ObjectPlusPlus, will have "free" all functionality for extent, persistence, etc. How do we achieve this? ,The ObjectPlusPlus class will inherit from the ObjectPlus class.

The requirements for this new class (ObjectPlusPlus) can be summarized as follows:

  • We will create a class that will inherit all business classes in our application.
  • This new class will inherit from an existing ObjectPlus class. This will also make it easier for us to manage extent, persistence, and more.
  • We will equip it with functionality that facilitates management:
    • ordinary binary associations,
    • qualified associations,
    • compositions (partially - condition no. 2 only).
  • We will use the second of the discussed approaches for association implementation: based on references.

Because all associations within a single object will be stored in one collection, we can not use a simple Vector or ArrayList container. We will use a container that holds keys and values:

  • The key will be the name of the role of the association. So in one object there will be as many keys as there are connected roles.
  • The value will be a map storing:
    • qualifier key. When we do not want to use a qualified association, the qualifier will be the same as the target object.
    • A value that is a reference to a particular link.

The above assumption requires a brief comment. So if we did not want to implement a qualified association, then instead of the above map, we could use a regular container, eg ArrayList. Because we want to be able to reach the target quickly in the case of a qualified association, we use the map. Someone may blame us for duplicating the key and value in the case of storing information about classic bindings. This is true, but otherwise we would have to have separate constructs for qualified and classical associations.

The following figure should make it easier to understand implementation assumptions:

Implementing links in ObjectPlusPlus

The main part of the drawing occupies a particular Movie object called "Terminator 1". Within it, we see a mapping collection (parameterized Hashtable) containing:

  • keys with names of association roles ("Actors", "Group")
  • and corresponding values ​​in the form of references (1, 2, 3) to subsequent mapping containers (parameterized HashMap) - one for each role:
    • the key is the qualifier,
    • the value is a reference showing a specific target, eg for the role of "Actors", through subsequent calls (1, 4) we arrive at an instance of Actor named "Arnold Schwarzenegger". Further references (5, 6) lead to other actors (not shown) playing in this movie. Other references (7, 8, 9) show the target as a "Group" (describing the affiliation of a movie to some group).

Since we already have a well-conceived concept, we just have to put it into action - below is the right program and explanations.

Java
public abstract class ObjectPlusPlus extends ObjectPlus implements Serializable {

	/** Stores information about all connections of this object. */
	private Map<String, Map<Object, ObjectPlusPlus>> links = new Hashtable<>();

	/** Stores information about all parts connected with any objects. */
	private static Set<ObjectPlusPlus> allParts = new HashSet<>();

	public ObjectPlusPlus() {
		super();
	}

	private void addLink(String roleName, String reverseRoleName, ObjectPlusPlus targetObject, Object qualifier, int counter) {
		Map<Object, ObjectPlusPlus> objectLinks;

		// Protection for the reverse connection
		if(counter < 1) {
			return;
		}

		// Find a collection of links for the role
		if(links.containsKey(roleName)) {
			// Get the links
			objectLinks = links.get(roleName);
		}
		else {
			// No links ==> create them
			objectLinks = new HashMap<>();
			links.put(roleName, objectLinks);
		}

		// Check if there is already the connection
		// If yes, then ignore the creation
		if(!objectLinks.containsKey(qualifier)) {
			// Add a link for the target object
			objectLinks.put(qualifier, targetObject);

			// Add the reverse connection
			targetObject.addLink(reverseRoleName, roleName, this, this, counter - 1);
		}
	}

	public void addLink(String roleName, String reverseRoleName, ObjectPlusPlus targetObject, Object qualifier) {
		addLink(roleName, reverseRoleName, targetObject, qualifier, 2);
	}

	public void addLink(String roleName, String reverseRoleName, ObjectPlusPlus targetObject) {
		addLink(roleName, reverseRoleName, targetObject, targetObject);
	}

	public void addPart(String roleName, String reverseRoleName, ObjectPlusPlus partObject) throws Exception {
		// Check if the part exist somewhere
		if(allParts.contains(partObject)) {
			throw new Exception("The part is already connected to a whole!");
		}

		addLink(roleName, reverseRoleName, partObject);

		// Store adding the object as a part
		allParts.add(partObject);
	}

	public ObjectPlusPlus[] getLinks(String roleName) throws Exception {
		Map<Object, ObjectPlusPlus> objectLinks;

		if(!links.containsKey(roleName)) {
			// No links for the role
			throw new Exception("No links for the role: " + roleName);
		}

		objectLinks = links.get(roleName);

		return (ObjectPlusPlus[]) objectLinks.values().toArray(new ObjectPlusPlus[0]);
	}

	public void showLinks(String roleName, PrintStream stream) throws Exception {
		Map<Object, ObjectPlusPlus> objectLinks;

		if(!links.containsKey(roleName)) {
			// No links
			throw new Exception("No links for the role: " + roleName);
		}

		objectLinks = links.get(roleName);

		Collection col = objectLinks.values();

		stream.println(this.getClass().getSimpleName() + " links, role '" + roleName + "':");

		for(Object obj : col) {
			stream.println("   " + obj);
		}
	}

	public ObjectPlusPlus getLinkedObject(String roleName, Object qualifier) throws Exception {
		Map<Object, ObjectPlusPlus> objectLinks;

		if(!links.containsKey(roleName)) {
			// No links
			throw new Exception("No links for the role: " + roleName);
		}

		objectLinks = links.get(roleName);
		if(!objectLinks.containsKey(qualifier)) {
			// No link for the qualifer
			throw new Exception("No link for the qualifer: " + qualifier);
		}

		return objectLinks.get(qualifier);
	}
}
  • (1). The ObjectPlusPlus class implementing links management.
  • (4). The main mapping collection declared as a parameterized Map. The key is the name of the association role, and the value is another map (parameterized Map) that holds:
    • The key as a qualifier,
    • The value that is a reference to the target object.
  • (7). Collection (parameterized HashSet) that holds references to all part objects connected to any object. This element is needed because of the implementation of condition no. 2 (part may belong only to one whole) of the composition.
  • (9). A constructor. All objects that are (inheriting) from ObjectPlusPlus must call it. Similarly, in its body is the constructor call with ObjectPlus (including adding the object to the extent). Without this call our classes will not work properly.
  • (13). The basic version of the linking method. It is worth noting that it is declared private. This is because we use it only internally. For developers using our class, there are overloaded and more friendly (simpler) versions.
  • (17). The counter is used as an endless anti-loop. Previously, we used to make sure that the object was already stored in the container. Now we will introduce another solution that is probably also more efficient. You also need to use it if you want to remember multiple links (within the same role) to the same object. This works in the same way that when calling the same method that adds a link from the perspective of the target, we decrease its value. As a result, at the next call, there is a break in the method and no looping.
  • (22). We are checking to see if there is a collection for the specified role name.
  • (24). If so, we return it.
  • (28). If not, then we create the appropriate collection (parameterized HashMap) and insert it into the main map (34).
  • (34). If we do not have such qualifier (because they must be unique), then we add link information (37).
  • (39). We call the same method, but in favor of the target to create a return link. We provide a reduced counter value so that the next call does not loop.
  • (43). Public version of the method to add a qualified association. As you can see, there is no parameter for a counter anymore. These are our internal implementation issues and should not be bothered by programmers who use our library.
  • (44). This method uses an internal implementation, passing the appropriate parameters.
    Important Note
    Creating one (or more) parameterized method and many of its public (overloaded) versions is a common practice. This makes the code in one place, and the programmers using our work have an easier life. It is also important for making corrections - we do it in one place.
  • (47). Public version of the method to add a link without a qualifier.
  • (48). This time we call the method (45), giving as the target qualifier (because we have to give something).
  • (51). Method adding a part in a composition.
  • (53). We are checking whether the given object-part no longer exists somewhere as a part. If so, we throw an exception.
  • (57). We call the standard method of an association. So we have another example for reuse. This is even consistent with the semantics of the composition - after all, it is finally an association.
  • (60). We will remember the object-part in the list of all parts (to prevent part sharing).
  • (63). Method to return a link table for a given role name. Of course, instead of an array, we can return another container, but the arrays are used by many other methods.
  • (66). We're checking to see if we have links for this role. If not, then we throw an exception (71).
  • (71). We read the map links (based on the key).
  • (73). We return an array using the method built into the map
  • (76). A method that displays links for a given role on a given stream. This allows them to be displayed on, for example, the console, but also directed to, for example, a file.
  • (79). We're checking to see if we have links for this role. If not, then we throw an exception.
  • (84). We will retrieve the links for this role and retrieve them as values (16).
  • (88). Display class name using reflection.
  • (90). Iterate on all target objects by sending their descriptions to the stream.
  • (95). Method returning the target based on the qualifier. Used only for qualified associations. For "classic" associations we have the getLinks(...) method.
  • (98). We're checking to see if we have links for this role. If not, then we throw an exception.
  • (103). We will retrieve a map of the links of this role.
  • (104).We check if this map contains the given qualifier. If not, then we throw an exception (5).
  • (109). We return the object based on the key (qualifier).

Example of use

Consider the following business case:

Business example for ObjectPlusPlus

Let's look at what simple construction our business class is. By inheriting from ObjectPlusPlus we do not need to include any functionality related to extent, associations, etc.

Java
public class Actor extends ObjectPlusPlus {
	private String name;

	public Actor(String name) {
		super();	// call the super constructor
		this.name = name;
	}

	@Override
	public String toString() {
		return "Actor: " + name;
	}
}

public class Movie extends ObjectPlusPlus {
	private String title;

	public Movie(String title) {
		super();	// call the super constructor
		this.title = title;
	}

	@Override
	public String toString() {
		return "Movie: " + title;
	}
}

public class Group extends ObjectPlusPlus {
	private int number;

	public Group(int number) {
		super();	// call the super constructor
		this.number = number;
	}

	@Override
	public String toString() {
		return "Group: " + number;
	}
}

Using them is just as trivial and does not require any special comment:

Java
// Create new objects (no links)
Actor a1 = new Actor("Arnold Schwarzenegger");
Actor a2 = new Actor("Michael Biehn");
Actor a3 = new Actor("Kristanna Loken");

Movie f1 = new Movie("Terminator 1");
Movie f3 = new Movie("Terminator 3");

Group g1 = new Group(1);
Group g2 = new Group(2);

// Add info about links
f1.addLink("actor", "movie", a1);
// f1.addLink("actor", "movie", a2);
f1.addLink("actor", "movie", a2, "MB");	// use the qualified association
f3.addLink("actor", "movie", a1);
f3.addLink("actor", "movie", a3);

g1.addPart("part", "whole", a1);
g1.addPart("part", "whole", a2);
g2.addPart("part", "whole", a3);
// g2.addPart("part", "whole", a1); // an exception because the part already belongs to another whole (group)


// Show infos
f1.showLinks("actor", System.out);
f3.showLinks("actor", System.out);

a1.showLinks("movie", System.out);

g1.showLinks("part", System.out);

// Test the qualified association
System.out.println(f1.getLinkedObject("actor", "MB"));

Effect of the above code:

Plain Text
Movie links, role 'actor':
	Actor: Arnold Schwarzenegger
	Actor: Michael Biehn
Movie links, role 'actor':
	Actor: Arnold Schwarzenegger
	Actor: Kristanna Loken
Actor links, role 'movie':
	Movie: Terminator 1
	Movie: Terminator 3
Group links, role 'part':
	Actor: Arnold Schwarzenegger
	Actor: Michael Biehn
Actor: Michael Biehn

Potential improvements

Unfortunately, our ObjectPlusPlus class is not ideal. Looking at the example of its use, you can see potential sources of problems.

First of all, links are identified by the regular string. This is bad for several reasons:

  • By typing this name we can make a typo and we will not get any error message. For a computer, each name is equally good.
  • We can pass an inappropriate object, eg an instance of Actor instead of Movie. And again there will be no exception.

How to deal with this? There are several possibilities (eg predefined enums instead of strings), but to do so you have to identify the source of our problems. Well, "computer" has no additional information (metadata) to capture these errors.

Important Note
Metadata is data about the data. This may be, for example, the author of the document, the date of the update. In our case, they will be associations that describe the links.

Adding them to the ObjectPlusPlus class would make our library able to validate the links. Using this functionality would take place in two steps:

  • We first define allowed associations. As a minimum we should give the name and types of the associated classes. In addition, one can be tempted to determine the cardinalities or other more sophisticated requirements;
  • Then, based on the defined association, we create links, such as the addLink(..) method takes as parameters the object describing the association and the business target.
Auxiliary assignment

Develop an enhancement for the ObjectPlusPlus class to eliminate above issues. It may be based on the above idea or some other improvements.

The provided solution (along with a brief description, source code and example of utilization) can receive up to 5% of the points planned for mini-projects.

Source codes

Download icon

Download all sources for MAS lectures.