Blog 8 Min. Lesezeit

OpenCV: Die Kunst, die KISS- und YAGNI-Prinzipien anzuwenden

Diesen Artikel teilen
OpenCV: The Art of Using the KISS and YAGNI Principles

Als Programmierer sind wir oft versucht, Design Patterns, Sprachidiome, fortgeschrittene Sprachfeatures und bekannte Bibliotheken einzusetzen – was durchaus sinnvoll ist. Bevor wir jedoch loslegen, sollten wir diese Techniken durch die Brille der KISS- und YAGNI-Prinzipien betrachten.

„KISS“ steht für „Keep It Simple, Stupid“. Es ist ein Designprinzip, nach dem Einfachheit ein zentrales Ziel sein sollte und unnötige Komplexität vermieden werden muss. Einfache Lösungen sind leichter zu verstehen, zu warten und zu debuggen. Das KISS-Prinzip wird in vielen Bereichen angewendet, darunter Ingenieurwesen, Softwareentwicklung, UI-Design und Projektmanagement.

YAGNI steht für „You Ain’t Gonna Need It“. Es ist ein Prinzip der Softwareentwicklung und agiler Methoden, das empfiehlt, Funktionen erst dann zur Codebasis hinzuzufügen, wenn sie tatsächlich benötigt werden, um ein konkretes Problem zu lösen oder eine Anforderung zu erfüllen.

Das YAGNI-Prinzip beruht auf der Idee, dass das vorzeitige Hinzufügen unnötiger Funktionen zu verschiedenen Problemen führen kann.

OpenCV (Open Source Computer Vision) ist eine plattformübergreifende Bibliothek mit Programmierfunktionen, die vor allem auf Computer Vision in Echtzeit ausgerichtet ist und im Intel-Forschungszentrum in Nischni Nowgorod entwickelt wurde. Ihr Schwerpunkt liegt auf der Bildverarbeitung in Echtzeit.

OpenCV ist ein umfangreiches Projekt mit zahlreichen komplexen Funktionen. Dennoch haben seine Entwickler grundlegende Prinzipien konsequent umgesetzt, wodurch die Codebasis deutlich leichter zu verstehen und zu warten ist.

Sehen wir uns einige Designentscheidungen von OpenCV an:

Modularität

1 – Bibliotheksbasierte Architektur

Eine bibliotheksbasierte Architektur erleichtert die Wiederverwendung und Integration bereitgestellter Funktionen in andere Projekte. Außerdem fördert sie saubere APIs und eine klare Trennung der Verantwortlichkeiten, sodass Entwickler sich auf kleine Teile des Gesamtsystems konzentrieren können.

OpenCV verfolgt diesen Ansatz und definiert zahlreiche Bibliotheken. Jede hat eine klar abgegrenzte Verantwortung, und alle verwenden die Bibliothek opencv_core.

opencv1

2 – Modularisierung durch Namespaces

OpenCV nutzt Namespaces intensiv, um seine Codebasis wirkungsvoll zu strukturieren. Hier sind einige Beispiele aus dem Projekt opencv_core:

opencv2

OpenCV verwendet den Ansatz „Namespace nach Feature“. Dabei werden alle Elemente, die zu genau einem Feature gehören, in einem gemeinsamen Namespace zusammengefasst. So entstehen Namespaces mit hoher Kohäsion und Modularität sowie minimaler Kopplung untereinander. Eng zusammenarbeitende Elemente liegen direkt beieinander.

Bei OpenCV werden Namespaces hauptsächlich aus drei Gründen verwendet:

  • Die Bibliotheken modularisieren.
  • Implementierungsdetails verbergen, etwa im Namespace „cv::detail“. Damit lässt sich dem Nutzer signalisieren, dass die darin enthaltenen Typen nur intern gedacht sind. In C# erfüllt das Schlüsselwort „internal“ diese Aufgabe; in C++ gibt es keine entsprechende Möglichkeit, öffentliche Typen vor Bibliotheksnutzern zu verbergen.
  • Anonymer Namespace: ein Namespace ohne Namen. Er vermeidet globale statische Variablen und ist ausschließlich innerhalb der Datei zugänglich, in der er definiert wurde.

