8. September 2018

Virtualisierung mit KVM auf Debian Stretch 9.5

KVM on Debian

Guten Morgen,

vor einer Weile habe ich auf Ebay ein nettes kleines Micro-ITX-Board mit Pentium G4400 geschossen. In ein geräumiges Gehäuse gepackt, ein paar Festplatten dazu und schon haben wir einen hübschen, effizienten Heimserver. Was könnte man damit bloß anstellen? Virtualisierung!

Einleitung

Vieles in diesem Artikel beschriebene lässt sich auf diverse Linux-Distributionen anwenden. Da es aber einige Unterschiede, bspw. beim Paket-Management gibt, werde ich mich hier auf Debian fokussieren. Entsprechend werden Sie bevor wir beginnen ein Debian 9.5 auf Ihrem System installieren müssen. Sollten Sie dabei Unterstützung brauchen, kann ich das Debian Handbuch empfehlen.

Wenn wir davon reden, eine VM auf KVM zu betreiben, meinen wir meistens eine große Sammlung von Programmen, die größtenteils gar nicht zu KVM gehören:

  • KVM ist die Kernel Virtual Machine, die die Virtualisierungsschicht des Linux Kernels darstellt.
  • qemu ist ein Emulator, der dank seines modularen Aufbaus diverse Hardwareplattformen emulieren kann. In unserem fall, wird die Hardware unserer virtuellen Maschine "emuliert".
  • libvirt ist eine API, die auf qemu und KVM aufsetzt. Sie beinhaltet einen Dienst und verschiedene Werkzeuge, die die Verwaltung von VMs deutlich einfacher machen.
  • virsh ist ein Kommandozeilenprogramm, mit dem libvirt-Instanzen gemanaged werden (z.B. Starten oder Stoppen von VMs).
  • virt-install ist ein Kommandozeilenprogramm, mit dem VMs für libvirt konfiguriert werden können.
  • virt-manager ist ein GUI-Client, der auf libvirt aufbaut. Er erlaubt die Verwaltung von VMs mit einer grafischen Oberfläche. Da ich lieber die Kommandozeilentools verwenden möchte, wird virt-manager nicht Teil dieses Artikels sein.

Ich habe festgestellt, dass viele Tutorials zu KVM und libvirt etwas unklar sind, insbesondere hinsichtlich Benutzern und Zugriffsrechten. Daher möchte ich hier einige Grundlagen abhandeln.

Libvirt kennt zwei verschiedene Betriebsmodi, die anhand Ihrer URL differenziert werden können: mit der URL qemu:///system verbindet man sich zum libvirt-Service (libvirtd), einer systemweiten Session, die üblicherweise mit Root-Berechtigungen läuft. Das ist die Session, mit der sich Tools wie virt-manager normalerweise verbinden. Da libvirtd als Root läuft, hat es alle notwendigen Rechte um auf Netzwerkbrücken, Block-Devies, usw. zuzugreifen. Virtuelle Festplatten, VM-Konfigurationen, etc. werden standardmäßig in /var/lib/libvirt abgelegt. Übrigens können nur VMs in qemu:///system beim Systemstart automatisch hochgefahren werden.

Verwendet man die URL qemu:///session, wird eine neue libvirt-Instanz für den aktuellen Benutzer erstellt. Es ist möglich, mehrere Benutzer-Instanzen parallel auf einem System zu starten. Die Berechtigungen der benutzerspezifischen Instanz richten sich nach dem Benutzer, der die Instanz gestartet hat. VMs, Festplatten, etc. werden im Benutzerverzeichnis gespeichert, sodass hier keine erweiterten Zugriffsrechte notwendig sind. Allerdings ist eine benutzerspezifische Session nicht in der Lage Block-Devices zu verwenden, ohne dass udev-Regeln eingerichtet werden, die dem Benutzer Zugriff auf /dev/[blockdevice] erlauben. Auch die Verwendung von Netzwerkbrücken braucht unter Umständen zusätzliche Zugriffsrechte.

Um herauszufinden, mit welcher Session man gerade verbunden ist, kann man virsh uri ausführen. Die Standard-Session ist normalerweise qemu:///session. Um das zu Ändern, kann man die folgende Umgebungsvariable in der .bashrc einfügen und sich anschließend aus und wieder einloggen (ansonsten muss man --connect qemu:///system an alle virsh und virt-install Befehle anhängen).

export LIBVIRT_DEFAULT_URI="qemu:///system"

Da qemu:///system im Allgemeinen als der Standardmodus angesehen wird, werde ich diese Session verwenden.

Einrichtung

Beginnen wir also mit der Installation der benötigten Pakete:

sudo apt install qemu-kvm libvirt-clients libvirt-daemon-system virtinst

Üblicherweise möchte man in den VMs auf das Netzwerk zugreifen können. Hierzu ist eine Netzwerkbrücke anzulegen, die die Verbindung zum physikalischen Netzwerkadapter herstellt. Passen Sie die Netzwerkkonfiguration folgendermaßen an:

nano /etc/network/interfaces
# The primary network interface
# DHCP muss am physikalischen Adapter deaktiviert werden
allow-hotplug eth0
iface eth0 inet dhcp
iface eth0 inet  manual
iface eth0 inet6 manual

# Virtual network bridge
auto vmbr0
iface vmbr0 inet dhcp
    bridge_ports eth0    # Ersetzen Sie eth0 durch Ihren Adapter
    bridge_stp off
    bridge_maxwait 0
    bridge_fd 0

Starten Sie das System anschließen neu.

Die erste VM erstellen

Mit der Vorbereitung aus dem Weg, können wir nun ein Boot-Image für unsere erste VM herunterladen. Ich werde ein Debian 9.5 netinstall-Image verwenden:

