Classes Working Together
A single class packages one concept. Real programs package many. This page builds three classes that work together: one from Concept #6 (slightly extended), one that inherits from it, and one that uses both — each exposing a different OOP relationship.
Composition is when a class holds an instance of another class as an instance variable. Quadratic has a vertex field of type OrderedPair. The quadratic does not become an ordered pair; it contains one. Composition models “has-a” relationships.
Inheritance is when a class extends another class, acquiring all of its fields and methods automatically. ComplexOrderedPair extends OrderedPair: every getter, every constructor path, every utility method of OrderedPair is available in ComplexOrderedPair for free. The child then overrides toString() to display a + bi instead of (a, b), and adds two new methods — conjugate() and modulus() — that have no meaning in the parent but are essential for complex numbers. Inheritance models “is-a” relationships.
When Quadratic calculates roots and the discriminant is negative, it creates ComplexOrderedPair objects to represent the complex roots. That single line connects all three classes: the inheritance hierarchy provides the complex number type; the composition provides the host object that requests it.
The PCNICOTGSU mnemonic is the teacher’s shorthand for the ten anatomy elements every well-formed class should have. It is embedded as a comment in the Java and JavaScript versions so you can see each element in place. The Quadratic class is designed to be a reference example: every letter of the mnemonic has at least one corresponding line of code.
There’s more to this story. The Java version of Quadratic was later upgraded to store roots as actual OrderedPair and ComplexOrderedPair objects, add a shared DecimalFormat, and fix double comparisons with EPSILON — a look at how a teaching example grows into production-quality code. See the
Java Upgrade Showcase
and the
Teacher’s Analysis.
Inheritance chain & composition map
PCNICOTGSU — 10 anatomy elements
Every code panel below is annotated with these labels. Find each letter in the Quadratic class source.
Calling the Classes — Driver Programs
These files exercise all three constructors, getRootsDescription() across all three discriminant cases, and setter side effects. Run them after loading the four class files.
// Paste OrderedPair.js, ComplexOrderedPair.js,
// Quadratic.js, then this file — DevTools Console.
console.log("=== Quadratic Runner ===\n");
console.log("--- Default: f(x) = x\u00B2 ---");
var q1 = new Quadratic();
console.log(q1.toString());
console.log("f(3) = " + q1.f(3));
console.log("Roots: " + q1.getRootsDescription());
console.log("\n--- Two real roots: x\u00B2 - 5x + 6 ---");
var q2 = new Quadratic(1, -5, 6);
console.log(q2.toString());
console.log("Roots: " + q2.getRootsDescription());
console.log("\n--- One repeated root: x\u00B2 - 6x + 9 ---");
var q3 = new Quadratic(1, -6, 9);
console.log(q3.toString());
console.log("Roots: " + q3.getRootsDescription());
console.log("\n--- Complex roots: x\u00B2 + 2x + 5 ---");
var q4 = new Quadratic(1, 2, 5);
console.log(q4.toString());
console.log("Roots: " + q4.getRootsDescription());
console.log("\n--- Setter side effects ---");
var q5 = new Quadratic(1, 0, -4);
console.log("Before: " + q5.toString());
q5.setB(-4);
console.log("After setB(-4): " + q5.toString());
console.log("\n--- Copy constructor ---");
var q6 = new Quadratic(q2);
console.log("Copy of q2: " + q6.toString());
console.log("\n--- Thanks for using our program! ---");
// JDoodle multi-file (Java): 4 files required —
// File 1: OrderedPair.java
// File 2: ComplexOrderedPair.java
// File 3: Quadratic.java
// File 4: QuadraticDriver.java ← set as main file
public class QuadraticDriver {
public static void main(String[] args){
System.out.println("=== Quadratic Driver ===\n");
System.out.println("--- Default: f(x) = x\u00B2 ---");
Quadratic q1 = new Quadratic();
System.out.println(q1);
System.out.println("f(3) = " + q1.f(3));
System.out.println("Roots: " + q1.getRootsDescription());
System.out.println(
"\n--- Two real roots: x\u00B2 - 5x + 6 ---");
Quadratic q2 = new Quadratic(1, -5, 6);
System.out.println(q2);
System.out.println("Roots: " + q2.getRootsDescription());
System.out.println(
"\n--- One repeated root: x\u00B2 - 6x + 9 ---");
Quadratic q3 = new Quadratic(1, -6, 9);
System.out.println(q3);
System.out.println("Roots: " + q3.getRootsDescription());
System.out.println(
"\n--- Complex roots: x\u00B2 + 2x + 5 ---");
Quadratic q4 = new Quadratic(1, 2, 5);
System.out.println(q4);
System.out.println("Roots: " + q4.getRootsDescription());
System.out.println("\n--- Setter side effects ---");
Quadratic q5 = new Quadratic(1, 0, -4);
System.out.println("Before: " + q5);
q5.setB(-4);
System.out.println("After setB(-4): " + q5);
System.out.println("\n--- Copy constructor ---");
Quadratic q6 = new Quadratic(q2);
System.out.println("Copy of q2: " + q6);
System.out.println(
"\n--- Thanks for using our program! ---");
}//end main
}//end class QuadraticDriver
# Add OrderedPair.py, ComplexOrderedPair.py,
# and Quadratic.py as extra files in OnlineGDB.
from Quadratic import Quadratic
def main():
print("=== Quadratic Main ===\n")
print("--- Default: f(x) = x\u00B2 ---")
q1 = Quadratic()
print(q1)
print(f"f(3) = {q1.f(3)}")
print("Roots:", q1.get_roots_description())
print("\n--- Two real roots: x\u00B2 - 5x + 6 ---")
q2 = Quadratic(1, -5, 6)
print(q2)
print("Roots:", q2.get_roots_description())
print("\n--- One repeated root: x\u00B2 - 6x + 9 ---")
q3 = Quadratic(1, -6, 9)
print(q3)
print("Roots:", q3.get_roots_description())
print("\n--- Complex roots: x\u00B2 + 2x + 5 ---")
q4 = Quadratic(1, 2, 5)
print(q4)
print("Roots:", q4.get_roots_description())
print("\n--- Setter side effects ---")
q5 = Quadratic(1, 0, -4)
print("Before:", q5)
q5.set_b(-4)
print("After set_b(-4):", q5)
print("\n--- Copy constructor ---")
q6 = Quadratic.from_quadratic(q2)
print("Copy of q2:", q6)
print("\n--- Thanks for using our program! ---")
if __name__ == "__main__":
main()
OrderedPair — the Foundation
This is Concept #6’s OrderedPair with two additions: getX() and getY(). These getters are required so ComplexOrderedPair’s copy constructor can read the parent’s private coordinates. For JDoodle, this is File 1 of 4 — paste it first before the other three files.
// getX() and getY() added for subclass access
class OrderedPair {
constructor(xOrOrig, y) {
if (xOrOrig instanceof OrderedPair) {
console.log("...OrderedPair copy constructor...");
this._x = xOrOrig._x;
this._y = xOrOrig._y;
this._absVal = this._computeAbsVal();
this._label = xOrOrig._label + "_copy";
} else if (xOrOrig !== undefined
&& y !== undefined) {
console.log("...OrderedPair two-parameter constructor...");
this._x = xOrOrig;
this._y = y;
this._absVal = this._computeAbsVal();
this._label = "P";
} else {
console.log("...OrderedPair default constructor...");
this._x = 0;
this._y = 0;
this._absVal = this._computeAbsVal();
this._label = "O";
}
}
toString() {
return this._label
+ "(" + this._x + ", " + this._y + ")";
}
getX() { return this._x; }
getY() { return this._y; }
getAbsVal() { return this._absVal; }
getLabel() { return this._label; }
setLabel(lbl) {
console.log("...setLabel...");
this._label = lbl;
}
_computeAbsVal() {
console.log("...computeAbsVal...");
return Math.sqrt(
Math.pow(this._x, 2) + Math.pow(this._y, 2));
}
transpose() {
console.log("...transpose...");
var temp = this._y;
this._y = this._x;
this._x = temp;
this._absVal = this._computeAbsVal();
}
}//end class OrderedPair
// Concept #6 + getX() / getY() for subclass access
public class OrderedPair {
private double x;
private double y;
private double absVal;
private String label;
public OrderedPair(){
System.out.println("...OrderedPair default constructor...");
x = 0; y = 0;
absVal = computeAbsVal();
label = "O";
}
public OrderedPair(double x, double y){
System.out.println(
"...OrderedPair two-parameter constructor...");
this.x = x; this.y = y;
absVal = computeAbsVal();
label = "P";
}
public OrderedPair(OrderedPair orig){
System.out.println("...OrderedPair copy constructor...");
x = orig.x; y = orig.y;
absVal = computeAbsVal();
label = orig.label + "_copy";
}
public String toString(){
return label + "(" + x + ", " + y + ")";
}
public double getX() { return x; }
public double getY() { return y; }
public double getAbsVal() { return absVal; }
public String getLabel() { return label; }
public void setLabel(String lbl){
System.out.println("...setLabel...");
label = lbl;
}
public double computeAbsVal(){
System.out.println("...computeAbsVal...");
return Math.sqrt(
Math.pow(x, 2) + Math.pow(y, 2));
}
public void transpose(){
System.out.println("...transpose...");
double temp = y; y = x; x = temp;
absVal = computeAbsVal();
}
}//end class OrderedPair
import math
class OrderedPair:
"""Concept #6 + get_x() / get_y() for subclass access."""
def __init__(self, x=0.0, y=0.0):
print("...OrderedPair constructor...")
self._x = x
self._y = y
self._abs_val = self._compute_abs_val()
self._label = "O" \
if (x == 0.0 and y == 0.0) else "P"
@classmethod
def from_ordered_pair(cls, orig):
"""Copy constructor."""
print("...OrderedPair copy constructor (classmethod)...")
instance = cls(orig._x, orig._y)
instance._label = orig._label + "_copy"
return instance
def __str__(self):
return (self._label
+ "(" + str(self._x)
+ ", " + str(self._y) + ")")
def get_x(self): return self._x
def get_y(self): return self._y
def get_abs_val(self): return self._abs_val
def get_label(self): return self._label
def set_label(self, lbl):
print("...set_label...")
self._label = lbl
def _compute_abs_val(self):
print("...compute_abs_val...")
return math.sqrt(self._x ** 2 + self._y ** 2)
def transpose(self):
print("...transpose...")
self._x, self._y = self._y, self._x
self._abs_val = self._compute_abs_val()
#end class OrderedPair
ComplexOrderedPair extends OrderedPair
Inherits everything from OrderedPair. Overrides toString() to show a + bi. Adds conjugate() and modulus(). No new fields needed — x is the real part, y is the imaginary part.
// x = real part, y = imaginary part of a + bi
class ComplexOrderedPair extends OrderedPair {
constructor(xOrOrig, y) {
if (xOrOrig instanceof ComplexOrderedPair) {
// copy path
console.log("...ComplexOrderedPair copy constructor...");
super(xOrOrig.getX(), xOrOrig.getY());
this.setLabel(xOrOrig.getLabel());
} else if (xOrOrig !== undefined && y !== undefined) {
// real + imaginary
console.log("...ComplexOrderedPair two-parameter constructor...");
super(xOrOrig, y);
this.setLabel("z");
} else {
// default: 0 + 0i
console.log("...ComplexOrderedPair default constructor...");
super();
this.setLabel("z");
}
}
// Override toString — a+bi notation
toString() {
var r = this.getX();
var i = this.getY();
if (i === 0) return "" + r;
if (r === 0) return i + "i";
if (i > 0) return r + " + " + i + "i";
return r + " - " + Math.abs(i) + "i";
}
// New methods added by the subclass
conjugate() {
return new ComplexOrderedPair(this.getX(), -this.getY());
}
modulus() { return this.getAbsVal(); }
}//end class ComplexOrderedPair
// extends = inherits all OrderedPair fields and methods
public class ComplexOrderedPair extends OrderedPair {
// default constructor: 0 + 0i
public ComplexOrderedPair(){
super(); // calls OrderedPair()
setLabel("z");
}
// two-parameter: real + imaginary
public ComplexOrderedPair(double real, double imaginary){
super(real, imaginary); // calls OrderedPair(x,y)
setLabel("z");
}
// copy constructor — must use getters (orig.x is private!)
public ComplexOrderedPair(ComplexOrderedPair orig){
super(orig.getX(), orig.getY());
setLabel(orig.getLabel());
}
// @Override toString — show a+bi instead of (real, imag)
@Override
public String toString(){
double r = getX(); // inherited getter
double i = getY(); // inherited getter
if (i == 0) return "" + r;
if (r == 0) return i + "i";
if (i > 0) return r + " + " + i + "i";
return r + " - " + Math.abs(i) + "i";
}
// New behavior added by the subclass
public ComplexOrderedPair conjugate(){
return new ComplexOrderedPair(getX(), -getY());
}
public double modulus(){ return getAbsVal(); }
}//end class ComplexOrderedPair
from OrderedPair import OrderedPair
import math
# (ClassName) = inherits from OrderedPair
class ComplexOrderedPair(OrderedPair):
"""x = real part, y = imaginary part."""
def __init__(self, real=0.0, imaginary=0.0):
super().__init__(real, imaginary) # calls OrderedPair.__init__
self._label = "z"
print("...ComplexOrderedPair constructor...")
@classmethod
def from_complex(cls, orig):
"""Copy constructor."""
instance = cls(orig.get_x(), orig.get_y())
instance._label = orig.get_label()
return instance
# Override __str__ — a+bi notation
def __str__(self):
r = self.get_x()
i = self.get_y()
if i == 0: return str(r)
if r == 0: return str(i) + "i"
if i > 0: return str(r) + " + " + str(i) + "i"
return str(r) + " - " + str(abs(i)) + "i"
# New methods added by the subclass
def conjugate(self):
return ComplexOrderedPair(self.get_x(), -self.get_y())
def modulus(self):
return self.get_abs_val() # inherited
#end class ComplexOrderedPair
Quadratic — the Full PCNICOTGSU Class
Every PCNICOTGSU element is present and annotated. Note how the setters call computeDiscriminant() and computeVertex() as side effects — a direct application of the DWR lesson from Concept #7.
// Quadratic: f(x) = ax\u00B2 + bx + c
// PCNICOTGSU anatomy annotated throughout
class Quadratic { // P C N
constructor(aOrOrig, b, c) { // C
if (aOrOrig instanceof Quadratic) {
this._a = aOrOrig._a; this._b = aOrOrig._b;
this._c = aOrOrig._c;
} else if (aOrOrig !== undefined) {
if (aOrOrig === 0) throw new Error("a\u22600");
this._a = aOrOrig; this._b = b; this._c = c;
} else {
this._a = 1; this._b = 0; this._c = 0;
}
this._computeDiscriminant();
this._computeVertex();
}
toString() { // O/T
return "f(x) = " + this._a + "x\u00B2 + "
+ this._b + "x + " + this._c
+ " | vertex: " + this._vertex.toString()
+ " | disc: " + this._discriminant;
}
// Getters G
getA() { return this._a; }
getB() { return this._b; }
getC() { return this._c; }
getDiscriminant() { return this._discriminant; }
getVertex() { return this._vertex; }
// Setters — validate + side effects S
setA(a) {
if (a === 0) throw new Error("a\u22600");
this._a = a;
this._computeDiscriminant(); this._computeVertex();
}
setB(b) { this._b = b; this._computeDiscriminant(); this._computeVertex(); }
setC(c) { this._c = c; this._computeDiscriminant(); this._computeVertex(); }
// Utility methods U
f(x) { return this._a*x*x + this._b*x + this._c; }
_computeDiscriminant() {
this._discriminant = this._b*this._b - 4*this._a*this._c;
}
_computeVertex() {
var h = -this._b / (2 * this._a);
this._vertex = new OrderedPair(h, this.f(h));
this._vertex.setLabel("V");
}
getRootsDescription() {
var d = this._discriminant;
if (d > 0) {
var r1 = (-this._b + Math.sqrt(d)) / (2*this._a);
var r2 = (-this._b - Math.sqrt(d)) / (2*this._a);
return "Two real: x=" + r1 + " and x=" + r2;
} else if (d === 0) {
return "One root: x=" + (-this._b/(2*this._a));
} else {
var rp = -this._b/(2*this._a);
var ip = Math.sqrt(-d)/(2*this._a);
return "Complex: " +
new ComplexOrderedPair(rp, ip).toString() +
" and " +
new ComplexOrderedPair(rp, -ip).toString();
}
}
}//end class Quadratic
//(P)erhaps (C)lown (N)onsense (I)s (C)onstructive
//(O)nly (T)oward (G)etting (S)ettlers (U)nderwear
public class Quadratic { // P C N
// Ivars ─────────────────────────────────── I
private double a, b, c;
private double discriminant;
private OrderedPair vertex; // another class as ivar!
// Constructors ───────────────────────────── C
public Quadratic(){
a=1; b=0; c=0;
computeDiscriminant(); computeVertex();
}
public Quadratic(double a, double b, double c){
if (a==0) throw new
IllegalArgumentException("a cannot be zero");
this.a=a; this.b=b; this.c=c;
computeDiscriminant(); computeVertex();
}
public Quadratic(Quadratic orig){
a=orig.a; b=orig.b; c=orig.c;
computeDiscriminant(); computeVertex();
}
// toString ───────────────────────────────── O/T
@Override public String toString(){
return "f(x)="+a+"x\u00B2+"+b+"x+"+c
+" vertex:"+vertex+" disc:"+discriminant;
}
// Getters ────────────────────────────────── G
public double getA() { return a; }
public double getDiscriminant(){ return discriminant; }
public OrderedPair getVertex() { return vertex; }
// Setters — validate + side effects ──────── S
public void setA(double a){
if (a==0) throw new
IllegalArgumentException("a cannot be zero");
this.a=a;
computeDiscriminant(); // side effect
computeVertex(); // side effect
}
public void setB(double b){
this.b=b; computeDiscriminant(); computeVertex();
}
public void setC(double c){
this.c=c; computeDiscriminant(); computeVertex();
}
// Utility methods ────────────────────────── U
public double f(double x){ return a*x*x + b*x + c; }
private void computeDiscriminant(){
discriminant = b*b - 4*a*c;
}
private void computeVertex(){
double h = -b/(2*a);
vertex = new OrderedPair(h, f(h));
vertex.setLabel("V");
}
public String getRootsDescription(){
if (discriminant > 0){
double r1=(-b+Math.sqrt(discriminant))/(2*a);
double r2=(-b-Math.sqrt(discriminant))/(2*a);
return "Two real: x="+r1+" and x="+r2;
} else if (discriminant == 0){
return "One root: x="+(-b/(2*a));
} else {
// disc < 0: ComplexOrderedPair enters the chat!
double rp=-b/(2*a);
double ip=Math.sqrt(-discriminant)/(2*a);
return "Complex: "
+new ComplexOrderedPair(rp,ip)
+" and "+new ComplexOrderedPair(rp,-ip);
}
}
}//end class Quadratic
from OrderedPair import OrderedPair
from ComplexOrderedPair import ComplexOrderedPair
import math
class Quadratic:
"""f(x) = ax\u00B2 + bx + c (a \u2260 0)."""
def __init__(self, a=1.0, b=0.0, c=0.0): # C
if a == 0: raise ValueError("a \u22600")
self._a = a; self._b = b; self._c = c
self._discriminant = 0.0
self._vertex = None
self._compute_discriminant()
self._compute_vertex()
@classmethod
def from_quadratic(cls, orig): # C copy
return cls(orig._a, orig._b, orig._c)
def __str__(self): # O/T
return (f"f(x)={self._a}x\u00B2+{self._b}x+{self._c}"
f" vertex:{self._vertex}"
f" disc:{self._discriminant}")
# Getters G
def get_a(self): return self._a
def get_discriminant(self): return self._discriminant
def get_vertex(self): return self._vertex
# Setters — validate + side effects S
def set_a(self, a):
if a == 0: raise ValueError("a \u22600")
self._a = a
self._compute_discriminant() # side effect
self._compute_vertex() # side effect
def set_b(self, b):
self._b = b
self._compute_discriminant()
self._compute_vertex()
def set_c(self, c):
self._c = c
self._compute_discriminant()
self._compute_vertex()
# Utility methods U
def f(self, x):
return self._a*x**2 + self._b*x + self._c
def _compute_discriminant(self):
self._discriminant = self._b**2 - 4*self._a*self._c
def _compute_vertex(self):
h = -self._b / (2 * self._a)
self._vertex = OrderedPair(h, self.f(h))
self._vertex.set_label("V")
def get_roots_description(self):
d = self._discriminant
if d > 0:
r1 = (-self._b + math.sqrt(d)) / (2*self._a)
r2 = (-self._b - math.sqrt(d)) / (2*self._a)
return f"Two real: x={r1} and x={r2}"
elif d == 0:
return f"One root: x={-self._b/(2*self._a)}"
else:
rp = -self._b / (2*self._a)
ip = math.sqrt(-d) / (2*self._a)
z1 = ComplexOrderedPair(rp, ip)
z2 = ComplexOrderedPair(rp, -ip)
return f"Complex: {z1} and {z2}"
#end class Quadratic
Why Does Each Version Look Different?
“What does each version reveal about the language’s personality?”
One constructor, all three paths — same as Concept #6. JavaScript allows exactly one constructor per class. The inheritance pattern via extends and super() is identical in syntax to Java, but the enforcement is not: nothing in JavaScript prevents you from bypassing the gate. _computeVertex is private by convention, not enforcement.
extends in ES6 is real prototype-based inheritance. When ComplexOrderedPair extends OrderedPair, JavaScript builds a prototype chain. this.getX() in ComplexOrderedPair walks up that chain to OrderedPair.prototype.getX. The method lookup is dynamic: a subclass method shadows a parent method with the same name, which is exactly how toString() override works.
Constructor overloading + @Override guarantee. Java’s three separate constructors are distinct, compiler-resolved signatures. The @Override annotation on toString() is not decoration — the compiler verifies that a method with this exact signature actually exists in the parent. If you misspell it or change the return type, the annotation causes a compile error rather than a silent shadow. That is a safety net JavaScript cannot offer.
Copy constructor accesses the parent’s private fields directly. In Quadratic’s copy constructor, this.a = orig.a compiles because both objects are the same class — Java allows private access within the same class regardless of which instance. In ComplexOrderedPair’s copy constructor, orig.x would fail because x is private in OrderedPair, a different class. Getters are required. This is the distinction that forces the getX() and getY() additions.
Single-argument super().__init__() — no class name, no self, no repeated ceremony. Python 3 resolves the Method Resolution Order (MRO) automatically. In contrast to Java’s explicit super(real, imaginary), Python’s cooperative super().__init__(real, imaginary) participates in the full inheritance chain, which matters in multiple-inheritance scenarios Python supports and Java does not.
from_quadratic is a @classmethod, not a constructor overload. Python has one __init__. The copy-constructor pattern requires a factory classmethod. This is the same pattern seen in Concept #6. It is more explicit than Java (you type Quadratic.from_quadratic(q2), not just new Quadratic(q2)) but reveals Python’s philosophy: named factory methods communicate intent clearly; overloaded constructors rely on type matching.