Skip to content

Session 9

Implementing associations

Updated View as Markdown

Associations carry the interesting decisions: which side holds the reference, whether both sides do, and how an association class is represented. This session implements two small but complete examples.

Objectives

Do not copy. Read for understanding and the viva
  • Complete questions 21 to 22 of the manual: implementing associations
  • Prepare the deliverable before the lab and finish it during the session
  • Be ready to explain every step in the viva

Questions Covered

Do not copy. Read for understanding and the viva
QuestionRequirementStatus
Q21Implement the following Associations using C++/JavaComplete
Q22Implement the following Associations using C++/JavaComplete

Preparation

Do not copy. Read for understanding and the viva
  • The manual asks for a problem description of 300 to 500 words and a list of assumptions before every diagram. Write both first; they fix the scope the evaluator marks you against.
  • The manual says C++ or Java. Every program here is given in C++, Rust, Python and TypeScript, and any of the four is acceptable in the lab; use the language you chose in Session 8.
  • Decide navigability: one-way associations need a reference on one side only; two-way ones need both plus code to keep them consistent.
  • An association class (OrderLine style) becomes its own class holding references to both ends.

Question 21

Problem Statement

Write in lab record

Implement the following Associations using C++/Java.

Train-Journey association

Figure 1.16: Train-Journey Association

Solution

Write in lab record

Assumptions

A railway keeps a list of trains and a list of journeys. A train has a number, a type such as Rajdhani or Shatabdi, and a maximum speed. A journey has a source station, a destination station and a journey time in hours, and records the number of the train that runs it. One train can be assigned to any number of journeys, including none, and a journey is assigned to at most one train at a time. Both ends of the link have role names in the figure, assignedTrain on the train side and assignedJourny on the journey side, and the line has no arrowhead, so the link must be navigable in both directions: from a journey you reach its train, and from a train you list its journeys. The program must let the operator assign a journey to a train, move a journey to another train, take a journey off its train, set and read the stations and the train type, and query a journey time or a train speed by train number. Whatever the sequence of operations, the two ends must never disagree.

  • Two-way association: TrainJourney::assignedTrain is a pointer (0..1) and Train::assignedJourny is a std::vector of pointers (0..*).
  • Both ends change only inside two free functions, assign and unassign. main never touches the pointers or the vector directly.
  • assign first calls unassign, so a journey can never appear under two trains.
  • The figure gives Train_No as an attribute of TrainJourney and also as a parameter of the getters. assign copies the train’s number into the journey, and the getters answer only when the number passed matches; otherwise they return a marker ((not this train) or -1).
  • Attribute and operation names keep the figure’s spelling, including Journy_Time and Set_Dastination_St.

Diagram elements

ClassAttributesOperations
TrainJourneyTrain_No: int, Source_St: String, Destination_St: String, Journy_Time: floatSet_Source_St(source: String), Set_Dastination_St(destination: String), Get_Source_St(Train_No: int): String, Get_Journy_Time(Train_No: int): float
TrainTrain_No: int, Train_Type: String, Max_Speed: floatGet_Train_No(): int, Set_Train_Type(trtype: String), Get_Train_Speed(Train_No: int): float

Association: TrainJourney 0..* (role assignedJourny) to Train 0..1 (role assignedTrain), no arrowhead, so two-way.

How the objects point at each other after the first three assign calls in main (each arrow is a pointer stored in the object at its tail):

 rajdhani (Train 12951)                shatabdi (Train 12009)
 assignedJourny: [ j1, j2 ]            assignedJourny: [ j3 ]
     |      |                              |
     v      v                              v
    j1     j2                             j3
 assignedTrain ---> rajdhani           assignedTrain ---> shatabdi
 assignedTrain ---> rajdhani

Every journey in a train’s vector points back at that train, and no journey is in two vectors. assign and unassign are the only code that may change this picture.

Steps

  1. Save the listing below as train_journey.cpp in a session-9 folder.
  2. Compile: clang++ -std=c++17 -Wall -Wextra -o train_journey train_journey.cpp.
  3. Run ./train_journey and paste the output.
  4. For another language, save the matching tab and run it: rustc -O --edition 2021 train_journey.rs && ./train_journey, python3 train_journey.py, or node train_journey.ts (Node 22.18 or later strips the types natively, no compiler needed).

Program

Lab record: write one language only. Pick yours once and every page opens on it; the other tabs are the same solution for comparison.

train_journey.cppcpp
// train_journey.cpp -- MCSL-222 Session 9, Q21
// Figure 1.16 (Train Journey -- Train) implemented in C++17 as a TWO-WAY
// association: TrainJourney.assignedTrain (0..1) and Train.assignedJourny (0..*).
// Build: clang++ -std=c++17 -Wall -Wextra -o train_journey train_journey.cpp

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

class Train;

// -------------------------------------------------------- TrainJourney
class TrainJourney {
public:
    int Train_No = 0;
    std::string Source_St;
    std::string Destination_St;
    float Journy_Time = 0.0f;
    Train* assignedTrain = nullptr;  // role assignedTrain, multiplicity 0..1

    TrainJourney(std::string src, std::string dst, float hours)
        : Source_St(std::move(src)), Destination_St(std::move(dst)), Journy_Time(hours) {}

    void Set_Source_St(const std::string& source) { Source_St = source; }
    void Set_Dastination_St(const std::string& destination) { Destination_St = destination; }
    // The figure passes Train_No to the getters, so they answer only for
    // the train this journey is assigned to.
    std::string Get_Source_St(int train_no) const {
        return train_no == Train_No ? Source_St : std::string("(not this train)");
    }
    float Get_Journy_Time(int train_no) const {
        return train_no == Train_No ? Journy_Time : -1.0f;
    }
};

// --------------------------------------------------------------- Train
class Train {
public:
    int Train_No;
    std::string Train_Type;
    float Max_Speed;
    std::vector<TrainJourney*> assignedJourny;  // role assignedJourny, multiplicity 0..*

    Train(int no, std::string type, float speed)
        : Train_No(no), Train_Type(std::move(type)), Max_Speed(speed) {}

