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| Question | Requirement | Status |
|---|---|---|
| Q21 | Implement the following Associations using C++/Java | Complete |
| Q22 | Implement the following Associations using C++/Java | Complete |
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 recordImplement the following Associations using C++/Java.
Figure 1.16: Train-Journey Association
Solution
Write in lab recordAssumptions
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::assignedTrainis a pointer (0..1) andTrain::assignedJournyis astd::vectorof pointers (0..*). - Both ends change only inside two free functions,
assignandunassign.mainnever touches the pointers or the vector directly. assignfirst callsunassign, so a journey can never appear under two trains.- The figure gives
Train_Noas an attribute of TrainJourney and also as a parameter of the getters.assigncopies 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_TimeandSet_Dastination_St.
Diagram elements
| Class | Attributes | Operations |
|---|---|---|
| TrainJourney | Train_No: int, Source_St: String, Destination_St: String, Journy_Time: float | Set_Source_St(source: String), Set_Dastination_St(destination: String), Get_Source_St(Train_No: int): String, Get_Journy_Time(Train_No: int): float |
| Train | Train_No: int, Train_Type: String, Max_Speed: float | Get_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 ---> rajdhaniEvery 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
- Save the listing below as
train_journey.cppin asession-9folder. - Compile:
clang++ -std=c++17 -Wall -Wextra -o train_journey train_journey.cpp. - Run
./train_journeyand paste the output. - For another language, save the matching tab and run it:
rustc -O --edition 2021 train_journey.rs && ./train_journey,python3 train_journey.py, ornode 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.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.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.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.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 element | Where it is in the code |
|---|---|
Role assignedTrain, 0..1 | Train* 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 navigability | Both members exist. j1.assignedTrain->Train_Type walks journey to train; printTrain walks train to journeys. |
| Consistency | assign 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 bound | assign 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 parameter | Get_Source_St(12951) on a Shatabdi journey returns (not this train); with the matching number it returns the station. |
| Setters | Set_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:
| Element | C++ | Rust | Python | TypeScript |
|---|---|---|---|---|
assignedTrain, 0..1 | Train* assignedTrain = nullptr | Option<Weak<RefCell<Train>>>; Weak because the train already holds an Rc to the journey, and a train() helper upgrades it | Optional[Train] = None | union 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 unassign | free functions writing through pointers | free functions taking &Rc<..> and using borrow_mut() on both ends | free functions | typed free functions, assign(t: Train, j: TrainJourney): void |
Train_No drawn in both boxes | plain member in each class | plain field in each struct | plain field in each class | interface TrainNumbered implemented by both classes; the isTrain helper the getters call accepts either |
Removing the journey in unassign | std::remove then erase | retain with Rc::ptr_eq | list.remove (identity, eq=False) | splice(indexOf(j), 1) |
Printing Max_Speed and Journy_Time as 130 and 15.5 | default stream format | {} prints 130 and 15.5 | print(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 recordImplement the following Associations using C++/Java.
Figure 1.17: Person and Bank Account
Solution
Write in lab recordAssumptions
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::accountsis astd::vectorof pointers (*).BankAccounthas no member that refers toPerson. Creditrejects an amount of zero or less.Withdrawrejects an amount of zero or less and any amount above the balance; the account never goes negative.- The
1at the Person end (every account has exactly one owner) cannot be checked from the account side in a one-way design. The program trustsmainto add each account to one person only. Making the link two-way is the fix if that check is required; Q21 shows how. totalBalanceis an extra helper for printing; the figure’s attributes and operations are otherwise unchanged.
Diagram elements
| Class | Attributes | Operations |
|---|---|---|
| Person | Person_ID: String, Name: String | addAccount(BankAccount a) |
| BankAccount | Acc_No: String, Acc_Balance: double | Credit(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
- Save the listing below as
person_account.cppin thesession-9folder. - Compile:
clang++ -std=c++17 -Wall -Wextra -o person_account person_account.cpp. - Run
./person_accountand paste the output. - For another language, save the matching tab and run it:
rustc -O --edition 2021 person_account.rs && ./person_account,python3 person_account.py, ornode 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.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.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(¤t);
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.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.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.00Check by hand: 5000 + 1500 = 6500 on the savings account, 12000 - 2000 = 10000 on the current account, total 16500.
Explanation
| Diagram element | Where it is in the code |
|---|---|
Person 1 has * BankAccount | std::vector<BankAccount*> accounts in Person; addAccount(BankAccount& a) pushes a pointer. |
| One-way navigability | BankAccount 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. |
| Printing | printPerson walks the vector from the Person side, which is the only direction available. |
Diagram element to code, where the four languages differ:
| Element | C++ | Rust | Python | TypeScript |
|---|---|---|---|---|
Person has * BankAccount | std::vector<BankAccount*> | Vec<Rc<RefCell<BankAccount>>>; Person itself is a plain struct because nothing links back to it | list[BankAccount] | typed array readonly accounts: BankAccount[] = [] |
| Credit and Withdraw changing the balance | member function on the object | savings.borrow_mut().Credit(1500.0): the account is shared with Person, so mutation goes through the RefCell | method | typed method Credit(amount: number): void; Acc_Balance is the one field without readonly |
What the commented savings.owner line would do | compile error | compile error | AttributeError at run time | tsc error, owner is not a property of BankAccount; a bare node run strips types without checking and throws TypeError at run time |
totalBalance | loop | iter().map(..).sum() | sum(generator) | reduce |
| Money format | std::fixed, setprecision(2) | {:.2} | f"{x:.2f}" | toFixed(2) |
Viva Questions
Do not copy. Read for understanding and the vivaQ: 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
assignedTrainon the journey and forgetting to push intoassignedJourny, 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
unassigndoes. - Adding an owner pointer to
BankAccountwhen the figure shows a one-way link, then claiming it matches the figure. - Letting
Withdrawdrive 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,.pyand.ts, two-way TrainJourney to Train association withassignandunassignkeeping both ends in step, run output attached (identical in all four languages) - Question 22:
person_account.cpp,.rs,.pyand.ts, one-way Person to BankAccount association with guardedCreditandWithdraw, run output attached (identical in all four languages) - Problem description and assumptions for both figures, plus a diagram-element table for each