04. Associations in OOP

Binary associations, multiplicity constraints (1..1, 1..*, *..*), navigation, and bidirectional link management.

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.

Definition

Link connects objects. Most often we deal with binary relationships, ie. those that only connects two objects at a time.

Definition

Association is a description of a group of links with the same semantics. It is used to describe connections between classes.

The relationship between an association and a link can be thought the same way as a class and an object. The link is something "real", between existing objects, eg Employee Blacksmith is hired by a Company Softmakers. By contrast, an association generally describes employment between an Employee and a Company. In other words, the link is an instance of an association (just as the object is an instance of a class).

The association name should be matched to make a meaningful expression in conjunction with the class names, eg Employee works in Company.

Knowledge Check
Select all that apply

Which statements are true?

Cardinalities of associations

In addition to the name, the association is described by the cardinalities. It defines how many objects can be connected to a particular object. We will remember two values: one for each side.

cardinalities of associations

The information from the above figure should be read as follows:

  • The specific object of the A class is connected with Y objects of the B class,
  • The concrete object of the B class is connected with X objects of the A class.
  • Instead of X, Y, we put one of the following values ​​(in the older version of UML 1.x combinations were also allowed):
    • 0,
    • 0..1,
    • * (means zero or more),
    • 1…* (means one or more),
    • particular number, eg. 4.
  • In the older versions of UML (1.x), cardinalities could be arbitrarily merged using the notation, eg. "0 ...4, 7, 8, 13, 45...*". Of course this did not always have a business sense. Therefore, in UML2, such "combined" syntax is excluded. If no information is given, it means that the cardinality is 1. Usually we use: 1, "0, 1", "1...*".

You might wonder why you should give the cardinality information. After all, you could make an assumption that everywhere is "*" and we can store every business situation. The main reason is the desire to fine-tune the model, because there are differences in implementation of the "one" and "many" (see below). An additional benefit is to validate some rules, eg. if there is a "1" somewhere, then we will not be able to create an object ("system" will not allow it), which will not be associated with exactly one object.

Commentary

Particularly cautious must be in the case of the 1 - 1 combination. From the data storage point of view, such a connection is not needed at all and both classes can be simply merged together. However, in some situations, we want to stress the existence of two different beings. In other words, when we see an association of 1 - 1 in the diagram, let's check it twice if it is really needed.

Knowledge Check
Select all that apply

Which statements are true?

Association roles

Placing a name is not the only way to describe an association. Instead, you can use role names. Note that the names are located at the target class, e.g. the Employer role with the Employee class. With this approach, we have a comprehensive description: from the point of view of each class. This is particularly useful when, at a later stage, we will transform the diagram into an implementation version (see below).

Two approaches to association implementation

Unfortunately in Java, MS C#, C++ associations do not occur directly. Therefore, we will have to implement them somehow. To do this we can use one of two approaches. The main difference between them is the way we store information about connections - we can use:

  • custom identifiers, such as numbers,
  • native programming language references (Java, C#) or pointers (C++).

Immediately the question arises: which approach is better? Which one to use? Traditionally there is no one universal answer. Depending on our needs the first or the second solution may seem more useful. Although it should be noted that in most cases we will use native programming language references - see further.

Implementation using identifiers

Let's start by discussing the implementation. For each business class we add an attribute that is an identifier, such as the int number. This number will allow to uniquely identify the object. Depending on business needs, its uniqueness may be within a single extent or all objects in the system. We keep information about connected objects by remembering their identifiers. Depending on cardinalities the storage will be a bit different.

Of course, in order to use bi-directional associations, we need to use pairs of identifiers. Let's look at the figure below. Part (a) shows a simple class diagram that allows you to store movie information and actors (or, in other words, the actors and movies they played). Part (b) contains a fragment of a sample situation that we have in the system using identifiers. Therefore, there are "actor" and "movie" attributes in the "objects" that are not in the class diagram. This is because they are equivalent to association roles. As you can see, the movie "Terminator 1" has an identifier of 1 and its third part "2". Note the attribute named "actor". It contains numbers that are the identifiers of the actors describing the actors playing the movie, eg "Id = 3" means actor "AS". And the other way: The actor "AS" has a "Movie" attribute that stores the IDs of the movies in which a particular actor was playing, for example Id = 3 would be "Movie = [1, 2]".

Implementation using identifiers

As you may know, the way is simple enough but effective. It allows you to store any connections between objects. How to implement it in Java? Let's look at the code:

Java
public class Actor {
	private int id;
	public String name;	// public for simplicity

	public int[] movieIds;

	private static List<Actor> extent = new ArrayList<>();

	public Actor(int id, String name, int[] movieIds) {
		// Add to the extent
		extent.add(this);

		this.id = id;
		this.name = name;
		this.movieIds = movieIds;
	}

	public static Actor findActor(int id) throws Exception {
		for(Actor actor : extent) {
			if(actor.id == id) {
				return actor;
			}
		}

		throw new Exception("Unable to find an actor with the id = " + id);
	}
}

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

	public int[] actorIds;

	private static ArrayList<Movie> extent = new ArrayList<Movie>();

	public Movie(int id, String title, int[] actorIds) {
		// Add to the extent
		extent.add(this);

		this.id = id;
		this.title = title;
		this.actorIds = actorIds;
	}

	public static Movie findMovie(int id) throws Exception {
		for(Movie movie : extent) {
			if(movie.id == id) {
				return movie;
			}
		}

		throw new Exception("Unable to find a movie with the id = " + id);
	}
}