    int Get_Train_No() const { return Train_No; }
    void Set_Train_Type(const std::string& trtype) { Train_Type = trtype; }
    float Get_Train_Speed(int train_no) const {
        return train_no == Train_No ? Max_Speed : -1.0f;
    }
};

// ---------------------------------------------- keeping both ends in step
// Both ends change in one place, so a journey can never point at a train
// that does not list it, and vice versa.
static void unassign(TrainJourney& j) {
    if (Train* t = j.assignedTrain) {
        t->assignedJourny.erase(
            std::remove(t->assignedJourny.begin(), t->assignedJourny.end(), &j),
            t->assignedJourny.end());
        j.assignedTrain = nullptr;
        j.Train_No = 0;
    }
}

static void assign(Train& t, TrainJourney& j) {
    unassign(j);  // a journey has at most one train (0..1)
    j.assignedTrain = &t;
    j.Train_No = t.Train_No;
    t.assignedJourny.push_back(&j);
}

// ---------------------------------------------------------------- main
static void printTrain(const Train& t) {
    std::cout << "Train " << t.Get_Train_No() << " (" << t.Train_Type << ", "
              << t.Max_Speed << " km/h) runs " << t.assignedJourny.size() << " journey(s)\n";
    for (const TrainJourney* j : t.assignedJourny)
        std::cout << "  " << j->Source_St << " -> " << j->Destination_St << ", "
                  << j->Journy_Time << " h, Train_No stored in journey = " << j->Train_No
                  << "\n";
}

int main() {
    Train rajdhani(12951, "Rajdhani", 130.0f);
    Train shatabdi(12009, "Shatabdi", 150.0f);

    TrainJourney j1("Mumbai", "Delhi", 15.5f);
    TrainJourney j2("Delhi", "Mumbai", 15.75f);
    TrainJourney j3("Mumbai", "Ahmedabad", 6.25f);

    assign(rajdhani, j1);
    assign(rajdhani, j2);
    assign(shatabdi, j3);

    std::cout << "--- after assignment ---\n";
    printTrain(rajdhani);
    printTrain(shatabdi);

    std::cout << "--- operations from the figure ---\n";
    j3.Set_Source_St("Mumbai Central");
    j3.Set_Dastination_St("Ahmedabad Jn");
    shatabdi.Set_Train_Type("Shatabdi Express");
    std::cout << "j3.Get_Source_St(12009) = " << j3.Get_Source_St(12009) << "\n";
    std::cout << "j3.Get_Source_St(12951) = " << j3.Get_Source_St(12951) << "\n";
    std::cout << "j3.Get_Journy_Time(12009) = " << j3.Get_Journy_Time(12009) << "\n";
    std::cout << "shatabdi.Get_Train_Speed(12009) = " << shatabdi.Get_Train_Speed(12009) << "\n";
    std::cout << "j1.assignedTrain->Train_Type = " << j1.assignedTrain->Train_Type << "\n";

    std::cout << "--- move j2 to the Shatabdi (0..1 keeps only one train) ---\n";
    assign(shatabdi, j2);
    printTrain(rajdhani);
    printTrain(shatabdi);

    std::cout << "--- unassign j3 ---\n";
    unassign(j3);
    std::cout << "j3.assignedTrain is " << (j3.assignedTrain ? "set" : "nullptr")
              << ", shatabdi lists " << shatabdi.assignedJourny.size() << " journey(s)\n";
    return 0;
}
train_journey.rsrust
// train_journey.rs -- MCSL-222 Session 9, Q21
// Figure 1.16 (Train Journey -- Train) in Rust 2021, standard library only, as a
// TWO-WAY association: TrainJourney.assignedTrain (0..1) and Train.assignedJourny (0..*).
// Ownership: Rc<RefCell<T>>; Train.assignedJourny holds Rc links and TrainJourney.assignedTrain holds a Weak back link, so the two-way link is not an Rc cycle.
// Build: rustc -O --edition 2021 train_journey.rs && ./train_journey
#![allow(non_snake_case)] // attribute and operation names are kept exactly as in the figure

use std::cell::RefCell;
use std::rc::{Rc, Weak};

type Ref<T> = Rc<RefCell<T>>;

fn new_ref<T>(x: T) -> Ref<T> {
    Rc::new(RefCell::new(x))
}

// -------------------------------------------------------- TrainJourney
struct TrainJourney {
    Train_No: i32,
    Source_St: String,
    Destination_St: String,
    Journy_Time: f32,
    assignedTrain: Option<Weak<RefCell<Train>>>, // role assignedTrain, multiplicity 0..1
}

impl TrainJourney {
    fn new(src: &str, dst: &str, hours: f32) -> Ref<TrainJourney> {
        new_ref(TrainJourney {
            Train_No: 0,
            Source_St: src.into(),
            Destination_St: dst.into(),
            Journy_Time: hours,
            assignedTrain: None,
        })
    }

    fn Set_Source_St(&mut self, source: &str) {
        self.Source_St = source.into();
    }
    fn Set_Dastination_St(&mut self, destination: &str) {
        self.Destination_St = destination.into();
    }
    // The figure passes Train_No to the getters, so they answer only for
    // the train this journey is assigned to.
    fn Get_Source_St(&self, train_no: i32) -> String {
        if train_no == self.Train_No { self.Source_St.clone() } else { "(not this train)".into() }
    }
    fn Get_Journy_Time(&self, train_no: i32) -> f32 {
        if train_no == self.Train_No { self.Journy_Time } else { -1.0 }
    }
    // Follows the Weak back link; None when unassigned.
    fn train(&self) -> Option<Ref<Train>> {
        self.assignedTrain.as_ref().and_then(Weak::upgrade)
    }
}

// --------------------------------------------------------------- Train
struct Train {
    Train_No: i32,
    Train_Type: String,
    Max_Speed: f32,
    assignedJourny: Vec<Ref<TrainJourney>>, // role assignedJourny, multiplicity 0..*
}