sudo wget "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-9.5.0-amd64-netinst.iso" -O "/var/lib/libvirt/boot/debian-9.5.iso"

Erstellen wir also unsere erste VM:

virt-install --virt-type kvm --name debian9 --memory 512 --cdrom /var/lib/libvirt/boot/debian-9.5.iso --disk size=10 --network bridge=vmbr0 --os-variant debian9

Sie sollten in etwa folgende Ausgabe sehen:

WARNING  Graphics requested but DISPLAY is not set. Not running virt-viewer.
WARNING No console to launch for the guest, defaulting to --wait -1
Starting install...
Allocating 'debian9.qcow2' | 10 GB 00:00:00
Creating domain... | 0 B 00:00:01
Domain installation still in progress. Waiting for installation to complete.

Hier eine Erklärung des obigen Kommandos:

--virt-type kvm: Die VM verwendet KVM

--name debian9: Die VM wird "debian9" genannt. Dieser Name wird angezeigt, wenn man virsh list aufruft. Er wird auch genutzt, wenn man die VM editieren möchte: virsh edit debian9. Beachten Sie, dass es sich NICHT um den Hostnamen der VM handelt.

--memory 512: Wir wollen unserer VM 512 MB RAM geben.

--cdrom /var/...: Das ISO-Image, von dem die VM gebootet werden soll.

--disk size=10: Erstellt neben der VM eine virtuelle Festplatte mit 10 GB Größe. Die resultierende Datei wird in /var/lib/libvirt/images abgelegt und heißt wie die VM.

--network bridge=vmbr0: The VM benutzt unsere zuvor eingerichtete Netzwerkbrücke.

--os-variant debian9: Wir werden in der VM Debian 9 laufen lassen. Dieser Schalter erlaubt verschiedene betriebssystemspezifische Optimierungen. Um eine Liste der möglichen Werte zu erhalten, führen Sie osinfo-query os aus (Sie müssen möglicherweise libosinfo-bin installieren). Der häufig genannte Befehl virt-install --os-variant list funktioniert nicht mehr.

Mit der VM Verbinden

Sie können sich jetzt mit Ihrer VM verbinden um... moment, Sie wissen nicht, wir man sich zu der VM verbinden soll. Hmm... stoppen wir zunächst einmal die Domäneninstallation mittels Ctrl+C. Wir sollten auch die halbfertige VM aufräumen:

virsh destroy debian9
virsh undefine debian9
sudo rm /var/lib/libvirt/images/debian9.qcow2

Es gibt zwei Möglichkeiten, eine VM so auf einem Headless-Server zu initialisieren, dass man ein System installieren kann.

1. Man kann die Konsole der VM im eigenen Terminal zur Verfügung stellen. Allerdings ist diese Funktion Betriebssystemabhängig und steht bspw. in Windows-VVMs nicht zur Verfügung. Zudem gibt es einige nervige Einschränkungen: Das Argument -x kann nicht mit --cdrom kombiniert werden. Früher konnte man ein ISO-Image mit dem Argument -l einbinden, allerdings funktioniert das heute nicht mehr. Stattdessen muss ein gültiger Installationsbaum bereitgestellt werden, was ich nur mit einem Ubuntu-Netinstall-Image geschafft habe:

virt-install --virt-type kvm --name ubuntu --memory 512 -l http://archive.ubuntu.com/ubuntu/dists/bionic/main/installer-amd64/ --disk size=10 --network bridge=vmbr0 --os-variant ubuntu17.10 -x='console=tty0 console=ttyS0,115200n8' --nographics

2. Man macht den Gast über VNC erreichbar. Dies ist die besser Lösung, da sie unabhängig vom Gast-Betriebssystem funktioniert. Außerdem kann eine Desktopumgebung in der VM verwendet werden.

virt-install --virt-type kvm --name debian9 --memory 512 --cdrom /var/lib/libvirt/boot/debian-9.5.iso --disk size=10 --network bridge=vmbr0 --os-variant debian9 --graphics vnc,listen=0.0.0.0 --noautoconsole

Den VNC-Port einer VM kann man ganz einfach herausfinden:

virsh vncdisplay debian9

Starten Sie also ihren VNC-Viewer, geben Sie IP und Port ein und voilà - eine VM mit KVM auf Debian Stretch!

17. June 2018

Ein Wort zu Fremdkomponenten

Guten Morgen,

eine Sache, die in der Programmierergemeinschaft immer wieder gepredigt wird, ist die Wiederverwendung von Code. Idealerweise sollte ein Problem nur und genau einmal gelöst werden. Jeder der dieses Problem antrifft, sollte diese eine Implementierung verwenden und sie erweitern, wenn Änderungen notwendig sind.1 Es gibt drei Hauptpunkte, die diese Idee unterstützen:

Das Gute

  • Weniger Entwicklungszeit, weil große Teile einer Anwendung durch existierenden Code abgedeckt sind.
  • Bibliotheken werden von vielen Programmierern genutzt und bearbeitet, ihre Code-Qualität sollte entsprechend hoch sein. Zudem sollten sie gut getestet sein.
  • Weniger Wartungsaufwand, weil die Bibliotheken von anderen gewartet werden.

Zusätzlich wird oft empfohlen, sich auf Open Source Bibliotheken zu konzentrieren. Wenn notwendig könnte die Bibliothek dann angepasst oder sogar geforked werden. Allerdings ist Open Source eine ganz eigenes Thema, das ich an dieser Stelle nicht im Detail beleuchten möchte. Man sollte aber im Hinterkopf behalten, dass auch Open Source nicht nur Voreile hat.

