Understanding Relationships Between Classes in Object-Oriented Programming
Inheritance
Inheritance enables a class (subclass) to acquire the functionality of another class (superclass) and extend it with new features. In Java, this relationship is defined using the extends keyword. UML diagrams represent inheritance with a solid line ending in an empty triangle arrow, pointing from the subclass to the superclass.
class LivingEntity {
protected String identifier;
public LivingEntity(String identifier) {
this.identifier = identifier;
}
public void consume() {
System.out.println(identifier + " consumes.");
}
}
class Cat extends LivingEntity {
private String furColor;
public Cat(String identifier, String furColor) {
super(identifier);
this.furColor = furColor;
}
@Override
public void consume() {
System.out.println(identifier + ", a " + furColor + " cat, consumes.");
}
public void vocalize() {
System.out.println(identifier + " meows!");
}
}
class Driver {
public static void main(String[] args) {
LivingEntity entity = new LivingEntity("Generic Entity");
entity.consume();
Cat cat = new Cat("Whiskers", "gray");
cat.consume();
cat.vocalize();
}
}
Implementation
Implementation occurs when a class fulfills the contract defined by an interface. In Java, the implements keyword denotes this relationship. UML uses a dashed line with an empty triangle arrow from the class to the interface.
interface Executable {
void execute();
}
class Employee implements Executable {
private String employeeName;
public Employee(String employeeName) {
this.employeeName = employeeName;
}
@Override
public void execute() {
System.out.println(employeeName + " executes a task.");
}
}
class Driver {
public static void main(String[] args) {
Employee employee = new Employee("Bob");
employee.execute();
}
}
Dependency
Dependency describes a transient, weak relationship where one class uses another, typically through method parameters or local variables. Changes in the used class may affect the dependent class. UML represents this with a dashed line arrow from the dependent class to the used class.
class DisplayDevice {
public void show(String data) {
System.out.println("Displaying: " + data);
}
}
class Report {
private DisplayDevice device;
public Report(DisplayDevice device) {
this.device = device;
}
public void present(String data) {
device.show(data);
}
}
class Driver {
public static void main(String[] args) {
DisplayDevice device = new DisplayDevice();
Report report = new Report(device);
report.present("Annual Summary");
}
}
Association
Association represents a semantic, long-term relationship between classes, stronger than dependency. It can be unidirectional or bidirectional. In code, this often appears as a class attribute referencing another class. UML uses a solid line arrow with optional role and multiplicity annotations.
class Customer {
private String customerName;
public Customer(String customerName) {
this.customerName = customerName;
}
public String getCustomerName() {
return customerName;
}
}
class Order {
private String orderId;
private Customer purchaser;
public Order(String orderId) {
this.orderId = orderId;
}
public void assignPurchaser(Customer purchaser) {
this.purchaser = purchaser;
}
public void process() {
if (purchaser != null) {
System.out.println("Order " + orderId + " processed for " + purchaser.getCustomerName());
} else {
System.out.println("Order " + orderId + " has no purchaser.");
}
}
}
class Driver {
public static void main(String[] args) {
Customer customer = new Customer("Charlie");
Order order = new Order("ORD123");
order.assignPurchaser(customer);
order.process();
}
}
Aggregation
Aggregation is a specific type of association representing a whole-part relationship where parts can exist independently of the whole. It models a "has-a" relationship with shared or multiple ownership. UML denotes aggregation with a hollow diamond on the whole side.
class Library {
private String libraryName;
private Book[] collection;
public Library(String libraryName) {
this.libraryName = libraryName;
}
public void includeBook(Book book) {
if (collection == null) {
collection = new Book[1];
collection[0] = book;
} else {
Book[] newCollection = new Book[collection.length + 1];
System.arraycopy(collection, 0, newCollection, 0, collection.length);
newCollection[collection.length] = book;
collection = newCollection;
}
}
public void listContents() {
System.out.println("Library: " + libraryName);
if (collection != null) {
for (Book book : collection) {
System.out.println("Book: " + book.getTitle());
}
}
}
}
class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
class Driver {
public static void main(String[] args) {
Book book1 = new Book("Design Patterns");
Book book2 = new Book("Clean Code");
Library library = new Library("City Library");
library.includeBook(book1);
library.includeBook(book2);
library.listContents();
}
}
Composition
Composition is a stronger form of aggregation where the part cannot exist without the whole, representing a "contains-a" relationship. The part's lifecycle is tied to the whole. UML uses a filled diamond on the whole side.
class Engine {
public void operate() {
System.out.println("Engine is running.");
}
}
class Vehicle {
private Engine powerUnit;
public Vehicle() {
this.powerUnit = new Engine();
}
public void move() {
powerUnit.operate();
}
}
class Driver {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.move();
}
}
Coupling Levels
Class relationships vary in coupling strength from high to low:
- Inheritance: High coupling. Subclasses are tightly linked to superclasses; changes in superclasses diretcly affect subclasses.
- Implementation: Medium coupling. Classes implementing interfaces depend on interface contracts but can be independent of other implementations.
- Aggregation: Low coupling. Parts can exist independently, with the whole having limited knowledge of parts.
- Composition: High coupling. Parts depend on the whole for existence but can be replaced.
- Association: Variable coupling. Classes reference eachother, possibly through temporary or paramter-based interactions.
- Dependency: Low coupling. One class uses another transiently without holding a persistent reference.
Design Principles
High cohesion and low coupling are key principles for maintainable and extensible systems.
- High Cohesion: Elements within a module are closely related and focused on a single responsibility. Benefits include easier understanding, testing, and modification.
- Low Coupling: Modules have minimal dependencies, allowing independent changes and reuse.
Strategies to achieve these principles include:
- Applying SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion).
- Utilizing design patterns (e.g., Factory, Strategy, Observer).
- Implementing dependency injection.
- Relying on interfaces and abstract classes for interactions.