Introduction
Association, aggregation, dependency all are developed relationship between objects. The relationship is attributed on the basis of the interaction of the objects. In this article, we will explore these relationships.
Association
Association relationship develops between objects when one object uses another object. Technically, it is a reference based relationship between two classes. One class holds a class level reference to the other class.
Example
........
class Teacher
{
}
class Student
{
List<Teacher> teachers;
}
In the above example, a Teacher
list is referred in the student
class. The system interprets a student
has multiple teachers. In this student teacher
relationship, one is being used by another. But there is no part-whole relationship between them. This means neither of them is the part of another. Hence they have their own lifetime.
Aggregation
Aggregation is the same as association. Like association, this relationship develops while one object uses the other. Additionally, the objects develop a part-whole relationship and the lifetime of part does not depend on life time of whole.
Example
........
class CellBattery
{
}
class CellPhone
{
CellBattery _battery;
void CellPhone (CellBattery battery)
{
_battery = battery;
}
}
In real life, cellphone battery is a part of cellphone. If for some reason (other than battery damage), my cellphone became non-functional, still I can use the battery in another phone. This kind of relationship implies aggregation. The lifetime of Part (cellphone battery), doesn’t depend on the lifetime of Whole (cellphone).
This real life scenario has been captured through the above coding. We are setting a CellBattery
instance from outside the CellPhone
class. This ensures, while the CellPhone
gets destroyed (garbage collected), CellBattery
survives. This CellBattery
could be reused to some other CellPhone
.
Difference between Association and Aggregation
Aggregation is the same as association and is often seen as a redundant relationship. Technically, aggregation doesn’t convey anything more effective about a software design than an association. The difference lies from the conceptual point of view. When the objects develop a Part-Whole relationship and the lifetime of Part doesn’t depend on the lifetime of Whole, the relationship is attributed as aggregation. But in case of association, there is no Part-Whole relationship.
Composition
In composition also, one object uses another object and they develop a Part-Whole relationship. But here the lifetime of Part depends on the lifetime of Whole.
Example
class Hotel
{
List<Room> rooms = new List<Room> ();
}
class Room
{
}
In real life, room
s are part of Hotel
. If the Hotel
is destroyed, obviously the rooms of the hotels will meet the same fate. This kind of relationship implies composition. The lifetime of Part (hotel’s room depends on the lifetime of Whole (hotel).
This real life scenario has been captured through the above coding. We are creating Rooms inside the Hotel
class. This kind of implementation ensures, while the Hotel
get destroyed (garbage collected), Room
is also destroyed.
Dependency
Dependency can develop between objects while they are associated, aggregated or composed. This kind of relationship develops while one object invokes another object’s functionality in order to accomplish some task. Any change in the called object may break the functionality of the caller.
Example
class GreetingSender
{
EmailSender _emailSender;
void SendGreetings(EmailSender emailSender)
{
_emailSender = emailSender;
_emailSender.SendEmail();
}
}
class EmailSender
{
public void SendEmail()
{
}
}
In the above example, Greetings
are being sent through Email
. The GreetingSender
object is using the SendEmail ()
method of EmailSender
object to accomplish it’s task. Now, if any modification ((e.g. introduction of a parameter) is made to the SendEmail()
method, the SendGreetings()
method of GreetingSender
class will break. Also, as per the implementation, we cannot send greetings by any other mode but only through Email
. That way, my GreetingSender
object’s functionality is dependent on EmailSender
object’s functionality. This kind of relationship is termed as dependency.
Dependency Injection
Dependency Injection is a practice about injecting functional dependency in the object. In the above example, we have injected a dependency into the GreetingSender
object. We can understand the injected dependency has developed a tight coupling between GreetingSender
and EmailSender
objects. We cannot send greetings by any other mode but only through email. So our injected dependency is violating the design principle. As a best practice, we should inject dependency in such a way that objects will be decoupled and re-usable.
IoC (Inversion of Control)
The term Inversion of Control (IoC) refers to a programming style where a framework or runtime controls the program flow. Inversion of control means we are changing the control from normal way.
If we analyze our above dependency example, we can see the EmailSender
object is controlling the GreetingSender
object’s SendGreetings
functionality. We cannot send greetings by any other mode but only through email. But this behavior is not desired. The implementation results to tight coupling. As a solution, we can invert the control flow and let the runtime control its SendGreetings
functionality. The runtime will take the decision, which mode of communication (Email/SMS) should be used for the functionality. This kind of solution followed a principle termed as IoC. We can implement the principle using the dependency injection. We can say, IoC is the strategy and DI is the implementation.
Let’s see how we can solve our dependency problem using IOC principle.
Solution of the Dependency Problem
The GreetingSender
class aggregating the EmailSender
class should not depend on the direct implementation of the class. GreetingSender
class should depend on the abstraction of the greeting sending functionality, rather than the concrete implementation of the functionality (sending greetings through email).
Let’s define the abstraction of “send
” functionality on defining an ISender
Interface:
interface ISender
{
void Send();
}
Implement the EmailSender
class from this interface. Implement email sending functionality in the Send()
method:
public class EmailSender: ISender
{
void Send()
{
Console.WriteLine("Sending Email....");
}
}
Aggregate the abstraction of “send
“ functionality in the GreetingSender
class:
class GreetingSender
{
ISender _sender;
public GreetingSender(ISender sender)
{
_sender = sender;
}
void SendGreetings()
{
_sender.Send();
}
}
In the above implementation, we are injecting abstraction of the functionality, not the concrete functionality. With this kind of injected dependency, we made the objects loosely coupled. Here, I am not defining anywhere, what kind of communication mode (Email or SMS) , I will be used for sending greetings. In future, if I want to send Greetings through SMS, I will simply define a SMSSender
class and implement the send
method for sending SMSes.
public class SMSSender : ISender
{
void Send()
{
Console.WriteLine("Sending SMS....");
}
}
Now, let’s provide the concrete instances from some other module, for sending greetings using email or SMS:
EmailSender emailSender = new EmailSender();
SMSSender smsSender = new SMSSender();
GreetingSender greetingsEmailSender = new GreetingSender(emailSender);
greetingsEmailSender.SendGreetings();
GreetingSender greetingsSMSSender = new GreetingSender(smsSender);
greetingsSMSSender.SendGreetings();
Console.ReadLine ();
Now, our greetings
class is not controlled by anyone in terms of communication mode for greetings sending. The class has become loosely coupled with EmailSender
/SMSsender
class. The control has been inverted. The runtime has taken the control.