Some important places to comment:

  • (1). Definition of the business class Actor.
  • (2). A private attribute of the type int that acts as an identifier.
  • (3). Normal business attribute.
  • (5). An array of int numbers that holds the movie IDs in which the actor was playing. It is worth noting that a single number is not sufficient because the size of the association informs us about the possibility of multiple associations. It's better to use a collection here (instead of an array), but we wanted to create the simplest possible example.
  • (7). Class attribute that stores extent. This type of element is essential in our approach. Why? Because we describe each connection with identifiers and we must have a way to get the corresponding object. To do this, we search the extent until we find the object with the given identifier. Is it possible to make it in a more efficient way? Of course - we can use a mapping structure, where the key is id, and the value is the object reference.
  • (9). A constructor.
  • (11). Within the constructor we add the object to the extent.
  • (13). Further, business part of the constructor.
  • (18). The method that returns a reference to the corresponding object based on the identifier (number) - see note to point (5).
  • (19). Iterate over all the objects in the extent. When we find an instance with the given identifier, we return it (18).
  • (25). If we look through all the extent and we do not find the object with the specified identifier, we throw an exception. Instead of an exception, we could use something less invasive, such as null or a message to the console.
  • (29). Movie class - similar to the Actor class.

And how can we use such classes?

Java
var movie1 = new mt.mas.associationsid.Movie(1, "T1", new int[]{3, 4});  // The 'var' requires Java 10+
var movie2 = new mt.mas.associationsid.Movie(2, "T3", new int[]{3});

var actor1 = new mt.mas.associationsid.Actor(3, "AS", new int[]{1, 2});
var actor2 = new mt.mas.associationsid.Actor(4, "MB", new int[]{1});
var actor3 = new mt.mas.associationsid.Actor(5, "KL", new int[]{2});

// Show information about the movie1
System.out.println(movie1.title);
for(int i = 0; i < movie1.actorIds.length; i++) {
	System.out.println("   " + mt.mas.associationsid.Actor.findActor(movie1.actorIds[i]).name);
}

	// Show information about the actor1
System.out.println(actor1.name);
for(int i = 0; i < actor1.movieIds.length; i++) {
	System.out.println("   " + mt.mas.associationsid.Movie.findMovie(actor1.movieIds[i]).title);
}

Let's look at:

  • (1). Creating an instance of the Movie class. We provide the identifier, title, and array of int numbers referring to the identifiers of the actors that are playing in it.
  • (4). Creating objects describing actors. As in the case of movies, we provide: id, initials, and array of numbers containing references to the movies. Note that if the "3" actor played in the movie "T1", actress "3" must also have information about the movie "1". This makes our relationships bi-directional.
  • (9). View movie information. In the loop, we call the findActor(...) method, which returns the reference to the object describing it.

As a result, we get the state of the console shown below:

Plain Text
T1
	AS
	MB
AS
	T1
	T3

How can we summarize the presented solution? Let's start with the flaws, or rather the essential one. It is necessary to search for an object based on its identification number, which can lead to performance problems. You can definitely improve it by using mapping containers instead of a regular list or an array. The sample code illustrating this modification is shown below.

Java
public class Movie {

	// [...]

	private static Map<Integer, Movie> extent = new TreeMap<>();

	public Movie(int id, String title, int[] actorIds) {
		// Add to the extent
		extent.put(id, this);

		// [...]
	}

	public static Movie findMovie(int id) throws Exception {
		return extent.get(id);
	}
}
  • (5).The class attribute is a map where the key is Integer, and the value is a reference to the corresponding object.
  • (9). We see that in the constructor we also add the object to the extent, but due to the specificity of the map we do it differently than before.
  • (15). By using the method built into the map our search has also been simplified (and it is much faster as well).

Remember that despite the use of the mapping structure, we still need to search in the extent. The map is much faster than the iterative browsing of the whole extent, but it may take a little while.

Let's move on to the advantages. It can be summarized in one sentence: independence of individual objects from each other. Because we do not use Java references, the virtual machine knows nothing about our connections. This is very important when, for example, we want to read one object from a database or upload it over a network.