Das Datenmodell als POD-Typen definieren

Jedes Projekt besitzt ein Datenmodell. Dieses kann mit Plain-Old-Data-Typen (POD) definiert werden. Ein POD-Typ ist eine Datenstruktur, die lediglich aus passiven Feldwerten besteht und keine objektorientierten Features nutzt. POD-Typen bieten zahlreiche Vorteile:

  1. Effizienz: POD-Typen besitzen typischerweise ein einfaches Speicherlayout, was häufig zu effizienterer Speichernutzung und höherer Geschwindigkeit führt. Der Overhead komplexer Datenstrukturen und Memberfunktionen entfällt.
  2. Kompatibilität: POD-Typen sind mit Low-Level-Programmierkonstrukten und Datenaustauschformaten kompatibel und eignen sich daher gut für Schnittstellen zu externen Systemen und Sprachen.
  3. Interoperabilität: POD-Typen lassen sich einfach zwischen Modulen, Komponenten, Systemen und Programmiersprachen austauschen und erleichtern damit die Interoperabilität.
  4. Einfache Verwendung: POD-Typen sind leicht zu verwenden und zu verstehen, da sie meist grundlegende Datentypen oder Aggregate daraus darstellen.
  5. Performance: Im Vergleich zu komplexeren Datentypen bieten POD-Typen häufig eine bessere Ausführungsgeschwindigkeit und Speichernutzung.
  6. Vorhersagbarkeit: Aufgrund ihrer einfachen und klar definierten Struktur ist das Verhalten von POD-Typen meist besser vorhersehbar, was Debugging und Optimierung erleichtert.

Insgesamt können POD-Typen zu einfacherem, effizienterem und besser wartbarem Code beitragen, insbesondere in performancekritischen oder ressourcenbeschränkten Umgebungen.

Suchen wir in der OpenCV-Codebasis nach Structs, die keine Methoden besitzen und ausschließlich Felder enthalten.

opencv3

Das Ergebnis betrifft 25 % der in OpenCV-Projekten definierten Typen. OpenCV definiert nahezu sein gesamtes Datenmodell in Structs, die nur Felder enthalten.

Mehrfachvererbung vermeiden

Mehrfachvererbung kann ein Design verkomplizieren und das Debugging erschweren. Deshalb empfehlen viele C++-Experten, sie zu vermeiden.

Suchen wir nach Klassen in der OpenCV-Codebasis, die von mehr als einer konkreten Basisklasse erben.

opencv4

Nur wenige Klassen aus Testprojekten verwenden Mehrfachvererbung; in der eigentlichen OpenCV-Codebasis wird dieses Konzept vermieden.

Komplexe Funktionen vermeiden

Zur Erkennung komplexer Funktionen gibt es viele Metriken. NBLinesOfCode, die Anzahl der Parameter und die Anzahl lokaler Variablen gehören zu den grundlegenden.

Weitere interessante Metriken zur Erkennung komplexer Funktionen sind:

  • Die zyklomatische Komplexität ist eine verbreitete prozedurale Softwaremetrik und entspricht der Anzahl möglicher Entscheidungen innerhalb einer Prozedur.
  • Nesting Depth misst bei Methoden die maximale Verschachtelungstiefe eines Scopes im Methodenkörper.
  • Max Nested Loop entspricht der maximalen Verschachtelungstiefe von Schleifen in einer Funktion.

Die maximal akzeptablen Werte dieser Metriken hängen von den Entscheidungen des Teams ab; universelle Grenzwerte gibt es nicht.

Suchen wir in der OpenCV-Codebasis nach Methoden, die als komplex gelten könnten.

opencv5

Nur 1 % sind Kandidaten für ein Refactoring zur Verringerung ihrer Komplexität.

Kopplung

Eine geringe Kopplung ist wünschenswert, weil Änderungen in einem Bereich der Anwendung dann weniger Folgeänderungen im restlichen System erfordern. Langfristig spart dies bei Anpassungen und neuen Features erheblich Zeit, Aufwand und Kosten.

