Hanspolo's Blog

Clean Code Developer - Roter Grad: Favour Composition over Inheritance und Simple Refactorings

Development

Favour Composition over Inheritance

*Ergänzende eigene Gedanken zu clean-code-developer.de

Mit Favour Composition over Inheritance kommt ein Schwergewicht in unseren Baukasten, das ein wenig mehr Erklärung benötigt.

Eine Eigenschaft von objektorientierten Programmiersprachen ist die Vererbung von Klassen. Hier zunächst ein kleines Beispiel in ruby:

1
2
3
4
5
6
7
8
class Animal
  def eat()
    ...
  end
end

class Duck < Animal
end

Die Klasse Duck erbt hierbei die Funktion eat() von Animal. Irgendwie beschreibt das eine Ente aber bisher noch nicht zur Genüge. Eine Ente hat außerdem Federn, kann fliegen, hat einen Schnabel, anders als andere Tiere, die mit Schnauze und Reißzähnen ausgestattet sind und schwimmt dabei vergnügt auf einem See herum. Um diese Eigenschaften über Vererbung abzudecken, müsstest du ggf. die komplette Tierwelt mit allen ihren Verästelungen abbilden.

Manche Sprachen bieten hier Mehrfachvererbung. Unsere Ente könnte also gleichzeitig von Animal, FlyingCreature und SwimmingCreature erben. Hierbei entstehen sehr harte Abhängigkeiten.

Ein Ziel von Code sollte aber eigentlich immer eine lose Kopplung sein. Lose Kopplung macht das System einfacher wandelbar, wartbar und testbarer.

Wie sieht also eine Komposition einer Ente aus, wenn sie zum Einen ein Lebewesen, dass essen muss, ist, aber auch fliegen und schwimmen kann? In einem einfachen Fall vielleicht ungefähr so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Duck
  def initialize(eatingAnimal, flyingAnimal, swimmingAnimal)
    @eater = eatingAnimal
    @flyer = flyingAnimal
    @swimmer = swimmingAnimal
  end

  def fly()
    @flyer.fly()
  end
end

Duck.new(Animal, FlyingCreature, SwimmingCreature)

In typisierten Sprachen und auch in Ruby 3.0 sollte man Interfaces benutzen, um z.B. sicherzustellen, dass flyingAnimal die Methode fly() auch wirklich definiert. Ein Beispiel in der Sprache go, die Interfaces und Typisierung unterstützt, würde dann so aussehen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Flyer interface {
  fly()
}

type Eater interface {
  eat()
}

type Swimmer interface {
  swim()
}

type Duck struct {
  eater Eater
  flyer Flyer
  swimmer Swimmer
}

func NewDuck(eater Eater, flyer Flyer, swimmer Swimmer) *Duck {
  return &Duck{
    eater: eater,
    flyer: flyer,
    swimmer: swimmer,
  }
}

func (d *Duck) fly() {
  d.fly()
}

Da go keine Vererbung mitbringt, ist hier die Frage nach Vererbung oder Komposition auch schnell beantwortet. Die Verwendung von Interfaces hat hierbei den Vorteil, dass wir der Ente alles übergeben können, was dieses Interface erfüllt. Ob das nun eine FishEatingCreature oder eine BreadFromGrannieEatingCreature ist, bleibt hier dem Aufrufer überlassen.

Simple Refactorings

*Ergänzende eigene Gedanken zu clean-code-developer.de

Bei dieser Praxis geht es vor allem darum, dass du die Prinzipien, die du gelernt hast, nicht nur auf neuen, sondern auch auf bestehenden Code anwendest. Wenn es einfach möglich ist, so versuche, dass sich der Code, den du vor dir hast, ähnlich der Pfadfinderregel ein wenig verbessert.

Der Artikel selbst benötigt meiner Meinung nach gar nicht so viel Ergänzung, daher verweise ich nur auf ihn.