impl Train {
    fn new(no: i32, ttype: &str, speed: f32) -> Ref<Train> {
        new_ref(Train { Train_No: no, Train_Type: ttype.into(), Max_Speed: speed, assignedJourny: Vec::new() })
    }

    fn Get_Train_No(&self) -> i32 {
        self.Train_No
    }
    fn Set_Train_Type(&mut self, trtype: &str) {
        self.Train_Type = trtype.into();
    }
    fn Get_Train_Speed(&self, train_no: i32) -> f32 {
        if train_no == self.Train_No { self.Max_Speed } else { -1.0 }
    }
}

// ---------------------------------------------- keeping both ends in step
// Both ends change in one place, so a journey can never point at a train
// that does not list it, and vice versa.
fn unassign(j: &Ref<TrainJourney>) {
    let old = j.borrow().train();
    if let Some(t) = old {
        t.borrow_mut().assignedJourny.retain(|x| !Rc::ptr_eq(x, j));
        let mut jj = j.borrow_mut();
        jj.assignedTrain = None;
        jj.Train_No = 0;
    }
}

fn assign(t: &Ref<Train>, j: &Ref<TrainJourney>) {
    unassign(j); // a journey has at most one train (0..1)
    let mut jj = j.borrow_mut();
    jj.assignedTrain = Some(Rc::downgrade(t));
    jj.Train_No = t.borrow().Train_No;
    t.borrow_mut().assignedJourny.push(Rc::clone(j));
}

// ---------------------------------------------------------------- main
fn print_train(t: &Train) {
    println!(
        "Train {} ({}, {} km/h) runs {} journey(s)",
        t.Get_Train_No(),
        t.Train_Type,
        t.Max_Speed,
        t.assignedJourny.len()
    );
    for j in &t.assignedJourny {
        let j = j.borrow();
        println!(
            "  {} -> {}, {} h, Train_No stored in journey = {}",
            j.Source_St, j.Destination_St, j.Journy_Time, j.Train_No
        );
    }
}

fn main() {
    let rajdhani = Train::new(12951, "Rajdhani", 130.0);
    let shatabdi = Train::new(12009, "Shatabdi", 150.0);

    let j1 = TrainJourney::new("Mumbai", "Delhi", 15.5);
    let j2 = TrainJourney::new("Delhi", "Mumbai", 15.75);
    let j3 = TrainJourney::new("Mumbai", "Ahmedabad", 6.25);

    assign(&rajdhani, &j1);
    assign(&rajdhani, &j2);
    assign(&shatabdi, &j3);

    println!("--- after assignment ---");
    print_train(&rajdhani.borrow());
    print_train(&shatabdi.borrow());

    println!("--- operations from the figure ---");
    j3.borrow_mut().Set_Source_St("Mumbai Central");
    j3.borrow_mut().Set_Dastination_St("Ahmedabad Jn");
    shatabdi.borrow_mut().Set_Train_Type("Shatabdi Express");
    println!("j3.Get_Source_St(12009) = {}", j3.borrow().Get_Source_St(12009));
    println!("j3.Get_Source_St(12951) = {}", j3.borrow().Get_Source_St(12951));
    println!("j3.Get_Journy_Time(12009) = {}", j3.borrow().Get_Journy_Time(12009));
    println!("shatabdi.Get_Train_Speed(12009) = {}", shatabdi.borrow().Get_Train_Speed(12009));
    let t1 = j1.borrow().train().unwrap();
    println!("j1.assignedTrain->Train_Type = {}", t1.borrow().Train_Type);

    println!("--- move j2 to the Shatabdi (0..1 keeps only one train) ---");
    assign(&shatabdi, &j2);
    print_train(&rajdhani.borrow());
    print_train(&shatabdi.borrow());

    println!("--- unassign j3 ---");
    unassign(&j3);
    // "nullptr" is printed for an empty link so the output matches the C++ version.
    println!(
        "j3.assignedTrain is {}, shatabdi lists {} journey(s)",
        if j3.borrow().assignedTrain.is_some() { "set" } else { "nullptr" },
        shatabdi.borrow().assignedJourny.len()
    );
}
train_journey.pypython
# train_journey.py -- MCSL-222 Session 9, Q21
# Figure 1.16 (Train Journey -- Train) in Python 3, standard library only, as a
# TWO-WAY association: TrainJourney.assignedTrain (0..1) and Train.assignedJourny (0..*).
# Run: python3 train_journey.py
# eq=False keeps identity comparison, so list.remove() unlinks that exact journey
# and the two-way links cannot recurse through a field-by-field ==.
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional


def g(x: float) -> str:
    """Print a float the way C++ streams do by default: 130, 15.5, 6.25."""
    return f"{x:g}"


# -------------------------------------------------------- TrainJourney
@dataclass(eq=False)
class TrainJourney:
    Source_St: str
    Destination_St: str
    Journy_Time: float
    Train_No: int = 0
    assignedTrain: Optional[Train] = None  # role assignedTrain, multiplicity 0..1

    def Set_Source_St(self, source: str) -> None:
        self.Source_St = source

    def Set_Dastination_St(self, destination: str) -> None:
        self.Destination_St = destination

    # The figure passes Train_No to the getters, so they answer only for
    # the train this journey is assigned to.
    def Get_Source_St(self, train_no: int) -> str:
        return self.Source_St if train_no == self.Train_No else "(not this train)"

    def Get_Journy_Time(self, train_no: int) -> float:
        return self.Journy_Time if train_no == self.Train_No else -1.0


# --------------------------------------------------------------- Train
@dataclass(eq=False)
class Train:
    Train_No: int
    Train_Type: str
    Max_Speed: float
    assignedJourny: list[TrainJourney] = field(default_factory=list)  # role assignedJourny, 0..*

    def Get_Train_No(self) -> int:
        return self.Train_No

    def Set_Train_Type(self, trtype: str) -> None:
        self.Train_Type = trtype

    def Get_Train_Speed(self, train_no: int) -> float:
        return self.Max_Speed if train_no == self.Train_No else -1.0