Viele Jahre lang, war ich ein Anhänger der Idee, Code möglichst oft wieder zu verwenden. Und ich denke weiterhin, dass man eigenen Code immer so schreiben und paketieren sollte, dass er tatsächlich wiederverwendbar ist. Man sollte so eine Reihe qualitativ hochwertiger Bibliotheken für die Verwendung in den eigenen Anwendungen erstellen.

Was Fremdkomponenten betrifft, habe ich mit der Zeit jedoch einige Nachteile entdeckt, auf die ich im Weiteren eingehen möchte.

Das Schlechte

  • Kenne Sie jede dieser 425 Bibliotheken?Jede Bibliothek von nennenswerter Größe hat eine Lernkurve. Meistens wird die Bibliothek anfänglich falsch eingesetzt, möglicherweise sogar entgegen ihres Designs. Das führt zu schlechtem Anwendungsdesign und, im schlimmsten Fall, zur Einbindung weiterer Bibliotheken, die die selben Probleme lösen sollen. Niemand möchte solchen Code warten.
  • Bibliotheken werden häufig in mehr oder minder zufälliger Art "zusammengesteckt". Drittherstellerbibliotheken zu verwenden bedarf großer Aufmerksamkeit, gerade hinsichtlich der Architektur und der korrekten Einbindung aller Bibliotheken. Da die Architektur ohnehin ein schwieriges Thema in vielen Anwendungen ist, macht "zusamenpappen" diverser Bibliotheken nur noch mehr ärger.
  • Die meisten Bibliotheken stellen viel mehr Funktionen bereit, als wir brauchen. Man mag sagen: "Was ist so schlimm daran, mehr Features zu haben, als wir brauchen?". Nun, diese Features sind Overhead. Code, den wir zum Kunden ausrollen müssen, ohne dass dieser ihn haben will. Dinge, die zu einer steileren Lernkurve führen, weil wir sämtliche Funktionen verstehen müssen, um beurteilen zu können, ob wir sie einsetzen willen oder nicht.
  • Und dann gibt es da noch diese nervigen Entwickler; meiner Erfahrung nach, wird jede Funktion, die irgendwo versteckt ist, auch irgendwann benutzt. Das ist besonders schädlich, wenn wir der Meinung sind, dass die spezifische Funktion schlecht ist. Lassen Sie mich ein Beispiel geben:
    Ich bin ein Fan von Dependency Injection. Für .NET gibt es in diesem Bereich verschiedene gute Bibliotheken, LightInjectNinjectAutofac, etc. Ich habe aber meine eigene geschrieben.2 Warum?
    Ich bin der Meinung, dass Dependency Injection eine ganz klare Beschreibung der Abhängigkeiten einer Klasse erfordert. Und diese Beschreibung lässt sich am besten über einen Konstruktor liefern, der die Bereitstellung aller Abhängigkeiten erzwingt. Ich möchte daher ausschließlich Constructor Injection verwenden. Alle Bibliotheken, die ich genannt habe, ermöglichen Property Injection. Was schön für einen Programmierer ist, weil weniger Code gebraucht wird. Der Nachteil ist, dass ich das Objekt dann instanzieren kann, ohne die Properties zu setzen. Wollen Sie in jeder einzelnen Methode sicherstellen, dass vermeintlich notwendige Abhängigkeiten möglicherweise nicht da sind? I nicht! Deshalb unterstütz mein DI-Container ausschließlich Constructor Injection, nicht mehr, nicht weniger.
  • Üblicherweise stolpern wir über einige Anwendungsfälle für eine Bibliothek, die so nicht bedacht wurden. Wir beginnen dann meistens, die Bibliothek zu erweitern. Das geschieht mit Wrappern, eigenen Ableitungen, Extension Methods, usw. Auch wenn sich das erstmal nicht so schlimm anhört, musste ich feststellen, dass diese Anbauten in den meisten Fällen zu extrem schlechtem Code und zu furchtbarer Architektur führen.
    Dass Problem ist, dass wir bei der Auswahl einer Bibliothek erst einmal nicht erwarten, dass ein Feature, was wir benötigen, nicht vorhanden ist.3 Wenn wir also eine Funktion vermissen, sind wir meistens unter Zeitdruck sie schnellstens einzubauen. Das Ganze wird noch dadurch verschärft, dass viele Bibliotheken nicht gerade leicht von Außen zu erweitern sind.4 Sie funktionieren gut für den Einsatzzweck, für den sie geschrieben wurden, machen aber Probleme, wenn etwas anderes gebraucht wird.
    An diesem Punkt wird häufig das Open Source Argument gebracht. Man könnte das vermisste Feature einfach zur Bibliothek hinzufügen. Aber widerspricht das nicht unseren ursprünglichen Zielen? Wenn ich eine Drittherstellerbibliothek verwende, möchte ich die Verantwortung den Code zu Warten und zu Erweitern los werden. Ich will den Code nicht verstehen, ich will dass er einfach funktioniert. Was ich also sicherlich nicht möchte, ist mir die Hände dreckig machen zu müssen, um zusätzliche Funktionen in eine Bibliothek einzubauen, oder sie sogar zu forken.

Das Hässliche