Eine geringe Kopplung lässt sich durch abstrakte Klassen erreichen. Drei wesentliche Vorteile sind:

  • Eine abstrakte Klasse definiert einen Vertrag, der Wiederverwendung fördert. Implementiert ein Objekt eine abstrakte Klasse, muss es diesen Standard erfüllen. Ein Objekt, das ein anderes Objekt verwendet, wird als Consumer bezeichnet. Die abstrakte Klasse bildet den Vertrag zwischen Objekt und Consumer.
  • Eine abstrakte Klasse schafft außerdem eine Abstraktionsebene, die Programme leichter verständlich macht. Entwickler können über das allgemeine Verhalten des Codes sprechen, ohne sich sofort mit Implementierungsdetails beschäftigen zu müssen.
  • Eine abstrakte Klasse erzwingt eine geringe Kopplung zwischen Komponenten und schützt ihren Consumer dadurch vor Implementierungsänderungen in den konkreten Klassen.

Suchen wir nach allen abstrakten Klassen, die OpenCV definiert:

opencv6

Wenn unser Hauptziel eine geringe Kopplung ist, gibt es bei abstrakten Klassen einen häufigen Fehler, der ihren Nutzen zunichtemachen kann: konkrete statt abstrakter Klassen zu referenzieren. Betrachten wir dazu folgendes Beispiel:

Die Klasse A implementiert die abstrakte Klasse IA, die die Methode calculate() enthält. Die Consumer-Klasse C ist wie folgt implementiert:

public class C
{
   ….
   public:
      void calculate()
      {
        …..
        m_a->calculate();
        ….
       }
       A* m_a;
 };

Klasse C referenziert statt der abstrakten Klasse IA die konkrete Klasse A. Dadurch geht der Vorteil der geringen Kopplung verloren. Diese Implementierung hat zwei wesentliche Nachteile:

  • Wenn wir eine andere Implementierung von IA verwenden möchten, müssen wir den Code von Klasse C ändern.
  • Werden A Methoden hinzugefügt, die in IA nicht existieren, und C verwendet diese, verlieren wir auch den Vertragsvorteil der Schnittstelle.

C# führte die explizite Interface-Implementierung ein, um sicherzustellen, dass eine Methode aus IA niemals über eine Referenz auf eine konkrete Klasse, sondern nur über eine Interface-Referenz aufgerufen wird. Diese Technik schützt Entwickler davor, die Vorteile von Interfaces unbeabsichtigt zu verlieren.

Kohäsion

Das Single-Responsibility-Prinzip besagt, dass eine Klasse nicht mehr als einen Grund für Änderungen haben sollte. Eine solche Klasse gilt als kohäsiv. Ein hoher LCOM-Wert weist im Allgemeinen auf eine geringe Kohäsion hin. Es gibt mehrere LCOM-Metriken: LCOM liegt im Bereich [0–1], LCOM HS (Henderson-Sellers) im Bereich [0–2]. Ein LCOM-HS-Wert über 1 sollte als Warnsignal betrachtet werden. Die Metriken werden wie folgt berechnet:

LCOM = 1 – (sum(MF)/M*F)
LCOM HS = (M – sum(MF)/F)(M-1)

Dabei gilt:

  • M ist die Anzahl der Methoden einer Klasse (statische und Instanzmethoden, einschließlich Konstruktoren, Property-Getter/-Setter sowie Add-/Remove-Methoden von Events).
  • F ist die Anzahl der Instanzfelder der Klasse.
  • MF ist die Anzahl der Methoden der Klasse, die auf ein bestimmtes Instanzfeld zugreifen.
  • Sum(MF) ist die Summe von MF über alle Instanzfelder der Klasse.

Die Grundidee dieser Formeln lautet: Eine Klasse ist vollständig kohäsiv, wenn alle Methoden alle Instanzfelder verwenden. Dann gilt sum(MF)=M*F und damit LCOM = 0 sowie LCOMHS = 0.

Ein LCOM-HS-Wert über 1 sollte als Warnsignal betrachtet werden.

opencv8

Nur wenige Typen weisen eine geringe Kohäsion auf.

Fazit

Ein Blick in den OpenCV-Quellcode überrascht durch die Einfachheit seiner Implementierung: keine unnötig fortgeschrittenen Designkonzepte und kein Overengineering – lediglich solide Grundprinzipien, die konsequent angewendet werden.

Diesen Artikel teilen