# ---------------------------------------------- keeping both ends in step
# Both ends change in one place, so a journey can never point at a train
# that does not list it, and vice versa.
def unassign(j: TrainJourney) -> None:
    t = j.assignedTrain
    if t is not None:
        t.assignedJourny.remove(j)
        j.assignedTrain = None
        j.Train_No = 0


def assign(t: Train, j: TrainJourney) -> None:
    unassign(j)  # a journey has at most one train (0..1)
    j.assignedTrain = t
    j.Train_No = t.Train_No
    t.assignedJourny.append(j)


# ---------------------------------------------------------------- main
def printTrain(t: Train) -> None:
    print(f"Train {t.Get_Train_No()} ({t.Train_Type}, {g(t.Max_Speed)} km/h) "
          f"runs {len(t.assignedJourny)} journey(s)")
    for j in t.assignedJourny:
        print(f"  {j.Source_St} -> {j.Destination_St}, {g(j.Journy_Time)} h, "
              f"Train_No stored in journey = {j.Train_No}")


def main() -> None:
    rajdhani = Train(12951, "Rajdhani", 130.0)
    shatabdi = Train(12009, "Shatabdi", 150.0)

    j1 = TrainJourney("Mumbai", "Delhi", 15.5)
    j2 = TrainJourney("Delhi", "Mumbai", 15.75)
    j3 = TrainJourney("Mumbai", "Ahmedabad", 6.25)

    assign(rajdhani, j1)
    assign(rajdhani, j2)
    assign(shatabdi, j3)

    print("--- after assignment ---")
    printTrain(rajdhani)
    printTrain(shatabdi)

    print("--- operations from the figure ---")
    j3.Set_Source_St("Mumbai Central")
    j3.Set_Dastination_St("Ahmedabad Jn")
    shatabdi.Set_Train_Type("Shatabdi Express")
    print(f"j3.Get_Source_St(12009) = {j3.Get_Source_St(12009)}")
    print(f"j3.Get_Source_St(12951) = {j3.Get_Source_St(12951)}")
    print(f"j3.Get_Journy_Time(12009) = {g(j3.Get_Journy_Time(12009))}")
    print(f"shatabdi.Get_Train_Speed(12009) = {g(shatabdi.Get_Train_Speed(12009))}")
    print(f"j1.assignedTrain->Train_Type = {j1.assignedTrain.Train_Type}")

    print("--- move j2 to the Shatabdi (0..1 keeps only one train) ---")
    assign(shatabdi, j2)
    printTrain(rajdhani)
    printTrain(shatabdi)

    print("--- unassign j3 ---")
    unassign(j3)
    # "nullptr" is printed for an empty link so the output matches the C++ version.
    state = "set" if j3.assignedTrain else "nullptr"
    print(f"j3.assignedTrain is {state}, shatabdi lists {len(shatabdi.assignedJourny)} journey(s)")


if __name__ == "__main__":
    main()
train_journey.tsts
// train_journey.ts -- MCSL-222 Session 9, Q21
// Figure 1.16 (Train Journey -- Train) in TypeScript, no dependencies, as a
// TWO-WAY association: TrainJourney.assignedTrain (0..1) and Train.assignedJourny (0..*).
// Run: node train_journey.ts   (Node 22.18 or later strips the types itself)
"use strict";

// Both boxes in the figure carry a Train_No; the getters compare against it.
interface TrainNumbered {
  readonly Train_No: number;
}

function isTrain(x: TrainNumbered, train_no: number): boolean { return train_no === x.Train_No; }

// -------------------------------------------------------- TrainJourney
class TrainJourney implements TrainNumbered {
  Train_No: number = 0;
  Source_St: string;
  Destination_St: string;
  readonly Journy_Time: number;
  assignedTrain: Train | null = null; // role assignedTrain, multiplicity 0..1

  constructor(src: string, dst: string, hours: number) {
    this.Source_St = src;
    this.Destination_St = dst;
    this.Journy_Time = hours;
  }

  Set_Source_St(source: string): void { this.Source_St = source; }
  Set_Dastination_St(destination: string): void { this.Destination_St = destination; }
  // The figure passes Train_No to the getters, so they answer only for
  // the train this journey is assigned to.
  Get_Source_St(train_no: number): string { return isTrain(this, train_no) ? this.Source_St : "(not this train)"; }
  Get_Journy_Time(train_no: number): number { return isTrain(this, train_no) ? this.Journy_Time : -1; }
}

// --------------------------------------------------------------- Train
class Train implements TrainNumbered {
  readonly Train_No: number;
  Train_Type: string;
  readonly Max_Speed: number;
  readonly assignedJourny: TrainJourney[] = []; // role assignedJourny, multiplicity 0..*

  constructor(no: number, type: string, speed: number) {
    this.Train_No = no;
    this.Train_Type = type;
    this.Max_Speed = speed;
  }

  Get_Train_No(): number { return this.Train_No; }
  Set_Train_Type(trtype: string): void { this.Train_Type = trtype; }
  Get_Train_Speed(train_no: number): number { return isTrain(this, train_no) ? this.Max_Speed : -1; }
}

// ---------------------------------------------- keeping both ends in step
// Both ends change in one place, so a journey can never point at a train
// that does not list it, and vice versa.
function unassign(j: TrainJourney): void {
  const t = j.assignedTrain;
  if (t !== null) {
    t.assignedJourny.splice(t.assignedJourny.indexOf(j), 1);
    j.assignedTrain = null;
    j.Train_No = 0;
  }
}

function assign(t: Train, j: TrainJourney): void {
  unassign(j); // a journey has at most one train (0..1)
  j.assignedTrain = t;
  j.Train_No = t.Train_No;
  t.assignedJourny.push(j);
}

// ---------------------------------------------------------------- main
function printTrain(t: Train): void {
  console.log(`Train ${t.Get_Train_No()} (${t.Train_Type}, ${t.Max_Speed} km/h) runs ${t.assignedJourny.length} journey(s)`);
  for (const j of t.assignedJourny) {
    console.log(`  ${j.Source_St} -> ${j.Destination_St}, ${j.Journy_Time} h, Train_No stored in journey = ${j.Train_No}`);
  }
}