Sie fragen mich also "Sollte ich jetzt Fremdkomponenten verwenden, oder nicht?". Meine Antwort ist klar und eindeutig: "Das kommt darauf an.". Wie es im Leben so ist, gibt es auch hier keine einfache Antwort, keinen eindeutigen, richtigen Weg. Was ich anbieten kann, ist aber folgendes: Lassen Sie uns einige Fragen beleuchten, die man sich stellen sollte, bevor man eine Fremdkomponente verwendet:

  1. Sind Sie der Meinung, dass sie ein Stück wartbare, gut getestete, wiederverwendbare Software schreiben können, um ein spezifisches Problem zu lösen? Ist die Antwort "Nein", versuchen Sie es erst gar nicht. Investieren Sie Ihre Zeit darin, die beste Bibliothek da draußen zu finden, die Ihren Anforderungen gerecht wird.
  2. Kennen Sie eine Bibliothek (mit lang währender Erfahrung), die Ihr Problem löst? Wenn Sie oder einer Ihrer erfahreneren Entwickler eine solche Bibliothek kennen, nutzen Sie diese.5
  3. Glauben (im Unterschied zu "wissen") Sie, eine Bibliothek gefunden zu haben, die Ihr Problem lösen würde? Hier wird es schwierig. Versuchen Sie Alles über die Bibliothek herauszufinden. Probieren Sie sie aus, testen Sie die Bibliothek bis aufs Blut. Versuchen Sie dabei alle Anwendungsfälle, die Sie haben werden (und einige, die Sie lieber nicht haben wollen) zu beleuchten. Und legen Sie besonderen Wert auf die korrekte Verwendung der Bibliothek in Ihrer Anwendung.
  4. Wenn keiner der genannten Punkte auf Sie zutrifft; damit Meine ich, dass sie keine passende Bibliothek gefunden haben, dass Sie die Lernkurve als zu steil ansehen und dass Sie Bauchschmerzen von den ganzen Features kriegen, die Sie lieber nicht in Ihrer Anwendung verwenden wollen. Dann sollten Sie sich überlegen eine eigene Bibliothek zu schreiben. Natürlich gelten die üblichen Regeln: Machen Sie die Bibliothek wiederverwendbar. Implementieren Sie automatische Tests. Legen Sie ganz besonderen Fokus auf die Schnittstelle und die Gesamtarchitektur der Bibliothek. Ihre Bibliotheken sollten einen noch höheren Code-Standard haben, als Ihre Anwendungen.

In jedem Fall, sollten Sie nur Drittherstellerbibliotheken verwenden, von denen Sie überzeugt sind. Stellen Sie sicher, dass die Lizenz akzeptabel ist, dass die Bibliothek gut gewartet wird und aktiv von einer größeren Gruppe von Programmierern weiterentwickelt wird, dass der Code ein hohe Qualität aufweist und gut getestet ist. Und schauen Sie nicht auf den Preis.6 Für eine Anwendung sind verwendete Bibliotheken üblicherweise eine Investition auf Lebenszeit. Lassen Sie sich nicht dazu hinreißen, eine "mögliche Alternative" der "perfekten Bibliothek" vorzuziehen, weil es sie kostenlos gibt. Solche Entscheidungen werden Sie zehnfach bei dem Wartungsaufwand heimsuchen.

Ich hoffe, dass ich Ihnen einige nützliche Hinweise zu der Auswahl von Fremdkomponenten geben konnte. Fällen Sie keine hastigen Entscheidungen. Überlegen Sie sich genau, welche Lösung die beste für Ihr Problem ist, und bleiben SIe bei Ihrer Antwort. Ich bin überzeugt, dass Sie dann mit Ihren Entscheidungen erfolgreich sein werden.

  1. Das ist in dieser Form natürlich unmöglich, weil es immer mehrere Wege gibt, ein komplexes Problem zu lösen. Und keiner davon ist perfekt. Das Ziel ist aber weiterhin gültig.
  2. Die man hier finden kann: https://github.com/programmersdigest/Injector
  3. Obwohl wir genau das erwarten sollten! Erwarten Sie nicht, dass eine Bibliothek all Ihre Bedürfnisse abdecken wird. Denken Sie voraus. Implementieren Sie Möglichkeiten, um weitere Funktionalität zu einer Bibliothek hinzuzufügen, bevor sie in Verlegenheit kommen, es tun zu müssen. Sie werden es schneller brauchen, als Sie erwarten.
  4. Ich schaue Dich an, Microsoft, mit deinen sealed classes.
  5. Natürlich sollte man die Bibliothek trotzdem prüfen, testen und so weiter. Einen Entwickler an der Hand zu haben, der Ihre Bedürfnisse gut kennt, dessen Meinung Sie vertrauen, der viel Erfahrung mit der spezifischen Bibliothek hat und der voraussichtlich noch lange Teil Ihres Teams sein wird, ist eine zielich gute Ausgangssituation.
  6. Diese Argument gilt hauptsächlich für Firmen. Für einen privaten Entwickler spielt der Preis sicherlich eine Rolle (das ist bei mir nicht anders). Andererseits ist das Risiko einer falschen Entscheidung in diesem Fall ja auch viel geringer, als bei einer Firma, in der mehrere zig oder gar hundert Mitarbeiter an der Anwendung beteiligt sind.
10. June 2018

LINQ: .NET Collection-Klassen

Guten Morgen,

Various collectionsLINQ ist eine der interessantes Funktionen von C#. Immer, wenn man mit Daten in Listen arbeitet (was meistens der Fall ist), werden Methoden gebraucht, diese Daten zu filtern und zu manipulieren. LINQ ermöglicht a) einfachen und konsistenten Zugriff auf Listen, und b) einen Funktionalen Ansatz, mit Listen zu Arbeiten.

Dieser Artikel ist der Erste in einer Reihe von Artikeln zu LINQ. In dieser Reihe werden wir

  • uns die Collection-Klassen von .NET anschauen
  • über Lambda-Expressions, Closures, Action<> und Func<> lernen
  • uns die Macht von Extension-Methods zu Nutze machen
  • das IEnumerable<>-Interface verwenden
  • in die diversen Methoden von LINQ eintauchen
  • und uns an PLINQ (parallel LINQ) versuchen.