Implementation with references

Now let's take a second approach to storing information about related objects. This solution is probably more common, and can be summarized in the following points:

  • We use native references (Java, C#) or pointers (C++) to process related objects. This way we do not have to search for objects.
  • Having a reference (or a pointer) pointing to the object, we have an instant access to it (faster is no longer possible). Simply virtual machine reads from a specific memory location, appropriately interpreting the data contained there.
  • As in the previous case, this way also requires creating reference pairs (if we want to navigate in two directions - and mostly we want).

Depending on the cardinalities of the association we use different constructions:

  • Cardinality 1 to 1. A single reference on each side. As you can see in the code below, in each class we have a single reference pointing to the connected object. For the Actor class, this is a reference movie of the type Movie, and for the Movie class: actor of type Actor.
    Java
    public class Actor {
    	public String name;	// public for simplicity
    	public Movie movie; // implementation of an association, cardinality "1"
    
    	public Actor(String name, Movie movie) {
    		this.name = name;
    		this.movie = movie;
    	}
    }
    
    public class Movie {
    	public String title; // public dla uproszczenia
    	public Actor actor; // implementation of an association, cardinality "1"
    
    	public Movie(String title, Actor actor) {
    		this.title = title;
    		this.actor = actor;
    	}
    }
    
  • Cardinality 1 do *. Single reference and a container. Sample source code is below. In the Actor class, the situation is the same as in the previous case. By contrast, in the Movie class, we have a container that holds the Actor reference (because we need to be able to show the "*" count for many instances). Here we have used the parameterized ArrayList class, but it may be another container, depending on your needs.
    Java
    public class Actor {
    	public String name; // public for simplicity
    	public Movie movie; // implementation of an association, cardinality "1"
    
    	public Actor(String name, Movie movie) {
    		this.name = name;
    		this.movie = movie;
    	}
    }
    
    public class Movie {
    	public String title; // public for simplicity
    	public ArrayList<Actor> actors; // implementation of an association, cardinality "*"
    
    	public Movie(String title) {
    		this.title = title;
    		this.actors = new ArrayList();
    	}
    }
    
  • Cardinality * to *. Two containers (one on each side). Below is an example source code. Note that in both classes there are collections - just like in the previous example for the "*" count.
    Java
    public class Actor {
    	public String name; // public for simplicity
    	public ArrayList<Movie> movie; // implementation of an association, cardinality "*"
    
    	public Actor(String name) {
    		this.name = name;
    	}
    }
    
    public class Movie {
    	public String title; // public for simplicity
    	public ArrayList<Actor> actor; // implementation of an association, cardinality "*"
    
    	public Movie(String title) {
    		this.title = title;
    	}
    }
    
Knowledge Check
Select all that apply

Which statements are true?

Directed association

The implementation of the directed association is obviously simpler than bidirectional and probably does not require comment. Just remember that its practical usefulness is quite limited, because we have difficulty accessing information, eg Actor does not know in which movies was playing.

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

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

public class Movie {
	public String title; // public for simplicity
	public ArrayList<Actor> actor; // implementation of an association, cardinality "*"

	public Movie(String title) {
		this.title = title;
	}
}
Knowledge Check
Select all that apply

Which statements are true?

Recursive association

Definition

Recursive association describes the connection between objects of the same class.

Because of this specificity, the role names in the case are mandatory. Otherwise we would not be able to tell which side the role plays.

The implementation is based on the same principles as the "classical" association. Let's look at the diagram and the code. We still have two containers (for * - *), but this time they are located in one class:

Recursive association
Java
public class Actor {
	public String name;	// public dla uproszczenia

	private ArrayList<Actor> parents = new ArrayList<Actor>();
	private ArrayList<Actor> children = new ArrayList<Actor>();

	// ...
}
Knowledge Check
Select all that apply

Which statements are true?

Association with attribute

In some situations it is necessary to store additional information about a particular association/link (connection of objects). Let's suppose we want to keep the information about companies and their employees, but one employee may have several jobs. Additionally, we would like to keep the period of time spent working in individual companies. How can we do that? The simplest idea that comes to mind is to insert the appropriate attributes into one of the classes. Unfortunately this will not work because we will have many values ​​(for different companies / employees) and connected to another class.

The best way to solve this problem is to use the association (class) attribute (see figure below). This is a normal class (has the same characteristics as any other), but "connected" to the association. Instances of an association class carry additional information about a particular link. You can think of it as a "sticky note" attached to the thread that represents the link.

Association attribute

To make it easier to understand the mechanism of the association class, let's look at the figure below, which contains an object diagram (not a class one). We see two Employee class instances, three Company class instances, and four instances of association classes. Based on the diagram we can say that:

  • Jan Kowalski worked in three companies:
    • „Speedo” from 2003.05.21 to 2004.06.14,
    • „Fasto” from 2004.07.01 to 2005.02.14,
    • „Medio” from 2005.03.01 to 2006.12.01,
  • Wojciech Nowak worked only in "Medio" company from 2003.01.01 to 2006.01.14. Let us also point out that it is the same company in which Jan Kowalski also worked. In other words, a particular object can be connected to many other objects. Of course, only when cardinalities allow - in our example we have "many to many", so everything is OK.
Diagram of objects

As we mentioned, the association attribute is a fully qualified class, so you can connect other associations and class-specific elements. However, it seems that this should be avoided. If this is business-justified and we want to add an association with an association class, then it's worth considering whether to make a "normal" class of it.

Commentary

Let's remember that the association attribute must be used when we have the cardinality * - *. In other cases it is unnecessary (the informationcould be put in the class, on the "1" side) and we should avoid it.

Implementing the association attribute requires a prior transformation of the diagram. The association attribute is replaced by the middle class and the two associations 1-* and *-1 - as in the diagram below.

Introduction of intermediate class

As a result, by replacing the association attribute with the intermediate class, we have received two "regular" associations. These "new" associations are implemented using one of the already known methods discussed earlier. Unfortunately, after this transformation we can expect minor problems with the semantics of this new class:

  • What name to give it? If we do not come across any meaningful business name, then we can create a new as a cluster of neighboring class names. Some argue that this should always be done - so you can immediately see that it is an intermediate class;
  • Another problem may be the names of "old" and "new" roles. Previously, we had two well-defined business roles. After the transformation we received two more. The good news is that we usually create new names by duplicating old ones.

The disadvantage of this approach is the difficulty of accessing the target. Previously, from the object Movie through the actor role, we got to the Actor instance. After the change we have to "pass" through the intermediate class object. Alternatively, you can implement some helper methods that will return the target after the call from the mediator class.

Knowledge Check
Select all that apply

Which statements are true?

Qualified association

In a classic binary association, to find a particular object on the other side, we have to look through all the links until we find one that meets our criteria (eg, a PESEL employee). On contrary, in the qualified association, we can retrieve the target object based on the qualifier very quickly. Let's look at the definition:

Definition

Qualifier is an attribute (or combination of attributes) that uniquely identifies the target object.

The difference between a simple association and qualified one is well illustrated by the drawing consisting of two parts:

Qualified association
  • (a) describes the employee's relationship with the company through a classic association. In order to find an employee with a particular PESEL number, we have to look through all the employees available through the links and for each of them we check whether he/she has the required PESEL.
  • (b) It also describes the employee's relationship with the company, but using a qualified association. Thanks to this solution, having a specific "qualifier" (PESEL number), we immediately get the corresponding object (employee).

The simplest implementation of a qualified association (actually its functionality) can be implemented as follows:

  • we use the existing association implementation approach (no difference, if we use identifiers or references),
  • we add a method, which based on the PESEL, returns an object of the Employee class.

Note that this approach is characterized by poor performance, because to find a specific PESEL, we need to "manually" view all the connected employees.

A better solution (and it is recommended) can be characterized as follows:

  • We do not use a standard container (such as ArrayList),
  • We use a mapping container where the key is the qualifier (eg PESEL) and the value is the reference to the target object (for example, e.g. of the Employee class).
  • Returning link is stored in the "standard" way (e.g. ArrayList).

Example code along with diagram and explanations:

Qualified association
Java
public class Actor {

	// [...]

	private Map<String, Movie> moviesQualif = new TreeMap<>();

	public void addMovieQualif(Movie newMovie) {
		// Check if we already have the info
		if(!moviesQualif.containsKey(newMovie.title)) {
			moviesQualif.put(newMovie.title, newMovie);

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

	public Movie findMovieQualif(String title) throws Exception {
		// Check if we have the info
		if(!moviesQualif.containsKey(title)) {
			throw new Exception("Unable to find a movie: " + title);
		}

		return moviesQualif.get(title);
	}
}
  • (1). A class that stores actor information.
  • (5). A map that is a key element in the implementation of a qualified association. The key is the qualifier (in our case the title of the movie), and the value is a reference to the target (movie).
  • (7). A method to create a connection within a qualified association. It also supports the returning link.
  • (9). We are checking to see if there is already information about the movie with such a title.
  • (10). If not, then we add a new entry to the map (the key is the title and the value is the reference to the movie object)
  • (13). Then we create a returning link by calling the appropriate method from the Movie class.
  • (17). The method that retrieves the target (movie) based on the qualifier (movie title).
  • (20). If the map does not contain a given key, it means that we do not have the information. In this situation we throw an exception. As we have already mentioned, this can be handled differently, eg by returning null.
  • (23). We use an already existing method from the map, which returns a value based on a key.
Knowledge Check
Select all that apply

Which statements are true?