function main(): void {
  const rajdhani = new Train(12951, "Rajdhani", 130.0);
  const shatabdi = new Train(12009, "Shatabdi", 150.0);

  const j1 = new TrainJourney("Mumbai", "Delhi", 15.5);
  const j2 = new TrainJourney("Delhi", "Mumbai", 15.75);
  const j3 = new TrainJourney("Mumbai", "Ahmedabad", 6.25);

  assign(rajdhani, j1);
  assign(rajdhani, j2);
  assign(shatabdi, j3);

  console.log("--- after assignment ---");
  printTrain(rajdhani);
  printTrain(shatabdi);

  console.log("--- operations from the figure ---");
  j3.Set_Source_St("Mumbai Central");
  j3.Set_Dastination_St("Ahmedabad Jn");
  shatabdi.Set_Train_Type("Shatabdi Express");
  console.log(`j3.Get_Source_St(12009) = ${j3.Get_Source_St(12009)}`);
  console.log(`j3.Get_Source_St(12951) = ${j3.Get_Source_St(12951)}`);
  console.log(`j3.Get_Journy_Time(12009) = ${j3.Get_Journy_Time(12009)}`);
  console.log(`shatabdi.Get_Train_Speed(12009) = ${shatabdi.Get_Train_Speed(12009)}`);
  // assignedTrain is `Train | null`; `!` tells the checker j1 is assigned here.
  console.log(`j1.assignedTrain->Train_Type = ${j1.assignedTrain!.Train_Type}`);

  console.log("--- move j2 to the Shatabdi (0..1 keeps only one train) ---");
  assign(shatabdi, j2);
  printTrain(rajdhani);
  printTrain(shatabdi);

  console.log("--- unassign j3 ---");
  unassign(j3);
  // "nullptr" is printed for an empty link so the output matches the C++ version.
  console.log(`j3.assignedTrain is ${j3.assignedTrain ? "set" : "nullptr"}, shatabdi lists ${shatabdi.assignedJourny.length} journey(s)`);
}

main();

Output

Compiled with zero warnings and run here; this is the real output. All four implementations print exactly this; the outputs were diffed and are identical byte for byte.

--- after assignment ---
Train 12951 (Rajdhani, 130 km/h) runs 2 journey(s)
  Mumbai -> Delhi, 15.5 h, Train_No stored in journey = 12951
  Delhi -> Mumbai, 15.75 h, Train_No stored in journey = 12951
Train 12009 (Shatabdi, 150 km/h) runs 1 journey(s)
  Mumbai -> Ahmedabad, 6.25 h, Train_No stored in journey = 12009
--- operations from the figure ---
j3.Get_Source_St(12009) = Mumbai Central
j3.Get_Source_St(12951) = (not this train)
j3.Get_Journy_Time(12009) = 6.25
shatabdi.Get_Train_Speed(12009) = 150
j1.assignedTrain->Train_Type = Rajdhani
--- move j2 to the Shatabdi (0..1 keeps only one train) ---
Train 12951 (Rajdhani, 130 km/h) runs 1 journey(s)
  Mumbai -> Delhi, 15.5 h, Train_No stored in journey = 12951
Train 12009 (Shatabdi Express, 150 km/h) runs 2 journey(s)
  Mumbai Central -> Ahmedabad Jn, 6.25 h, Train_No stored in journey = 12009
  Delhi -> Mumbai, 15.75 h, Train_No stored in journey = 12009
--- unassign j3 ---
j3.assignedTrain is nullptr, shatabdi lists 1 journey(s)

Explanation

Diagram elementWhere it is in the code
Role assignedTrain, 0..1Train* assignedTrain = nullptr in TrainJourney. A pointer that may be null is exactly 0..1.
Role assignedJourny, 0..*std::vector<TrainJourney*> assignedJourny in Train.
Two-way navigabilityBoth members exist. j1.assignedTrain->Train_Type walks journey to train; printTrain walks train to journeys.
Consistencyassign sets the pointer, copies Train_No and pushes into the vector in one place. unassign erases from the vector and clears the pointer. Nothing else writes the two ends.
0..1 upper boundassign starts with unassign(j), so moving j2 to the Shatabdi removes it from the Rajdhani, as the output after the move shows: Rajdhani 1 journey, Shatabdi 2.
Getters with a Train_No parameterGet_Source_St(12951) on a Shatabdi journey returns (not this train); with the matching number it returns the station.
SettersSet_Source_St, Set_Dastination_St and Set_Train_Type assign the string; the change shows in the second print of the Shatabdi.

Diagram element to code, where the four languages differ:

ElementC++RustPythonTypeScript
assignedTrain, 0..1Train* assignedTrain = nullptrOption<Weak<RefCell<Train>>>; Weak because the train already holds an Rc to the journey, and a train() helper upgrades itOptional[Train] = Noneunion field assignedTrain: Train or null, starting as null; main writes j1.assignedTrain! where it knows the link is set
assignedJourny, 0..*std::vector<TrainJourney*>Vec<Rc<RefCell<TrainJourney>>>list[TrainJourney]typed array readonly assignedJourny: TrainJourney[] = []
assign and unassignfree functions writing through pointersfree functions taking &Rc<..> and using borrow_mut() on both endsfree functionstyped free functions, assign(t: Train, j: TrainJourney): void
Train_No drawn in both boxesplain member in each classplain field in each structplain field in each classinterface TrainNumbered implemented by both classes; the isTrain helper the getters call accepts either
Removing the journey in unassignstd::remove then eraseretain with Rc::ptr_eqlist.remove (identity, eq=False)splice(indexOf(j), 1)
Printing Max_Speed and Journy_Time as 130 and 15.5default stream format{} prints 130 and 15.5print(130.0) would give 130.0, so a one-line helper g(x) returns f"{x:g}"template literal prints 130

Question 22

Problem Statement

Write in lab record

Implement the following Associations using C++/Java.

Person and Bank Account association

Figure 1.17: Person and Bank Account

Solution

Write in lab record

Assumptions