Also, fangen wir direkt an, die mit den verschiedenen Collection-Klassen, die in .NET Framework1 zur Verfügung stehen. Da die meisten Business-Anwendungen rein datengetrieben sind (an wie vielen richtigen Programmen ohne Datenbank haben Sie bisher gearbeitet?), ist Wissen über die Collection-Klassen einer Sprache meiner Meinung nach ähnlich grundlegend, wie Wissen über die Basis-Datentypen der jeweiligen Sprache.

Die wichtigsten Collections in .NET finden sich im Namespace System.Collections.Generic. Ihre nicht-generischen Equivalente aus dem Namespace System.Collections sollten nicht verwendet werden. Sie existieren nur noch für Abwärtskompatibilität. Concurrent Collections (concurrent = nebenläufig; in diesem Zusammenhang: multithreading-fähig) können im Namespace System.Collections.Concurrent gefunden werden.

Array / List<T>

Ein Array (bzw. eine List<T>, die im Grunde nur ein Wrapper um Array ist), ist in C# ziemlich genau das, was man von einem Array in jeder anderen Sprache erwarten würde: Eine Sammlung von Elementen, die einen zusammenhängenden Block im RAM belegt. Zugriff mittels Index ist daher sehr schnell. Einträge anhand ihrer Attribute zu suchen, erfordert aber einen vollständigen Scan des Arrays.

Einem Array Einträge hinzu zu fügen, ist normalerweise günstig, da C# das RAM in Blöcken reserviert. Nur Einfügen und Löschen von Elementen in der Mitte des Arrays ist etwas teurer, weil alle nachfolgenden Einträge verschoben werden müssen. Ist das Ihr Anwendungsfall, sollten Sie sich die LinkedList<T> anschauen (siehe unten).

HashSet<T>

Wie der Name vermuten lässt, speichert das HashSet<T> einen Hash für jeden Eintrag in der Collection. Im Gegensatz zu einem Array, garantiert das HashSet<T> aber nicht, dass die Reihenfolge der Einträge erhalten bleibt. Außerdem kann jeder Eintrag nur ein einziges Mal vorkommen, womit das HashSet<T> optimal geeignet ist, wenn jeder Eintrag in der Collection eindeutig sein muss. Ob Einträge gleich sind, wird über die Equals()-Methode der Einträge bestimmt.2

Das HashSet<T> ist fantastisch, wenn ein Eintrag mittels einer Instanz (oder eher: eines "Dings", was als identisch angesehen wird) gefunden werden soll. Ein gutes Beispiel ist eine Zufällige Liste von Zeichenfolgen. Da identische (in Hinsicht auf ihren Inhalt) Zeichenfolgen als gleich angesehen werden, ist die Konvertierung einer List<string> in ein HashSet<string> eine simple Variante, jeden String in der Collection eindeutig zu machen.

var list = new List<string> {
    "One", "Two", "Three", "Two", "Three"
};
var set = new HashSet<string>(list); // Output: "One", "Two", "Three"

Dictionary<T, U>

Das Dictionary<T, U> ist eine Key-Value-Collection. Jeder Schlüssel (vom Typ T) ist einem Eintrag (vom Typ U) zugeordnet. Zugriff über den Schlüssel ist sehr effizient, was das Dictionary<T, U> exzellent macht, um Einträge über ein Attribut (wie den Primärschlüssel) zu finden.

Die Schlüssel eines Dictionary<T, U> müssen eindeutig sein. Im Gegensatz zu dem HashSet<T>, was doppelte Einträge einfach ignoriert, wirft ein Dictionary<T, U> eine Exception, wenn ein Schlüssel zwei Mal hinzugefügt wird. Analog zu dem HashSet<T> wird die Gleichheit von Schlüsseln über die Equals()-Methode geprüft.

Wann immer man mehrfach über eine Liste iteriert, um einen einzelnen Eintrag über ein bestimmtes Feld zu finden (bspw. eine Person in einer List<Person> über ihren Namen), sollte man sich überlegen, ein Dictionary<T, U> zu verwenden. Selbst bei ein paar Iterationen kann es sich lohnen, temporär ein Dictionary<T, U> zu erstellen. LINQs ToDictionary()-Method hilft hier weiter.3

ConcurrentDictionary<T, U>

Alle Concurrent-Collections erlauben paralleles Lesen und Schreiben von mehreren Threads. Da sie üblicherweise eine Kopie der Collection auf jedem Thread halten müssen, und die Kopien synchronisiert werden müssen, ist es nicht ratsam Concurrent Collections in einem nicht-parallelen Anwendungsfall zu benutzen. Sie sind aber mächtige Werkzeuge in hochgradig parallelen Szenarien.

Die nützlichste Concurrent-Collection ist das ConcurrentDictionary<T, U>. Es stellt eine thread-sichere Implementierung des Dictionary<T, U> dar und ist besonders hilfreich bei der Zwischenspeicherung von Daten oder bei Multi-Threading-fähigen Service-Implementierungen.

Auch wenn alle Concurrent-Collections grundlegend thread-safe sind, ist nicht sichergestellt, dass alle Operationen auf diese Collections ebenfalls thread-sicher sind. Die Dokumentation schließt Thread-Sicherheit sogar aus für explizite Interface-Implementierungen, Extension-Methods (muss jemand an LINQ denken?) und Methoden, die Delegaten als Parameter annehmen.4 Bitte prüfen Sie immer die Dokumentation der jeweiligen Methode, insbesondere wenn Sie LINQ auf eine Concurrent-Collection anwenden.

Andere nützliche Collections

