Understanding the Connections Between Classes in OOP

Let's start with a common term used with a class association called 'Coupling'.

Class Coupling

Class Coupling

  • It quantifies the level of interconnectedness among classes and subsystems.
  • Tight coupling between classes makes them more difficult to modify, as changes in one class can cascade across many others.
  • Loosely coupled software allows for easier modifications compared to tightly coupled software
  • Classes exhibit two main types of relationships: Inheritance and Composition.

Inheritance

  • It's when one class can use code from another.
  • For example, saying a Car is a type of Vehicle.
  • This helps reuse code and make objects behave differently based on their types.

Usage

public class Car : Vehicle
{
}

Example

Below is a Presentation class object. Text, Table, and Image classes are child classes/ derived from the base Presentation class

Presentation class object

class Presentation
{
    public string Title { get; }
}

class Text : Presentation
{
}

class Image : Presentation
{
}

class Table : Presentation
{
}

Composition

  • It's when one class has another class inside it.
  • For instance, a Car might have an Engine.
  • The good things about Composition are that it helps reuse code, makes things flexible, and keeps them loosely connected.

Example

class Logger
{
    // Method to log a message
    public void Log(string message) => Console.WriteLine($"Logging: {message}");
}

class Installer
{
    private readonly Logger _logger;

    // Constructor to initialize Installer with a Logger instance
    public Installer(Logger logger) => _logger = logger;

    // Method to perform installation
    public void Install()
    {
        _logger.Log("Installing...");
        // Installation logic here
        _logger.Log("Installation complete.");
    }
}

class DbMigrator
{
    private readonly Logger _logger;

    // Constructor to initialize DbMigrator with a Logger instance
    public DbMigrator(Logger logger) => _logger = logger;

    // Method to perform database migration
    public void Migrate()
    {
        _logger.Log("Migrating database...");
        // Database migration logic here
        _logger.Log("Migration complete.");
    }
}

Composition or Inheritance. Which one to choose?

Let's consider an example to understand more.

Suppose we have a Shape class representing various shapes such as circles, squares, and triangles. We then create subclasses Circle, Square, and Triangle that inherit from the Shape class.

class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing a generic shape.");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square.");
    }
}

class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle.");
    }
}

Now, let's say we want to introduce a new shape called Rectangle. We decided to create a new subclass Rectangle that inherits from the Shape class.

class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a rectangle.");
    }
}

However, what if later we realize that a rectangle has different properties compared to other shapes? For example, rectangles have a width and height, while other shapes do not.

class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }

    public override void Draw()
    {
        Console.WriteLine("Drawing a rectangle.");
    }
}

In this situation, opting for inheritance from the Shape class might not be ideal due to the inconsistency in properties among different shapes.

A more effective strategy would involve employing composition over inheritance. By creating a Rectangle class that encompasses a Shape object rather than inheriting from it, we can better capture the relationship between the entities. This approach ensures a more accurate representation and mitigates the risk of encountering inheritance-related issues.

To solve the problem using composition instead of inheritance, we can create a Rectangle class that contains a Shape object as a member. This way, we can leverage the functionality of the Shape class without inheriting it directly. Here's how we can implement it.

using System;

class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing a generic shape.");
    }
}

class Rectangle
{
    private readonly Shape _shape;

    public Rectangle(Shape shape)
    {
        _shape = shape;
    }

    public void Draw()
    {
        Console.WriteLine("Drawing a rectangle.");
        _shape.Draw(); // Delegate drawing the generic shape to the Shape object
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square.");
    }
}

class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle.");
    }
}

class Program
{
    static void Main()
    {
        var circle = new Circle();
        var square = new Square();
        var triangle = new Triangle();

        var rectangleWithCircle = new Rectangle(circle);
        var rectangleWithSquare = new Rectangle(square);
        var rectangleWithTriangle = new Rectangle(triangle);

        rectangleWithCircle.Draw(); // Drawing a rectangle, Drawing a circle
        rectangleWithSquare.Draw(); // Drawing a rectangle, Drawing a square
        rectangleWithTriangle.Draw(); // Drawing a rectangle, Drawing a triangle
    }
}

I hope this article will help you in understanding various associations between classes while designing. Wishing you successful coding!


Similar Articles