A bank keeps, for each customer, the accounts that customer holds. A person has an ID and a name. A bank account has an account number and a balance, and supports a credit that increases the balance and a withdrawal that decreases it. One person can hold any number of accounts and every account belongs to exactly one person. The figure gives Person an operation addAccount(BankAccount a) and gives BankAccount no operation or attribute that refers to a person, so the link is navigable from person to account only. From a person you can reach and total all their accounts; from an account you cannot ask who owns it. The program must create a person and two accounts, attach the accounts, credit and withdraw amounts, refuse a withdrawal that exceeds the balance and a credit that is not positive, and print the accounts with a total.

  • One-way association: Person::accounts is a std::vector of pointers (*). BankAccount has no member that refers to Person.
  • Credit rejects an amount of zero or less. Withdraw rejects an amount of zero or less and any amount above the balance; the account never goes negative.
  • The 1 at the Person end (every account has exactly one owner) cannot be checked from the account side in a one-way design. The program trusts main to add each account to one person only. Making the link two-way is the fix if that check is required; Q21 shows how.
  • totalBalance is an extra helper for printing; the figure’s attributes and operations are otherwise unchanged.

Diagram elements

ClassAttributesOperations
PersonPerson_ID: String, Name: StringaddAccount(BankAccount a)
BankAccountAcc_No: String, Acc_Balance: doubleCredit(double amount), Withdraw(double amount)

Association: Person 1 has BankAccount *, navigable from Person only.

Pointers after the two addAccount calls in main:

 asha (Person P001)
 accounts: [ savings, current ]
     |         |
     v         v
  savings    current          (no arrow back to asha:
  SB-1001    CA-2001           BankAccount has no Person member)

Steps

  1. Save the listing below as person_account.cpp in the session-9 folder.
  2. Compile: clang++ -std=c++17 -Wall -Wextra -o person_account person_account.cpp.
  3. Run ./person_account and paste the output.
  4. For another language, save the matching tab and run it: rustc -O --edition 2021 person_account.rs && ./person_account, python3 person_account.py, or node person_account.ts.

Program

Lab record: write one language only. Pick yours once and every page opens on it; the other tabs are the same solution for comparison.

person_account.cppcpp
// person_account.cpp -- MCSL-222 Session 9, Q22
// Figure 1.17 (Person 1 has * BankAccount) implemented in C++17 as a ONE-WAY
// association: Person knows its accounts, BankAccount knows nothing of Person.
// Build: clang++ -std=c++17 -Wall -Wextra -o person_account person_account.cpp

#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

// --------------------------------------------------------- BankAccount
class BankAccount {
public:
    std::string Acc_No;
    double Acc_Balance;

    BankAccount(std::string no, double opening) : Acc_No(std::move(no)), Acc_Balance(opening) {}

    void Credit(double amount) {
        if (amount <= 0) {
            std::cout << "  refused: credit amount must be positive\n";
            return;
        }
        Acc_Balance += amount;
    }
    void Withdraw(double amount) {
        if (amount <= 0 || amount > Acc_Balance) {
            std::cout << "  refused: cannot withdraw " << amount << " from " << Acc_No
                      << " (balance " << Acc_Balance << ")\n";
            return;
        }
        Acc_Balance -= amount;
    }
};

// -------------------------------------------------------------- Person
class Person {
public:
    std::string Person_ID;
    std::string Name;
    std::vector<BankAccount*> accounts;  // has: Person (1) --> (*) BankAccount

    Person(std::string id, std::string name) : Person_ID(std::move(id)), Name(std::move(name)) {}

    void addAccount(BankAccount& a) { accounts.push_back(&a); }

    double totalBalance() const {
        double sum = 0;
        for (const BankAccount* a : accounts) sum += a->Acc_Balance;
        return sum;
    }
};

// ---------------------------------------------------------------- main
static void printPerson(const Person& p) {
    std::cout << p.Name << " (" << p.Person_ID << ") holds " << p.accounts.size()
              << " account(s)\n";
    for (const BankAccount* a : p.accounts)
        std::cout << "  " << a->Acc_No << "  balance " << std::fixed << std::setprecision(2)
                  << a->Acc_Balance << "\n";
    std::cout << "  total " << std::fixed << std::setprecision(2) << p.totalBalance() << "\n";
}

int main() {
    Person asha("P001", "Asha");
    BankAccount savings("SB-1001", 5000.00);
    BankAccount current("CA-2001", 12000.00);

    asha.addAccount(savings);
    asha.addAccount(current);

    std::cout << "--- after addAccount ---\n";
    printPerson(asha);

    std::cout << "--- Credit and Withdraw ---\n";
    savings.Credit(1500.00);
    current.Withdraw(2000.00);
    savings.Withdraw(9000.00);  // refused: more than balance
    current.Credit(-50.00);     // refused: not positive
    printPerson(asha);

    // One-way navigation: from an account there is no way back to Asha.
    // The line below would not compile, which is the point of the figure:
    // std::cout << savings.owner->Name;
    return 0;
}
person_account.rsrust
// person_account.rs -- MCSL-222 Session 9, Q22
// Figure 1.17 (Person 1 has * BankAccount) in Rust 2021, standard library only, as a
// ONE-WAY association: Person knows its accounts, BankAccount knows nothing of Person.
// Ownership: BankAccount is an Rc<RefCell<BankAccount>> shared by main and Person.accounts; Person is a plain struct because nothing links back to it.
// Build: rustc -O --edition 2021 person_account.rs && ./person_account
#![allow(non_snake_case)] // attribute and operation names are kept exactly as in the figure

use std::cell::RefCell;
use std::rc::Rc;

type Ref<T> = Rc<RefCell<T>>;

// --------------------------------------------------------- BankAccount
struct BankAccount {
    Acc_No: String,
    Acc_Balance: f64,
}

impl BankAccount {
    fn new(no: &str, opening: f64) -> Ref<BankAccount> {
        Rc::new(RefCell::new(BankAccount { Acc_No: no.into(), Acc_Balance: opening }))
    }