Natürlich gibt es noch eine Reihe hilfreicher, aber speziellerer Collections:

  • Stack<T>: Eine "first in last out" (FILO) Collection
  • Queue<T>: Eine "first in first out" (FIFO) Collection
  • LinkedList<T>: Eine typische "Linked List". Nützlich, wenn man häufig Einträge in der Mitte einer größeren Collection hinzufügen oder entfernen möchte.
  • SortedList<T>, SortedSet<T>, SortedDictionary<T>: Varianten von List<T>, HashSet<T> und Dictionary<T, U>, die die beinhalteten Einträge sortieren.5
  • Die anderen Collections im Namespace System.Collections.Concurrent: ConcurrentStack<T> und ConcurrentQueue<T> sind nebenläufige Implementierungen von Stack<T> und Queue<T>. ConcurrentBag<T> ist etwas speziell, da es tatsächlich einen Sack von Einträgen darstellt: Einträge sind unsortiert und können weder mittels Index noch per Instanz abgefragt werden. Man stopft Sachen hinein und iteriert über Alles, wenn man einen Eintrag herausholen will (bin ich der Einzige, der an seine Schubladen denken muss?).
  • Die verschiedenen Collections aus dem Namespace System.Collections.ObjectModel: Sie werden üblicherweise für UI-Entwicklung verwendet. Besonders die ObservableCollection<T>, die INotifyCollectionChanged implementiert und häufig für DataBindings in WPF genutzt wird.
  • Die brandneuen ImmutableCollections aus dem NuGet-Package System.Collections.Immutable:6 Die Idea ist einfach: Collections, die nicht verändert werden können, können problemlos von mehreren Threads gelesen werden. Es stehen Implementierungen für alle üblichen Collektions zur Verfügung, ImmutableList<T>, ImmutableDictionary<T, U>, ImmutableQueue<T>, usw. Obwohl diese Collections nicht direkt Teil von .NET sind, wollte ich sie erwähnt haben, weil sie eine weitere Möglichkeit schaffen, Collections von mehreren Threads aus zu verwenden.7
6. June 2018

Microsoft kauft GitHub

Guten Morgen,

GitHub Octocat

es hat rumort im Internetz, jetzt ist es bestätigt: Microsoft hat GitHub gekauft1 - und die Community ist in Panik. Viele OpenSource-Projekte befürchten, dass Microsoft seinen Einfluss nutzen wird, um Ihnen zu schaden und die eigene Agenda durchzudrücken. Schauen wir uns die Vor- und Nachteile, sowie die Intentionen der Beteiligten einmal genauer an.

Warum sollte Microsoft GitHub kaufen?

Ich glaube die Antwort ist einfach: Microsoft hat GitHub seit vielen Jahren als Code-Hosting-Plattform genutzt. Sie haben einige ihrer größten Projekte 2014/2015 auf GitHub umgezogen,2 und damit die Schließung ihrer eigenen Code-Hosting-Plattform CodePlex vorbereitet.3 Heute hat Microsoft in seinem Haupt-Account über 1800 Repositories auf GitHub. Darüberhinaus nutz Microsoft Git mittlerweile als Haupt-Versionsverwaltung, und hat den gesamten Windows-Quellcode in das "größte Git Repo auf dem Planeten" umgezogen.4 Strategisch macht es meiner Meinung nach absolut Sinn für Microsoft, GitHub zu kaufen. Dieser Schritt erlaubt es ihnen, ihre meistgenutzte Code-Hosting-Plattform an die eigenen Bedürfnisse anzupassen. Und an die Bedürfnisse ihrer Kunden (= Entwickler auf den Microsoft Plattformen).

Warum sollte GitHub verkauft werden?

2012 hat Chris Wanstrath, einer der Gründer von GitHub, seinen Platz als CEO geräumt, um Tom Preston-Werner (einem weiteren Mitgründer) die Führung zu überlassen. 2014 wurde Chris Wanstrath dann erneut CEO, wollte diesen Posten 2017 aber wieder abgeben (Mitte 2018, hatte GitHub noch immer keinen Ersatz für ihn gefunden). Die Firma hatte zu dieser Zeit einen Jahresumsatz von 200 Millionen Dollar und einen Marktwert von geschätzt 2 Milliarden Dollar.5 Sie hat aber noch nie einen Pfennig Gewinn gemacht. Wenn Sie in dieser Situation wären und Microsoft würde sagen: "Ihre Firma ist 2 Milliarden Dollar wert, wir geben Ihnen 7,5 Milliarden dafür (und haben einen guten CEO dafür)", was wäre Ihre Antwort? Ich weiß, dass ich verkaufen würde!

Und was ist mit uns?

Microsoft logo

Was heißt das alles für uns, Benutzer von GitHub? Ich bin der Meinung, dass Microsoft GitHub nicht umkrempeln wird. Es wird Veränderungen geben, das ist klar, und einige werden besser sein, andere eher nicht. Insgesamt denke ich aber, dass GitHub jetzt endlich eine stabile Firma geworden ist (und damit eine verlässliche Plattform). Microsoft ist nicht darauf angewiesen, dass GitHub irgendwann einmal Gewinn erwirtschaftet. Sie brauchen Entwickler, die ihre Produkte nutzen, sie brauchen eine Community. Und das ist es, was sie sich von diesem Kauf erhoffen. Ich bin außerdem froh zu sehen, dass GitHub wieder einen guten CEO hat. Nicht dass Wanstrath nicht gut war (er hat GitHub zu dem gemacht, was es heute ist), er wollte aber nicht weiter machen und es war kein Ersatz in Sicht. Nun, dieses Problem ist jetzt gelöst.

