Du schreibst Klassen und baust daraus Objekte: jedes mit seinen eigenen Daten und mit Methoden, die damit arbeiten. Ein Objekt passt auf seinen Zustand auf, kennt andere Objekte, zeigt sich als Text und erbt von einer Klasse, was es nicht selbst anders macht.
Eine Klasse ist ein Bauplan. Aus ihm baut man Objekte, so viele man
will, und jedes hat seine eigenen Daten, die Attribute. Was ein Objekt
kann, steht in seinen Methoden. Dieser Zähler fängt bei 0 an und zählt
bei jedem increment eins weiter:
class Counter def initialize @value = 0 end def increment @value += 1 end def value @value end end clicks = Counter.new clicks.increment puts clicks.value # 1
initialize läuft, wenn Counter.new ein neues Objekt baut. Ein Name mit
@ davor ist ein Attribut: Er gehört dem Objekt, nicht der Methode, und
bleibt, wenn die Methode fertig ist.
class Counter: def __init__(self): self.value = 0 def increment(self): self.value += 1 clicks = Counter() clicks.increment() print(clicks.value) # 1
__init__ läuft, wenn Counter() ein neues Objekt baut. self ist das
Objekt selbst; self.value ist ein Attribut: Es gehört dem Objekt, nicht
der Methode, und bleibt, wenn die Methode fertig ist. Jede Methode bekommt
self als ersten Parameter, beim Aufruf schreibt man es nicht hin.
class Counter { constructor() { this.value = 0; } increment() { this.value += 1; } } const clicks = new Counter(); clicks.increment(); console.log(clicks.value); // 1
constructor läuft, wenn new Counter() ein neues Objekt baut. this ist
das Objekt selbst; this.value ist ein Attribut: Es gehört dem Objekt,
nicht der Methode, und bleibt, wenn die Methode fertig ist.
Was gibt die letzte Zeile aus?
a = Counter.new b = Counter.new a.increment a.increment b.increment puts a.value
Was gibt die letzte Zeile aus?
a = Counter() b = Counter() a.increment() a.increment() b.increment() print(a.value)
Was gibt die letzte Zeile aus?
const a = new Counter(); const b = new Counter(); a.increment(); a.increment(); b.increment(); console.log(a.value);
Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.
a zählt bis 2, b bis 1: Jedes Objekt hat sein eigenes Attribut value.
Die Klasse gibt nur vor, dass es eins gibt; welchen Wert es hat, steht in
jedem Objekt für sich.
Methoden können auch Parameter haben, wie Funktionen. Das Objekt bekommt seine Daten beim Bauen mit:
class Circle def initialize(radius) @radius = radius end def diameter 2 * @radius end end puts Circle.new(5).diameter # 10
class Circle: def __init__(self, radius): self.radius = radius def diameter(self): return 2 * self.radius print(Circle(5).diameter()) # 10
class Circle { constructor(radius) { this.radius = radius; } diameter() { return 2 * this.radius; } } console.log(new Circle(5).diameter()); // 10
Schreib eine Klasse Rectangle, deren Objekte mit Breite und Höhe gebaut werden, Rectangle.new(3, 4), und die zwei Methoden hat: area gibt die Fläche zurück (12), perimeter den Umfang (14). Die Prüfung baut auch zwei Rechtecke nebeneinander.
initialize bekommt Breite und Höhe, aber die Parameter sind weg, wenn initialize fertig ist. Was muss das Objekt sich merken, damit area später daran kommt?
Schreib in initialize @width = width und @height = height. Dann können area und perimeter mit @width und @height rechnen.
Schreib eine Klasse Rectangle, deren Objekte mit Breite und Höhe gebaut werden, Rectangle(3, 4), und die zwei Methoden hat: area() gibt die Fläche zurück (12), perimeter() den Umfang (14). Die Prüfung baut auch zwei Rechtecke nebeneinander.
__init__ bekommt Breite und Höhe, aber die Parameter sind weg, wenn __init__ fertig ist. Was muss das Objekt sich merken, damit area später daran kommt?
Schreib in __init__ self.width = width und self.height = height. Dann können area und perimeter mit self.width und self.height rechnen.
Schreib eine Klasse Rectangle, deren Objekte mit Breite und Höhe gebaut werden, new Rectangle(3, 4), und die zwei Methoden hat: area() gibt die Fläche zurück (12), perimeter() den Umfang (14). Die Prüfung baut auch zwei Rechtecke nebeneinander.
Der constructor bekommt Breite und Höhe, aber die Parameter sind weg, wenn er fertig ist. Was muss das Objekt sich merken, damit area später daran kommt?
Schreib im constructor this.width = width; und this.height = height;. Dann können area und perimeter mit this.width und this.height rechnen.
Ein Objekt ändert seine Attribute nur in seinen Methoden. Darum kann es Regeln durchsetzen: Ein Konto lässt sich nicht ins Minus bringen, wenn die Methode, die abhebt, vorher nachsieht. Wer das Konto benutzt, muss die Regel nicht kennen; er ruft die Methode auf, und das Objekt entscheidet. Das heißt Kapselung.
Schreib eine Klasse Account für ein Konto, das mit einem Kontostand gebaut wird, Account.new(10). deposit(amount) zahlt ein, balance gibt den Kontostand zurück. withdraw(amount) hebt ab und gibt true zurück, aber nur, wenn genug da ist; sonst bleibt der Kontostand, wie er ist, und withdraw gibt false zurück.
Das Konto merkt sich seinen Stand in einem Attribut, @balance. Alle vier Methoden arbeiten damit.
In withdraw kommt zuerst die Frage, ob amount größer ist als der Kontostand. Wenn ja: return false, ohne etwas zu ändern.
Sonst ziehst du ab und gibst true zurück: @balance -= amount, dann true.
Schreib eine Klasse Account für ein Konto, das mit einem Kontostand gebaut wird, Account(10). deposit(amount) zahlt ein, balance() gibt den Kontostand zurück. withdraw(amount) hebt ab und gibt True zurück, aber nur, wenn genug da ist; sonst bleibt der Kontostand, wie er ist, und withdraw gibt False zurück.
Das Konto merkt sich seinen Stand in einem Attribut. Nenn es nicht self.balance, denn so heißt schon die Methode; self.amount oder self.money geht.
In withdraw kommt zuerst die Frage, ob amount größer ist als der Kontostand. Wenn ja: return False, ohne etwas zu ändern.
Sonst ziehst du ab und gibst True zurück.
Schreib eine Klasse Account für ein Konto, das mit einem Kontostand gebaut wird, new Account(10). deposit(amount) zahlt ein, balance() gibt den Kontostand zurück. withdraw(amount) hebt ab und gibt true zurück, aber nur, wenn genug da ist; sonst bleibt der Kontostand, wie er ist, und withdraw gibt false zurück.
Das Konto merkt sich seinen Stand in einem Attribut. Nenn es nicht this.balance, denn so heißt schon die Methode; this.money geht.
In withdraw kommt zuerst die Frage, ob amount größer ist als der Kontostand. Wenn ja: return false;, ohne etwas zu ändern.
Sonst ziehst du ab und gibst true zurück.
Ein Attribut kann auch ein anderes Objekt sein, oder eine Liste von Objekten. Eine Playlist kennt ihre Songs, und jeder Song kennt seinen Titel und seine Länge:
class Song attr_reader :title, :seconds def initialize(title, seconds) @title = title @seconds = seconds end end class Playlist def initialize @songs = [] end def add(song) @songs << song end def count @songs.length end end mix = Playlist.new mix.add(Song.new("Intro", 95)) mix.add(Song.new("Outro", 210)) puts mix.count # 2
attr_reader :title, :seconds schreibt die Methoden title und seconds,
die nur den Wert des Attributs zurückgeben, damit man von außen
song.title fragen kann.
class Song: def __init__(self, title, seconds): self.title = title self.seconds = seconds class Playlist: def __init__(self): self.songs = [] def add(self, song): self.songs.append(song) def count(self): return len(self.songs) mix = Playlist() mix.add(Song("Intro", 95)) mix.add(Song("Outro", 210)) print(mix.count()) # 2
class Song { constructor(title, seconds) { this.title = title; this.seconds = seconds; } } class Playlist { constructor() { this.songs = []; } add(song) { this.songs.push(song); } count() { return this.songs.length; } } const mix = new Playlist(); mix.add(new Song("Intro", 95)); mix.add(new Song("Outro", 210)); console.log(mix.count()); // 2
Die Klassen Song und Playlist von oben stehen schon da. Schreib in Playlist zwei Methoden: total_seconds gibt zurück, wie lange alle Songs zusammen dauern, und longest_title den Titel des längsten Songs; dauern zwei gleich lang, den, der zuerst kam. In einer leeren Playlist ergibt total_seconds 0 und longest_title nil.
Beide Methoden gehen durch @songs. Jeder Song kennt seine Länge, song.seconds, und seinen Titel, song.title.
Für total_seconds zählst du alle song.seconds zusammen, mit einer Schleife oder @songs.sum { |song| song.seconds }.
Für longest_title merkst du dir den längsten Song bisher, anfangs nil, und tauschst ihn nur, wenn ein Song länger ist. Am Ende gibst du seinen Titel zurück, oder nil, wenn es keinen gab.
Die Klassen Song und Playlist von oben stehen schon da. Schreib in Playlist zwei Methoden: total_seconds() gibt zurück, wie lange alle Songs zusammen dauern, und longest_title() den Titel des längsten Songs; dauern zwei gleich lang, den, der zuerst kam. In einer leeren Playlist ergibt total_seconds() 0 und longest_title() None.
Beide Methoden gehen durch self.songs. Jeder Song kennt seine Länge, song.seconds, und seinen Titel, song.title.
Für total_seconds zählst du alle song.seconds zusammen, mit einer Schleife oder sum(song.seconds for song in self.songs).
Für longest_title merkst du dir den längsten Song bisher, anfangs None, und tauschst ihn nur, wenn ein Song länger ist. Am Ende gibst du seinen Titel zurück, oder None, wenn es keinen gab.
Die Klassen Song und Playlist von oben stehen schon da. Schreib in Playlist zwei Methoden: totalSeconds() gibt zurück, wie lange alle Songs zusammen dauern, und longestTitle() den Titel des längsten Songs; dauern zwei gleich lang, den, der zuerst kam. In einer leeren Playlist ergibt totalSeconds() 0 und longestTitle() null.
Beide Methoden gehen durch this.songs. Jeder Song kennt seine Länge, song.seconds, und seinen Titel, song.title.
Für totalSeconds zählst du alle song.seconds zusammen, mit einer Schleife oder mit reduce.
Für longestTitle merkst du dir den längsten Song bisher, anfangs null, und tauschst ihn nur, wenn ein Song länger ist. Am Ende gibst du seinen Titel zurück, oder null, wenn es keinen gab.
Macht man aus einem Objekt einfach Text, steht da wenig: in Ruby und Python
die Klasse und wo das Objekt im Speicher liegt, in JavaScript nur
[object Object]. Was ein Mensch lesen soll, bestimmt eine Methode mit
einem festen Namen:
class Point def initialize(x, y) @x = x @y = y end def to_s "(#{@x}, #{@y})" end end puts Point.new(3, 4) # (3, 4)
puts ruft to_s auf, und "Punkt #{p}" auch.
class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" print(Point(3, 4)) # (3, 4)
print und str(p) rufen __str__ auf, und f"Punkt {p}" auch.
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } console.log(`${new Point(3, 4)}`); // (3, 4)
String(p) und `Punkt ${p}` rufen toString auf. console.log(p)
allein zeigt dagegen die Attribute, nicht toString.
Schreib eine Klasse Money, deren Objekte mit Euro und Cent gebaut werden, Money.new(3, 5), und die sich als Text so zeigt: 3,05 €. Die Cent haben immer zwei Ziffern. Die Prüfung ruft to_s auf und setzt ein Objekt auch in einen Text ein.
Merk dir Euro und Cent in zwei Attributen und schreib eine Methode to_s, die einen Text zurückgibt.
Die Cent bekommen mit format("%02d", @cents) zwei Ziffern, mit einer 0 vorn, wo nötig.
"#{@euros},#{format("%02d", @cents)} €" setzt alles zusammen.
Schreib eine Klasse Money, deren Objekte mit Euro und Cent gebaut werden, Money(3, 5), und die sich als Text so zeigt: 3,05 €. Die Cent haben immer zwei Ziffern. Die Prüfung ruft str() auf und setzt ein Objekt auch in einen f-String ein.
Merk dir Euro und Cent in zwei Attributen und schreib eine Methode __str__, die einen Text zurückgibt.
Die Cent bekommen mit f"{self.cents:02d}" zwei Ziffern, mit einer 0 vorn, wo nötig.
f"{self.euros},{self.cents:02d} €" setzt alles zusammen.
Schreib eine Klasse Money, deren Objekte mit Euro und Cent gebaut werden, new Money(3, 5), und die sich als Text so zeigt: 3,05 €. Die Cent haben immer zwei Ziffern. Die Prüfung ruft String() auf und setzt ein Objekt auch in einen Template-String ein.
Merk dir Euro und Cent in zwei Attributen und schreib eine Methode toString(), die einen Text zurückgibt.
Die Cent bekommen mit String(this.cents).padStart(2, "0") zwei Ziffern, mit einer 0 vorn, wo nötig.
`${this.euros},${String(this.cents).padStart(2, "0")} €` setzt alles zusammen.
Manche Klassen sind fast gleich. Statt alles zweimal zu schreiben, erbt eine Klasse von einer anderen: Sie bekommt deren Attribute und Methoden und schreibt nur, was bei ihr anders ist. Diese Methode heißt dann überschrieben:
class Animal def initialize(name) @name = name end def sound "..." end def describe "#{@name} sagt #{sound}" end end class Dog < Animal def sound "Wuff" end end class Cat < Animal def sound "Miau" end end
class Dog < Animal heißt: Dog erbt von Animal.
class Animal: def __init__(self, name): self.name = name def sound(self): return "..." def describe(self): return f"{self.name} sagt {self.sound()}" class Dog(Animal): def sound(self): return "Wuff" class Cat(Animal): def sound(self): return "Miau"
class Dog(Animal) heißt: Dog erbt von Animal.
class Animal { constructor(name) { this.name = name; } sound() { return "..."; } describe() { return `${this.name} sagt ${this.sound()}`; } } class Dog extends Animal { sound() { return "Wuff"; } } class Cat extends Animal { sound() { return "Miau"; } }
class Dog extends Animal heißt: Dog erbt von Animal.
Was gibt puts Dog.new("Bello").describe aus?
Was gibt print(Dog("Bello").describe()) aus?
Was gibt console.log(new Dog("Bello").describe()) aus?
Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.
describe steht nur in Animal, und trotzdem kommt Bello sagt Wuff
heraus: Ruft describe die Methode sound auf, nimmt die Sprache die des
Objekts, und das ist ein Dog. Dieselbe Zeile Code tut also je nach Objekt
etwas anderes. Das heißt Polymorphie: Wer describe aufruft, muss nicht
wissen, ob er einen Hund oder eine Katze vor sich hat.
Schreib zwei Klassen, Square (gebaut mit einer Seitenlänge) und Rectangle (mit Breite und Höhe), beide mit einer Methode area. Dann eine Funktion total_area(shapes), die die Flächen aller Formen einer Liste zusammenzählt. total_area fragt jede Form nur nach area, ohne nachzusehen, von welcher Klasse sie ist: Die Prüfung gibt ihr auch eine Form, die du nicht kennst.
Beide Klassen merken sich ihre Maße in initialize und haben eine Methode area, jede mit ihrer eigenen Rechnung.
total_area geht durch die Liste und zählt shape.area zusammen, mit einer Schleife oder shapes.sum { |shape| shape.area }.
Schreib zwei Klassen, Square (gebaut mit einer Seitenlänge) und Rectangle (mit Breite und Höhe), beide mit einer Methode area(). Dann eine Funktion total_area(shapes), die die Flächen aller Formen einer Liste zusammenzählt. total_area fragt jede Form nur nach area(), ohne nachzusehen, von welcher Klasse sie ist: Die Prüfung gibt ihr auch eine Form, die du nicht kennst.
Beide Klassen merken sich ihre Maße in __init__ und haben eine Methode area, jede mit ihrer eigenen Rechnung.
total_area geht durch die Liste und zählt shape.area() zusammen, mit einer Schleife oder sum(shape.area() for shape in shapes).
Schreib zwei Klassen, Square (gebaut mit einer Seitenlänge) und Rectangle (mit Breite und Höhe), beide mit einer Methode area(). Dann eine Funktion totalArea(shapes), die die Flächen aller Formen einer Liste zusammenzählt. totalArea fragt jede Form nur nach area(), ohne nachzusehen, von welcher Klasse sie ist: Die Prüfung gibt ihr auch eine Form, die du nicht kennst.
Beide Klassen merken sich ihre Maße im constructor und haben eine Methode area(), jede mit ihrer eigenen Rechnung.
totalArea geht durch die Liste und zählt shape.area() zusammen, mit einer Schleife oder mit reduce.
Soll eine überschriebene Methode die der Oberklasse nicht ersetzen, sondern
ergänzen, ruft sie sie mit super auf:
class Puppy < Dog def sound super + " (leise)" end end puts Puppy.new("Fips").describe # Fips sagt Wuff (leise)
In initialize gibt super(name) den Namen an das initialize der
Oberklasse weiter.
class Puppy(Dog): def sound(self): return super().sound() + " (leise)" print(Puppy("Fips").describe()) # Fips sagt Wuff (leise)
In __init__ gibt super().__init__(name) den Namen an das __init__ der
Oberklasse weiter.
class Puppy extends Dog { sound() { return super.sound() + " (leise)"; } } console.log(new Puppy("Fips").describe()); // Fips sagt Wuff (leise)
Im constructor gibt super(name) den Namen an den constructor der
Oberklasse weiter; erst danach darf man this benutzen.
Die Klasse Employee steht schon da: Eine Angestellte hat einen Namen und ein Gehalt im Monat, und yearly gibt das Gehalt im Jahr zurück. Schreib eine Klasse Manager, die von Employee erbt, dazu einen Bonus im Jahr bekommt, Manager.new("Bo", 4000, 5000), und deren yearly den Bonus dazurechnet. Benutze dabei super, in initialize und in yearly, statt das Gehalt noch einmal selbst zu rechnen.
Manager braucht nur den Bonus selbst. Name und Monatsgehalt gibt sein initialize mit super(name, monthly) an Employee weiter.
In yearly ruft super das yearly von Employee auf. Dazu kommt der Bonus.
Die Klasse Employee steht schon da: Eine Angestellte hat einen Namen und ein Gehalt im Monat, und yearly() gibt das Gehalt im Jahr zurück. Schreib eine Klasse Manager, die von Employee erbt, dazu einen Bonus im Jahr bekommt, Manager("Bo", 4000, 5000), und deren yearly() den Bonus dazurechnet. Benutze dabei super(), in __init__ und in yearly, statt das Gehalt noch einmal selbst zu rechnen.
Manager braucht nur den Bonus selbst. Name und Monatsgehalt gibt sein __init__ mit super().__init__(name, monthly) an Employee weiter.
In yearly ruft super().yearly() das yearly von Employee auf. Dazu kommt der Bonus.
Die Klasse Employee steht schon da: Eine Angestellte hat einen Namen und ein Gehalt im Monat, und yearly() gibt das Gehalt im Jahr zurück. Schreib eine Klasse Manager, die von Employee erbt, dazu einen Bonus im Jahr bekommt, new Manager("Bo", 4000, 5000), und deren yearly() den Bonus dazurechnet. Benutze dabei super, im constructor und in yearly, statt das Gehalt noch einmal selbst zu rechnen.
Manager braucht nur den Bonus selbst. Name und Monatsgehalt gibt sein constructor mit super(name, monthly) an Employee weiter; erst danach darf er this benutzen.
In yearly ruft super.yearly() das yearly von Employee auf. Dazu kommt der Bonus.
Klassen, Objekte und Vererbung gibt es seit 1967, aus einer Sprache für Simulationen.
Kristen Nygaard (1926–2002) und Ole-Johan Dahl (1931–2002), norwegische Informatiker am Norwegischen Rechenzentrum in Oslo. In den 1960er-Jahren bauten sie Simula, eine Sprache, um Abläufe zu simulieren. In Simula 67 gab es zum ersten Mal Objekte, Klassen, Unterklassen und Vererbung.
Smalltalk übernahm die Idee, später C++ und Java. Für Simula bekamen beide den Turing Award für 2001. Jede Klasse, die du schreibst, geht auf ihre Arbeit zurück.
Ein Extra für Schnelle: Die Seite ist ohne dieses Kapitel vollständig.
Zwei Katas, die nicht mitzählen: ein Stack als Klasse, und ein Bruch, der rechnen kann.
Schreib eine Klasse Stack für einen Stapel, bei dem nur das oberste Element zugänglich ist: push(x) legt x oben drauf, pop nimmt das oberste weg und gibt es zurück, peek gibt es zurück, ohne es wegzunehmen, size sagt, wie viele Elemente drauf liegen. Ist der Stack leer, geben pop und peek nil zurück.
Der Stack merkt sich seine Elemente in einer Liste, @items = []. Oben ist ihr Ende.
@items.push(x) legt drauf, @items.pop nimmt das letzte weg und gibt es zurück, bei einer leeren Liste nil; @items.last schaut nur.
Schreib eine Klasse Stack für einen Stapel, bei dem nur das oberste Element zugänglich ist: push(x) legt x oben drauf, pop() nimmt das oberste weg und gibt es zurück, peek() gibt es zurück, ohne es wegzunehmen, size() sagt, wie viele Elemente drauf liegen. Ist der Stack leer, geben pop() und peek() None zurück.
Der Stack merkt sich seine Elemente in einer Liste, self.items = []. Oben ist ihr Ende.
self.items.append(x) legt drauf, self.items.pop() nimmt das letzte weg; bei einer leeren Liste bricht es mit einem Fehler ab, darum fragst du vorher if not self.items:. self.items[-1] schaut nur.
Schreib eine Klasse Stack für einen Stapel, bei dem nur das oberste Element zugänglich ist: push(x) legt x oben drauf, pop() nimmt das oberste weg und gibt es zurück, peek() gibt es zurück, ohne es wegzunehmen, size() sagt, wie viele Elemente drauf liegen. Ist der Stack leer, geben pop() und peek() null zurück.
Der Stack merkt sich seine Elemente in einer Liste, this.items = [];. Oben ist ihr Ende.
this.items.push(x) legt drauf, this.items.pop() nimmt das letzte weg; bei einer leeren Liste gibt es undefined, darum fragst du vorher if (this.items.length === 0). this.items[this.items.length - 1] schaut nur.
Schreib eine Klasse Fraction für Brüche, gebaut mit Zähler und Nenner, Fraction.new(2, 4), die sich gleich gekürzt merkt, also als 1/2. to_s zeigt sie als 1/2, und add(other) gibt einen neuen, gekürzten Bruch mit der Summe zurück, ohne die beiden zu ändern. Integer#gcd gibt den größten gemeinsamen Teiler: 12.gcd(18) ergibt 6.
Für die Summe von a/b und c/d gilt: Zähler a·d + c·b, Nenner b·d. Danach kürzen.
Kürzt initialize schon, muss add nur Fraction.new(…, …) mit dem ungekürzten Ergebnis bauen; dafür braucht add Zähler und Nenner des anderen Bruchs, also zwei Methoden, die sie zurückgeben (attr_reader).
Schreib eine Klasse Fraction für Brüche, gebaut mit Zähler und Nenner, Fraction(2, 4), die sich gleich gekürzt merkt, also als 1/2. str() zeigt sie als 1/2, und add(other) gibt einen neuen, gekürzten Bruch mit der Summe zurück, ohne die beiden zu ändern. math.gcd(12, 18) gibt den größten gemeinsamen Teiler, 6.
Für die Summe von a/b und c/d gilt: Zähler a·d + c·b, Nenner b·d. Danach kürzen.
Kürzt __init__ schon, muss add nur Fraction(…, …) mit dem ungekürzten Ergebnis bauen. Die Attribute des anderen Bruchs erreichst du mit other.numerator.
Schreib eine Klasse Fraction für Brüche, gebaut mit Zähler und Nenner, new Fraction(2, 4), die sich gleich gekürzt merkt, also als 1/2. String() zeigt sie als 1/2, und add(other) gibt einen neuen, gekürzten Bruch mit der Summe zurück, ohne die beiden zu ändern. Den größten gemeinsamen Teiler gibt die Funktion gcd, die schon dasteht.
Für die Summe von a/b und c/d gilt: Zähler a·d + c·b, Nenner b·d. Danach kürzen.
Kürzt der constructor schon, muss add nur new Fraction(…, …) mit dem ungekürzten Ergebnis bauen. Die Attribute des anderen Bruchs erreichst du mit other.numerator.