    fn Credit(&mut self, amount: f64) {
        if amount <= 0.0 {
            println!("  refused: credit amount must be positive");
            return;
        }
        self.Acc_Balance += amount;
    }
    fn Withdraw(&mut self, amount: f64) {
        if amount <= 0.0 || amount > self.Acc_Balance {
            println!(
                "  refused: cannot withdraw {:.2} from {} (balance {:.2})",
                amount, self.Acc_No, self.Acc_Balance
            );
            return;
        }
        self.Acc_Balance -= amount;
    }
}

// -------------------------------------------------------------- Person
struct Person {
    Person_ID: String,
    Name: String,
    accounts: Vec<Ref<BankAccount>>, // has: Person (1) --> (*) BankAccount
}

impl Person {
    fn new(id: &str, name: &str) -> Person {
        Person { Person_ID: id.into(), Name: name.into(), accounts: Vec::new() }
    }

    fn addAccount(&mut self, a: &Ref<BankAccount>) {
        self.accounts.push(Rc::clone(a));
    }

    fn totalBalance(&self) -> f64 {
        self.accounts.iter().map(|a| a.borrow().Acc_Balance).sum()
    }
}

// ---------------------------------------------------------------- main
fn print_person(p: &Person) {
    println!("{} ({}) holds {} account(s)", p.Name, p.Person_ID, p.accounts.len());
    for a in &p.accounts {
        let a = a.borrow();
        println!("  {}  balance {:.2}", a.Acc_No, a.Acc_Balance);
    }
    println!("  total {:.2}", p.totalBalance());
}

fn main() {
    let mut asha = Person::new("P001", "Asha");
    let savings = BankAccount::new("SB-1001", 5000.00);
    let current = BankAccount::new("CA-2001", 12000.00);

    asha.addAccount(&savings);
    asha.addAccount(&current);

    println!("--- after addAccount ---");
    print_person(&asha);

    println!("--- Credit and Withdraw ---");
    savings.borrow_mut().Credit(1500.00);
    current.borrow_mut().Withdraw(2000.00);
    savings.borrow_mut().Withdraw(9000.00); // refused: more than balance
    current.borrow_mut().Credit(-50.00); // refused: not positive
    print_person(&asha);

    // One-way navigation: from an account there is no way back to Asha.
    // The line below would not compile, which is the point of the figure:
    // println!("{}", savings.borrow().owner.Name);
}
person_account.pypython
# person_account.py -- MCSL-222 Session 9, Q22
# Figure 1.17 (Person 1 has * BankAccount) in Python 3, standard library only, as a
# ONE-WAY association: Person knows its accounts, BankAccount knows nothing of Person.
# Run: python3 person_account.py
from __future__ import annotations

from dataclasses import dataclass, field


# --------------------------------------------------------- BankAccount
@dataclass(eq=False)
class BankAccount:
    Acc_No: str
    Acc_Balance: float

    def Credit(self, amount: float) -> None:
        if amount <= 0:
            print("  refused: credit amount must be positive")
            return
        self.Acc_Balance += amount

    def Withdraw(self, amount: float) -> None:
        if amount <= 0 or amount > self.Acc_Balance:
            print(f"  refused: cannot withdraw {amount:.2f} from {self.Acc_No} "
                  f"(balance {self.Acc_Balance:.2f})")
            return
        self.Acc_Balance -= amount


# -------------------------------------------------------------- Person
@dataclass(eq=False)
class Person:
    Person_ID: str
    Name: str
    accounts: list[BankAccount] = field(default_factory=list)  # has: Person (1) --> (*) BankAccount

    def addAccount(self, a: BankAccount) -> None:
        self.accounts.append(a)

    def totalBalance(self) -> float:
        return sum(a.Acc_Balance for a in self.accounts)


# ---------------------------------------------------------------- main
def printPerson(p: Person) -> None:
    print(f"{p.Name} ({p.Person_ID}) holds {len(p.accounts)} account(s)")
    for a in p.accounts:
        print(f"  {a.Acc_No}  balance {a.Acc_Balance:.2f}")
    print(f"  total {p.totalBalance():.2f}")


def main() -> None:
    asha = Person("P001", "Asha")
    savings = BankAccount("SB-1001", 5000.00)
    current = BankAccount("CA-2001", 12000.00)

    asha.addAccount(savings)
    asha.addAccount(current)

    print("--- after addAccount ---")
    printPerson(asha)

    print("--- Credit and Withdraw ---")
    savings.Credit(1500.00)
    current.Withdraw(2000.00)
    savings.Withdraw(9000.00)  # refused: more than balance
    current.Credit(-50.00)     # refused: not positive
    printPerson(asha)

    # One-way navigation: from an account there is no way back to Asha.
    # The line below would raise AttributeError, which is the point of the figure:
    # print(savings.owner.Name)


if __name__ == "__main__":
    main()
person_account.tsts
// person_account.ts -- MCSL-222 Session 9, Q22
// Figure 1.17 (Person 1 has * BankAccount) in TypeScript, no dependencies, as a
// ONE-WAY association: Person knows its accounts, BankAccount knows nothing of Person.
// Run: node person_account.ts   (Node 22.18 or later strips the types itself)
"use strict";

// --------------------------------------------------------- BankAccount
class BankAccount {
  readonly Acc_No: string;
  Acc_Balance: number;

  constructor(no: string, opening: number) {
    this.Acc_No = no;
    this.Acc_Balance = opening;
  }

  Credit(amount: number): void {
    if (amount <= 0) {
      console.log("  refused: credit amount must be positive");
      return;
    }
    this.Acc_Balance += amount;
  }
  Withdraw(amount: number): void {
    if (amount <= 0 || amount > this.Acc_Balance) {
      console.log(`  refused: cannot withdraw ${amount.toFixed(2)} from ${this.Acc_No} (balance ${this.Acc_Balance.toFixed(2)})`);
      return;
    }
    this.Acc_Balance -= amount;
  }
}

// -------------------------------------------------------------- Person
class Person {
  readonly Person_ID: string;
  readonly Name: string;
  readonly accounts: BankAccount[] = []; // has: Person (1) --> (*) BankAccount