Alles in Allem freue ich mich auf die Entwicklung von GitHub als Teil von Microsoft. Natürlich gibt es Risiken, es gibt aber auch große Chancen für GitHub, noch besser zu werden. Ich hoffe, dass wir letztere zu genießen kriegen.

27. May 2018

Die Herausforderung der Anpassbarkeit

Guten Morgen,

in der Softwareentwicklung wird zwischen Produkten und Projekten unterschieden. Die Unterscheidung ist folgende:1

  • Ein Produkt wird für den Markt entwickelt. Die Sofwarefirma investiert Geld in die Entwicklung eines Produktes mit dem Ziel, es einer großen Anzahl von Kunden zu verkaufen. Investieren meint in diesem Sinn Geld vorschießen, was vielleicht Gewinne bringt, wenn das Produkt fertig ist. Ein Produkt lockt daher mit gutem Gewinn (einmal Schreiben, häufig Verkaufen), birgt aber auch beträchtliche Risiken (der Markt interessiert sich nicht für das Produkt).
  • Ein Projekt wird für einen einzigen Kunden entsprechend seiner Bedürfnisse entwickelt. Üblicherweise muss die Softwarefirma auch hier das notwendige Budget für die Entwicklung vorlegen. Das Risiko ist aber deutlich geringer, da es mit Sicherheit einen Kunden gibt, der später die Entwicklungskosten der Software tragen wird. Der Gewinn ist andererseits aber auch niedriger, weil die Projektsoftware nicht an andere Kunden verkauft werden kann.

Viele Softwarefirmen haben Ihren Platz in der Mitte zwischen beiden Varianten gefunden: Sie bieten am Markt ein Standardprodukt an, schneiden dieses aber auch für die jeweiligen Kundenbedürfnisse zu. Dieses "Zuschneiden" wird üblicherweise Anpassung (oder dengl. Customizing) genannt - und ist eine der größten Herausforderungen beim Design eines Produkts.

In meinen Jahren als Softwareentwickler habe ich diverse Produkte angetroffen, die vielfach angepasst wurden. Und die meisten hatten mindestens eins der folgenden Probleme:

  • Alles ist private
    Dieser Umstand kann für einen Entwickler, der ein Produkt anpassen soll, extrem frustrierend sein. Das Produkt bietet nur wenige Einstiegspunkte, die nicht die benötigte Freiheit bieten. An jeder Ecke muss das Produktentwicklungs-Team gebeten werden, einen weiteren Einstiegspunkt zu definieren, oder noch eine Methode auf public umzustellen. Das kann ein zwei-Tage-Projekt schnell zu einer mehrwöchigen Odyssee machen.
  • Alles ist public
    Obwohl dieser Punkt das genaue Gegenteil des vorherigen ist, ist er kein Stück besser für eine Firma. Wenn irgendeine Methode public ist, wird sie jemand benutzen (und es vergessen). Eine öffentliche Schnittstelle kann niemals geändert werden, ohne die Sorge, dass eine Anpassung kaputt geht. Und wenn diese Anpassung wichtig war, kann aus einer kleinen Produktänderung ganz schnell ein politischer Alptraum werden. Das Resultat ist eine Sackgasse: wenn alles public ist, kann nichts mehr geändert werden. Die Produktentwicklung kommt zum Erliegen.
  • Die Produktarchitektur ist ein Chaos
    Eine chaotische Architektur ist schon für die Produktentwicklung schlimm genug. Ab einem gewissen Punkt, wird es immer schwieriger, eine schlechte Code-Basis zu verändern. Das Risiko, bestehende Funktionalität zu beschädigen, wird immer größer. Wie soll jemand eine komplexe kundenspezifische Funktion in eine solche Code-Basis einbauen, ohne etwas kaputt zu machen? Und wir soll man so eine Anpassung umsetzen, ohne dass sie noch schlechter wird als der bereits jetzt schlechte Code? Noch schlimmer: Kunden wollen manchmal Anpassungen auf Basis bestehender Anpassungen haben. Das ist wie ein schlechtes Essen mit miserabler Sauce und noch ekelhafterer Würzung.
  • Eine ausgezeichnete Vertriebsabteilung
    Einige Verkäufer können so ziemlich alles an den Mann bringen - und tun es auch.2 Und dass obwohl den Entwicklern die Haare zu Berge stehen, weil sie die Anforderungen kaum "hingebastelt" kriegen. Das resultiert häufig in Workarounds mit den seltsamsten Seiteneffekten.
  • Der "Produktaufkleber" wird auf Anpassungen geklebt
    Je nach Vertragswerk mit dem Kunden, kann eine Softwarefirma Anpassungen in das Produkt aufnehmen. Das ist super, wenn eine Anpassung wohlüberlegt als neue Funktion in das Produkt eingefügt wird. Wenn jedoch eine Anpassung nach der anderen einfach in das Produkt "reingeklatscht" wird, wird das Produkt nach einer Weile sehr wahrscheinlich chaotisch. Der Grund ist schnell erklärt: Eine Anpassung muss üblicherweise nicht alle Funktionen des Produkts berücksichtigen. Es ist klar, welche Funktionen der Kunde benötigt und einsetzt. Um die Kosten für die Anpassung niedrig zu halten, werden nur die notwendigen Änderungen gemacht. Warum sollte man eine Anpassung mit Funktion x kompatibel machen, wenn der Kunde diese Funktion gar nicht benutzt? Diese Anpassung einfach so in den Produktcode zu übernehmen führt aber zu einem Problem: Ein anderer Kunde wird die Funktion x sehr wohl verwenden, leider ist sie aber inkompatibel.

