Programming in C++

Introduction to Object Oriented Programming (OOP)

Modelling "Has-a Relationships"

Gerald Senarclens de Grancy

What are "Has-a Relationships"

Classes can have data members of any type, including complex objects

For example

  • a class contains vector<int64_t> as data member
  • it is then said that it has a vector<int64_t>

There are two different has-a relationships called
aggregation and composition

In addition, there are two similar, but weaker relationships called
dependency, association

Besides "has-a" relationships there are also "is-a" relationships (inheritance)

Examples to illustrate these relationships are frequently highly artificial and have little to do with actual programming (eg. pets, cars or houses)

To have independent objects relate to each other in a stable system, we should (will) study design patterns

Dependency

A dependency typically implies that an object accepts another object as a method parameter, instantiates, or uses another object

Association, aggregation and composition (see the following slides) imply a dependency

UML
dotted line with arrow towards used object
Mermaid syntax
class Master ..> class Dependency
class Dependency <.. class Master

Example

A trivial example is a class that uses the output stream operator

What does this class depend on?

---
  config:
    class:
      hideEmptyMembersBox: true
---

classDiagram
  Master ..> ostream : Master depends on ostream
Dependencies of class Master

Association

Association is the most general type of relationship between classes
it simply indicates that there is a connection

  • Usually implies that an object has another object as property
  • Can be one to one, one to many, many to one and many to many
  • Does not make implications on lifecycle or ownership

Aggregation and composition are associations

UML
Bidirectional association or arrow towards used object
Mermaid syntax
class A <--> class B
class Master --> class Used
class Used <-- class Master

Example

---
  config:
    class:
      hideEmptyMembersBox: true
---

classDiagram
  Course --> Student : Course has student(s)
---
  config:
    class:
      hideEmptyMembersBox: true
---

classDiagram
  Course <--> Student : Classes are associated
---
  config:
    class:
      hideEmptyMembersBox: true
---

classDiagram
  Course <-- Student : Student has course(s)
Associations between Course and Student classes

Aggregation

Aggregation models a "has-a" relationship

  • The member can exist independent of containing object
  • Container A "uses" member B and B exists independently from A
  • Aggregation is a weaker directed association than composition
  • Member can be part of more than one containing object at a time
  • Conceptual usage is high
  • Practical usage in C++ is rather limited
    • Lifetime is not managed by containing classes' ctor and dtor
    • Containing object only holds a raw pointer, a reference, or a std::shared_ptr to contained object
UML
Hollow diamond towards containing object
Mermaid syntax
Master o-- Used
---
  config:
    class:
      hideEmptyMembersBox: true
---

classDiagram
  Master o-- Used : Master has one or more Used
Example: modelling a school
School (master) has vector<Person> (student members)
Persons still exist when School is deleted

Example: Circle Center (Reference - Avoid when Copyable!)

#include <iostream>
namespace draw {

struct Point {
  Point(double x, double y) : x(x), y(y) {};
  double x = 0.0;
  double y = 0.0;
};

class Circle {
public:
  Circle(Point& center, double radius) : center_(center), radius_(radius) {};
  void draw() { std::cout << "drawing Circle(" << center_.x << "/"
    << center_.y << ", " << radius_ << ")" << std::endl; }
private:
  Point& center_;  // references cannot be reassigned; use Point* if needed
  // assigning a new value to a reference would use copy assignment of the type
  double radius_{1.0};
};
}  // namespace draw

int main(void) {
  draw::Point center{17, -2};  // devs have to ensure `center` stays in scope
  draw::Circle c(center, 5);
  draw::Circle c2(center, 12);  // center can be shared
  c.draw();
  c2.draw();
}

Visualize execution

Example: Circle Center (Pointer)

#include <iostream>
namespace draw {

struct Point {
  Point(double x, double y) : x(x), y(y) {};
  double x = 0.0;
  double y = 0.0;
};

class Circle {
public:
  Circle(Point* center, double radius) : center_(center), radius_(radius) {};
  void draw() { std::cout << "drawing Circle(" << center_->x << "/"
    << center_->y << ", " << radius_ << ")" << std::endl; }
  void center(Point* c) { center_ = c; }  // only works with pointers
private:
  Point* center_{nullptr};  // pointer can be reassigned
  double radius_{1.0};
};

}  // namespace draw

int main(void) {
  draw::Point center1{17, -2};  // devs have to ensure `center` stays in scope
  draw::Point center2{-18, 12};
  draw::Circle c(&center1, 5);
  c.draw();
  c.center(&center2);
  c.draw();
}

Visualize execution

Composition

Composition models a strong "has-a" association between two objects

  • Container A "owns" member B: B does not exist in the system without A
  • Member can only belong to one object at a time
  • It has its lifetime managed by the containing object
  • Users of the containing object do not need to manage its parts' lifetime
  • Compositions are one of the easiest relationship types in C++
  • Composition designs are robust: classes clean up after themselves
  • Design C++ classes to use composition if possible
  • Compositions requiring the free store may use pointer members
UML
Filled diamond towards containing object
Mermaid syntax
Master *-- Used
---
  config:
    class:
      hideEmptyMembersBox: true
---

classDiagram
  Master *-- Used : Master has one or more Used
Example
Directory (master) has vector<File> (members)
Files cannot exist without a containing Directory

Example: Circle Center

#include <iostream>
namespace draw {

struct Point {
  Point(double x, double y) : x(x), y(y) {};
  double x = 0.0;
  double y = 0.0;
};

class Circle {
public:
  Circle(Point center, double radius) : center_(center), radius_(radius) {};
  void draw() { std::cout << "drawing Circle(" << center_.x << "/"
    << center_.y << ", " << radius_ << ")" << std::endl; }
private:
  // as an alternative a Point* could be used (via new and delete in ctor);
  // this would require a dtor and should only be done for large objects
  Point center_{0, 0};
  double radius_{1.0};
};

}  // namespace draw

int main(void) {
  draw::Point center{-2, -5};
  draw::Circle c(center, 3);
  c.draw();
}

Visualize execution

Overview of Defining Object Relationships in Mermaid

TypeDescription
<|--Inheritance
*--Composition
o--Aggregation
-->Association
--Link (Solid)
..>Dependency
..|>Realization
..Link (Dashed)
classDiagram
  classA --|> classB : A inherits from B
  classC --* classD : Composition
  classE --o classF : Aggregation
  classG --> classH : Association
  classI -- classJ : Link(Solid)
  classK ..> classL : Dependency
  classM ..|> classN : Realization
  classO .. classP : Link(Dashed)
  classQ <--> classR : Association
  classA: +int boo
  classA:  +foo()
Object relationships

Best Practice

Implement the simplest relationship that meets your program's needs,
not what seems right in real-life.

Best Practices

Model in the easiest way possible with your programming language

KISS
Keep it simple, stupid
YAGNI
You aren't gonna need it
C.12
Don't make [...] references in a copyable or movable type

Prefer simpler models to real life models (in C++, use composition when possible and aggregation when needed)

Don't over-model class diagrams

Questions
and feedback...

Further Reading

Alex Aggregation https://www.learncpp.com/cpp-tutorial/aggregation/
Alex Composition https://www.learncpp.com/cpp-tutorial/composition/