  constructor(id: string, name: string) {
    this.Person_ID = id;
    this.Name = name;
  }

  addAccount(a: BankAccount): void { this.accounts.push(a); }

  totalBalance(): number { return this.accounts.reduce((sum, a) => sum + a.Acc_Balance, 0); }
}

// ---------------------------------------------------------------- main
function printPerson(p: Person): void {
  console.log(`${p.Name} (${p.Person_ID}) holds ${p.accounts.length} account(s)`);
  for (const a of p.accounts) console.log(`  ${a.Acc_No}  balance ${a.Acc_Balance.toFixed(2)}`);
  console.log(`  total ${p.totalBalance().toFixed(2)}`);
}

function main(): void {
  const asha = new Person("P001", "Asha");
  const savings = new BankAccount("SB-1001", 5000.00);
  const current = new BankAccount("CA-2001", 12000.00);

  asha.addAccount(savings);
  asha.addAccount(current);

  console.log("--- after addAccount ---");
  printPerson(asha);

  console.log("--- Credit and Withdraw ---");
  savings.Credit(1500.00);
  current.Withdraw(2000.00);
  savings.Withdraw(9000.00); // refused: more than balance
  current.Credit(-50.00);    // refused: not positive
  printPerson(asha);

  // One-way navigation: from an account there is no way back to Asha.
  // `tsc` rejects the line below (`owner` is not a property of BankAccount);
  // `node` alone strips the types without checking, so it would throw at run
  // time instead. Either way, that is the point of the figure:
  // console.log(savings.owner.Name);
}

main();

Output

Compiled with zero warnings and run here; this is the real output. All four implementations print exactly this; the outputs were diffed and are identical byte for byte.

--- after addAccount ---
Asha (P001) holds 2 account(s)
  SB-1001  balance 5000.00
  CA-2001  balance 12000.00
  total 17000.00
--- Credit and Withdraw ---
  refused: cannot withdraw 9000.00 from SB-1001 (balance 6500.00)
  refused: credit amount must be positive
Asha (P001) holds 2 account(s)
  SB-1001  balance 6500.00
  CA-2001  balance 10000.00
  total 16500.00

Check by hand: 5000 + 1500 = 6500 on the savings account, 12000 - 2000 = 10000 on the current account, total 16500.

Explanation

Diagram elementWhere it is in the code
Person 1 has * BankAccountstd::vector<BankAccount*> accounts in Person; addAccount(BankAccount& a) pushes a pointer.
One-way navigabilityBankAccount has no Person* member. The commented line at the end of main, savings.owner->Name, would not compile, which is the point.
Credit(double amount)Adds to Acc_Balance after the positive check.
Withdraw(double amount)Subtracts after checking amount is positive and not above Acc_Balance; the refusal for 9000 on a balance of 6500 is the first refused line of the output.
PrintingprintPerson walks the vector from the Person side, which is the only direction available.

Diagram element to code, where the four languages differ:

ElementC++RustPythonTypeScript
Person has * BankAccountstd::vector<BankAccount*>Vec<Rc<RefCell<BankAccount>>>; Person itself is a plain struct because nothing links back to itlist[BankAccount]typed array readonly accounts: BankAccount[] = []
Credit and Withdraw changing the balancemember function on the objectsavings.borrow_mut().Credit(1500.0): the account is shared with Person, so mutation goes through the RefCellmethodtyped method Credit(amount: number): void; Acc_Balance is the one field without readonly
What the commented savings.owner line would docompile errorcompile errorAttributeError at run timetsc error, owner is not a property of BankAccount; a bare node run strips types without checking and throws TypeError at run time
totalBalanceloopiter().map(..).sum()sum(generator)reduce
Money formatstd::fixed, setprecision(2){:.2}f"{x:.2f}"toFixed(2)

Viva Questions

Do not copy. Read for understanding and the viva

Q: What decides whether an association is one-way or two-way in code? A: Navigability. An arrowhead, or an operation on one side only, means only that class stores the link. Role names on both ends with no arrowhead, as in figure 1.16, mean both classes store it.

Q: Why does the train program change both ends inside assign and never in main? A: A two-way link is two members that must agree. If any code can set one without the other, they drift apart. One function that always updates both is the only way to guarantee consistency.

Q: What does 0..1 become in C++? A: A pointer that may be nullptr. 1 is a pointer that must not be null; * and 0..* are a std::vector.

Q: What is lost with one-way navigation in figure 1.17? A: The 1 at Person cannot be enforced or even checked from the account, and you cannot find an account’s owner without scanning every person.

Q: What would change to make Person and BankAccount two-way? A: Add Person* owner to BankAccount, set it in addAccount, and refuse addAccount when owner is already set.

Q: Why keep the misspelt names such as Journy_Time? A: The lab record is checked against the figure. Matching names show the mapping is exact; fixing spellings is a separate remark, not a silent change.

Q: Why does assign call unassign first? A: The 0..1 on the train end means a journey has at most one train. Removing the old link before adding the new one keeps that bound.

Common Mistakes

Do not copy. Read for understanding and the viva
  • Setting assignedTrain on the journey and forgetting to push into assignedJourny, or the reverse; the printed lists then disagree with the pointers.
  • Erasing from a vector inside a range-for loop over the same vector. Use the erase-remove idiom on a copy of the pointer, as unassign does.
  • Adding an owner pointer to BankAccount when the figure shows a one-way link, then claiming it matches the figure.
  • Letting Withdraw drive the balance negative because the check compares the wrong way round.
  • Comparing floats printed with different precision and thinking the values changed; set the precision once.

Session Summary

Write in lab record
  • Question 21: train_journey.cpp, .rs, .py and .ts, two-way TrainJourney to Train association with assign and unassign keeping both ends in step, run output attached (identical in all four languages)
  • Question 22: person_account.cpp, .rs, .py and .ts, one-way Person to BankAccount association with guarded Credit and Withdraw, run output attached (identical in all four languages)
  • Problem description and assumptions for both figures, plus a diagram-element table for each
Navigation

Type to search…

↑↓ navigate↵ selectEsc close