Viele dieser Herausforderungen können nur mit viel Selbstbeherrschung und guter Voraussicht angegangen werden. Allerdings gibt es verschiedene Wege den einzelnen Problemen zu begegnen:

  • Bei der Entwicklung eines neuen Produkts muss klar sein, ob die Software anpassbar sein muss, oder nicht. Im Zweifelsfall sollte die Antwort ja lauten.3
  • Jede Software muss eine klare Architektur haben. Dies gilt besonders, wenn andere die Software erweitern können sollen. Ich persönlich bin der Meinung, dass die genaue Ausprägung der Architektur nicht so wichtig ist, wie die konsistente Einhaltung dieser Architektur. Wenn ein neues Team-Mitglied (egal ob in der Produkt- oder Projektentwicklung) einen Teil der Anwendung verstanden hat, sollte dieses Wissen auf alle anderen Teile der Software übertragbar sein.
  • Überlegen Sie die Implementierung eines Plugin-Systems Plugins sind ein mächtiges Werkzeug für Anpassbarkeit. Im Extremfall kann die gesamte Funktionalität eines Produkts in mehrere Schichten von Plugins gepackt sein, womit alles für den Kunden angepasst werden kann (allerdings wird die Architektur dann schwieriger zu verstehen).
  • Verwenden Sie automatisierte (Regressions-)Tests. Automatisiertes Testen hilft nicht nur bei der Entwicklung des Produkts oder der spezifischen Anpassung. Der tatsächliche Nutzen wird erst ersichtlich, wenn automatisierte Regressionstests auf einmal für das Produkt und alle seine Anpassungen durchgeführt werden können. Immer wenn die Produktentwicklung (mal wieder) die Schnittstelle kaputt gemacht hat, wird der Fehler durch automatisierte Tests gefunden, bevor die Änderung zum Kunden ausgerollt wird. Das verhilft dem Produktentwickler, wie auch dem Projektentwickler zu ruhigeren Nächten, weil beide ziemlich sicher sein können, dass ihr Zeug auch am nächsten Tag noch funktioniert.
  • Während der Umsetzung der Anpassung sollten Produkt-Features, die nicht kompatibel sein werden, berücksichtigt und dokumentiert4 werden. Sollte später jemand die Anpassung in das Produkt übernehmen, ist die Chance damit höher, dass inkompatible Funktionen entdeckt werden.
  • Es sollte nicht alles public/private gemacht werden. Für jeden Teil der Anwendung muss bedacht werden, ob Anpassbarkeit notwendig ist oder nicht. Alles was anpassbar sein soll, muss eine klare öffentliche Schnittstelle (im Grunde eine API) besitzen. Diese Schnittstelle muss derart gestaltet sein, dass a) sämtliche notwendige Funktionalität vorhanden ist und b) die Schnittstelle um Funktionalität erweitert werden kann, die wir in a) vergessen haben.5 Wenn die Produktentwicklung sich unsicher ist, welche Teile der Schnittstelle öffentlich sein müssen, sollte gewartet werden, bis die erstem Anpassungen umgesetzt werden. Meiner Erfahrung nach ist es besser zu viel private statt public zu machen. Allerdings muss es einen einfachen Weg für Projektentwickler geben, die benötigte öffentliche Schnittstelle zu erhalten, wenn sie gebraucht wird. Das führt mich zum nächsten und wichtigsten Punkt:
  • Reden Sie mit Ihren Kollegen (Kaffeemaschine sind super dafür). Meiner Erfahrung nach, gibt es häufig Mauern6 zwischen Produktentwicklung und Projektentwicklung. Insbesondere wenn es sich dabei um unterschiedliche Abteilungen handelt. Allerdings wissen nur die Mitarbeiter der Customizing-Abteilung, was sie wirklich brauchen.

Meiner Meinung nach ist Anpassbarkeit eine riesige Herausforderung. Customizing ist eine der größten Einnahmequellen vieler Software-Unternehmen, aber es gibt keinen einen Weg, sie korrekt umzusetzen. Trotzdem hoffe ich, hier einige Denkanstöße gegeben zu haben, die die Arbeit von Produkt- und Projektentwicklung gleichermaßen verbessern.

  1. Das ist natürlich eine vereinfachte Unterscheidung. Wie üblich gibt es in der Realität viele Übergänge zwischen Produkt und Projekt. Eine Softwarefirma könnte bspw. eine Anpassung vergünstigt anbieten, dafür aber die Erlaubnis vom Kunden einholen, die Anpassung auch anderen Kunden zu verkaufen.
  2. Diese Anmerkung sollte nicht zu ernst genommen werden. Wie so oft, sind weder der Verkäufer, noch die Softwareentwicklung hier "schuld". Es gibt aber Anpassungen, die besser nicht gemacht worden wären. Und bessere Kommunikation zwischen den Abteilungen kann Wunder bewirken, um solche Missstände zu vermeiden.
  3. Zugelassene Antworten sind: ja und vielleicht.
    4 Diese Dokumentation könnte sogar in Form eines Tests abgebildet werden: Wenn Funktion x aktiv ist, schlägt der Test mit einer passenden Fehlermeldung fehl.
  4. Meines Erachtens nach ist das eine der größten Schwierigkeiten. Eine gute Lösung für dieses Problem ist eine Kombination aus Plugin-System und Dependency Injection. Plugins kriegen ihre Abhängigkeiten übergeben, die wiederum eine leicht zu erweiternde öffentliche Schnittstelle haben. Allerdings kann man mit diesem Thema Bücher füllen, es kann hier also nicht vollständig behandelt werden.
  5. Die Größe dieser Mauer kann von Gartenzaun bis zu Chinesischer Mauer reichen. Nur offene Kommunikation kann zu guter Software führen. Insbesondere wenn die Software noch nicht am Markt etabliert ist.