10 3 5 15 8 13 4 24 18 19 Bachelorstudiengang Informatik/IT-Sicherheit Programmierkonzepte [ProgKonz] Autoren: Prof. Dr. Felix Freiling Philipp Klein Hochschule Erlangen-Nürnberg Modul Programmierkonzepte [ProgKonz] Studienbrief 6: Coding Conventions und Best Practices Autoren: Prof. Dr. Felix Freiling Philipp Klein 1. Auflage Hochschule Erlangen-Nürnberg © 2015 Hochschule Erlangen-Nürnberg Lehrstuhl für Informatik 1 IT-Sicherheitsinfrastrukturen Martensstr. 3 91058 Erlangen 1. Auflage (3. Dezember 2015) Das Werk einschließlich seiner Teile ist urheberrechtlich geschützt. Jede Verwendung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung der Verfasser unzulässig und strafbar. Das gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen. Um die Lesbarkeit zu vereinfachen, wird auf die zusätzliche Formulierung der weiblichen Form bei Personenbezeichnungen verzichtet. Wir weisen deshalb darauf hin, dass die Verwendung der männlichen Form explizit als geschlechtsunabhängig verstanden werden soll. Das diesem Bericht zugrundeliegende Vorhaben wurde mit Mitteln des Bundesministeriums für Bildung, und Forschung unter dem Förderkennzeichen 160H11068 gefördert. Die Verantwortung für den Inhalt dieser Veröffentlichung liegt beim Autor. Inhaltsverzeichnis Seite 3 Inhaltsverzeichnis Einleitung zu den Studienbriefen I. Abkürzungen der Randsymbole und Farbkodierungen . . . . . . . . . II. Zu den Autoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . III. Modullehrziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Studienbrief 6 Coding Conventions und Best Practices 6.1 Lernergebnisse . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Advance Organzier . . . . . . . . . . . . . . . . . . . . . . 6.3 Coding Conventions . . . . . . . . . . . . . . . . . . . . . 6.3.1 Namenskonventionen . . . . . . . . . . . . . . . . 6.3.2 Klammersetzung . . . . . . . . . . . . . . . . . . . 6.3.3 Einrückung . . . . . . . . . . . . . . . . . . . . . . 6.3.4 Zeilenlänge . . . . . . . . . . . . . . . . . . . . . . 6.3.5 Zeilenumbrüche . . . . . . . . . . . . . . . . . . . 6.3.6 Encoding . . . . . . . . . . . . . . . . . . . . . . . 6.3.7 Sprache . . . . . . . . . . . . . . . . . . . . . . . 6.4 Dokumentation . . . . . . . . . . . . . . . . . . . . . . . . 6.4.1 Kommentare . . . . . . . . . . . . . . . . . . . . . 6.5 Best Practices . . . . . . . . . . . . . . . . . . . . . . . . . 6.5.1 Testing . . . . . . . . . . . . . . . . . . . . . . . . 6.5.2 Refactoring . . . . . . . . . . . . . . . . . . . . . . 6.5.3 Don’t repeat yourself (DRY) . . . . . . . . . . . . . 6.5.4 Methods should do one thing (nach Martin [2008] 6.5.5 Magic Numbers . . . . . . . . . . . . . . . . . . . 6.5.6 Strings bauen mit StringBuilder . . . . . . . . . . . 6.5.7 Wenn es sicher sein soll: SecureRandom . . . . . . 6.5.8 Eindeutigkeit . . . . . . . . . . . . . . . . . . . . . 6.5.9 Das Rad nicht neu erfinden . . . . . . . . . . . . . 6.5.10 Usereingaben validieren . . . . . . . . . . . . . . 6.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . 6.7 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 5 6 11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Liste der Lösungen zu den Kontrollaufgaben Verzeichnisse I. Beispiele . . . . II. Exkurse . . . . . III. Kontrollaufgaben IV. Tabellen . . . . . V. Literatur . . . . . 4 11 11 11 12 17 17 18 19 20 21 22 22 27 28 28 29 30 32 33 34 35 36 37 40 41 47 49 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 49 49 49 49 Liste der Lösungen zu den Übungen 51 Anhang 57 Seite 4 Einleitung zu den Studienbriefen Einleitung zu den Studienbriefen I. Abkürzungen der Randsymbole und Farbkodierungen Beispiel B Exkurs E Kontrollaufgabe K Merksatz M Quelltext Q Übung Ü Zu den Autoren Seite 5 II. Zu den Autoren Felix Freiling ist seit Dezember 2010 Inhaber des Lehrstuhls für ITSicherheitsinfrastrukturen an der Friedrich-Alexander-Universität ErlangenNürnberg. Zuvor war er bereits als Professor für Informatik an der RWTH Aachen (2003-2005) und der Universität Mannheim (2005-2010) tätig. Schwerpunkte seiner Arbeitsgruppe in Forschung und Lehre sind offensive Methoden der IT-Sicherheit, technische Aspekte der Cyberkriminalität sowie digitale Forensik. In den Verfahren zur Online-Durchsuchung und zur Vorratsdatenspeicherung vor dem Bundesverfassungsgericht diente Felix Freiling als sachverständige Auskunftsperson. Philipp Klein ist seit November 2013 wissenschaftlicher Mitarbeiter am Lehrstuhl für IT-Sicherheitsinfrastrukturen unter der Leitung von Prof. Dr. Felix Freiling. Im Oktober 2013 erhielt er seinen Abschluss Master of Science in Informatik an der Friedrich-Alexander-Universität Erlangen-Nürnberg. Während des Masterstudiums legte er seinen Schwerpunkt auf das Thema IT-Sicherheit. Er besitzt einen Bachelor-Abschluss im Fach Wirtschaftsinformatik der Georg-Simon-OhmHochschule Nürnberg. Seite 6 Einleitung zu den Studienbriefen III. Modullehrziele In dieser Lehrveranstaltung lernen Sie, die Programmiersprache Java zu nutzen, um selbstständig Programme zu schreiben. Nach Lektüre dieses Moduls sind Sie in der Lage, die meisten Programmieraufgaben, die an Sie gestellt werden, zu lösen. Sollten Sie ein bestimmtes Konstrukt der Sprache noch nicht gelernt haben, so können Sie es sich selbstständig in kurzer Zeit aneignen. Darüber hinaus können Sie sich weitere Programmiersprachen selbstständig aneignen. Die Prinzipien und Strukturen, die Sie in dieser Lehrveranstaltung erlernen, finden Sie in den meisten anderen Programmiersprachen wieder. Java bietet dabei einen guten Startpunkt, sowohl hardwarenähere Sprachen als auch beispielsweise Skriptsprachen zu erlernen. Programmieren ist etwas, das man weniger aus Büchern lernt, sondern anwenden und beherrschen muss. Aus diesem Grund sind im Studienbrief viele Kontrollaufgaben, die Sie bearbeiten können. Die Lösungen finden Sie am Ende des Studienbriefs. Darüber hinaus müssen Sie Übungsaufgaben bearbeiten, die Sie am Ende jedes Studienbriefs finden. Das Bearbeiten dieser Aufgaben ist Pflicht, sodass Sie gezwungen sind, wirklich zu programmieren. Gleichzeitig bietet das Abgabesystem aber genug Freiheit, um sich die Zeit selbstständig einzuteilen. Dieses Modul teilt sich in zwei Lehrveranstaltungen auf. In der ersten Lehrveranstaltung haben Sie die Grundlagen von Java und des Programmierens erlernt. In der zweiten Lehrveranstaltung werden wir nun einzelne Aspekte von Java genauer betrachten sowie die Theorie hinter der Programmierung erläutern. Modullehrziele Seite 7 Modulbeschreibung Modulbezeichnung: Grundlagen der Programmierung Studiengang: Bachelor IT-Sicherheit Verwendbarkeit: Dieses Modul ist verwendbar für • Studierende der Informatik • Studierende der Wirtschaftsinformatik • Studierende der Mathematik und Informatik auf Bachelorniveau. Dieses Modul kann nicht als Wahlpflichtmodul gewählt werden, sondern ist ein Pflichtmodul. Lehrveranstaltungen und Lehrformen: Einführung in das Programmieren Programmierkonzepte Modulverantwortliche(r): Prof. Dr. Felix Freiling Lehrende: Prof. Dr. Felix Freiling Dauer: 2 Semester Credits: 10 ECTS Studien- und Prüfungsleistungen: Schriftliche Prüfung: 120 Minuten Um zur Prüfung zugelassen zu werden, müssen die bereitgestellten Übungsaufgaben in jeder Lehrveranstaltung zu mindestens 70% richtig bearbeitet werden. Berechnung der Modulnote: Notwendige Voraussetzungen: Empfohlene Voraussetzungen: Keine Unterrichts- und Prüfungssprache: Deutsch Zuordnung des Moduls zu den Fachgebieten des Curriculums: Einordnung ins Fachsemester: Ab Studiensemester 1 Generelle Zielsetzung des Moduls: Modul zur Förderung und Verstärkung der Fachkompetenz Seite 8 Arbeitsaufwand bzw. Gesamtworkload: Einleitung zu den Studienbriefen Für dieses Modul: Präsenzzeit: 60 h • Vorlesungsteil: 20 h • Übungsteil: 10 h • Praktischer Teil: 20 h • Prüfungsvorbereitungsveranstaltung: 8 h • Prüfung: 2 h Eigenstudium: 240 h • Durcharbeiten der Studienbriefe: 100 h • Wahrnehmen der Online Betreuung und Beratung: 20 h • Ausarbeiten von Aufgaben: 100 h • Individuelle Prüfungsvorbereitung der Studierenden: 20 h Modullehrziele Lerninhalt und Niveau: Seite 9 Einführung in das Programmieren Eine Einführung in die Programmiersprache Java. Mit Hilfe der Entwicklungsumgebung BlueJ wird den Studierenden der Umgang mit Java und Objektorientierung vertraut gemacht. Themen sind unter Anderem: • Ausdrücke und Algorithmische Kernsprache von Java • Sprachbeschreibung und Objekttypen • Eine Einführung in bereits existierende Methoden und Klassen in der Programmiersprache Java • Testen und Test Driven Development mit JUnit Darüber hinaus erhalten die Studierenden einen praktischen Einblick in die folgenden programmierrelevanten Technologien/Techniken: • Versionsverwaltung mit Git Programmierkonzepte Diese Lehrveranstaltung knüpft nahtlos an die Veranstaltung „Einführung in das Programmieren“ an. Die Studierenden lernen weitere Komponenten der Programmiersprache Java kennen, wie beispielsweise: • Coding Conventions und Best Practices • Exceptionhandling • I/O-Verarbeitung • Modellierung mit UML • Rekursion • Programmiertheorie (Verifikation, Asymptotisches Laufzeitverhalten) • Einbinden von Datenbanken Neben diesen Inhalten lernen die Studierenden ebenso theoretische Grundlagen der Programmierung kennen, wie die Laufzeitanalyse und die Korrektheit von Algorithmen. Das Niveau der Lerninhalte liegt gemessen am DQR-Niveau bei 6 (Bachelor) Seite 10 Angestrebte Lernergebnisse: Einleitung zu den Studienbriefen Fachkompetenz: Die Studierenden können beliebige Programme in Java erstellen. Sprachkomponenten, die Sie noch nicht kennen, können Sie sich in kürzester Zeit aneignen. Zudem sind die Studierenden in der Lage, sich selbstständig neue Programmiersprachen beizubringen. Sie schreiben sichere Programme und wissen, wo potentielle Schwachstellen in einem Programm zu finden sind. Methodenkompetenz: Die Studierenden beherrschen den Umgang mit beliebigen IDEs. Sie können fremde Programme untersuchen und den Kontrollfluss nachvollziehen. Sie sind in der Lage, Schwachstellen und Fehler in einem Programm zu finden und zu beseitigen. Sozialkompetenz: Durch das gemeinsame Lösen von Aufgaben erlangen die Studierenden die Fähigkeit, eigene Handlungsziele mit den Einstellungen und Werten einer Gruppe zu verknüpfen und ihre Teamfähigkeit zu stärken. In der Präsenzphase erlangen sie u.A. durch Pair-Programming die Kompetenz, eigene Ideen gegenüber einem anderen Programmierer zu kommunizieren, Kompromisse zu bilden und diese im Team umzusetzen. Selbstkompetenz: Die Studierenden erlangen die Fähigkeit zur Bildung einer Meinung über eigene Programme und Programme anderer. Darüber hinaus erlangen sie die Fähigkeit, in komplexen Situationen zu handeln und eine Lösung für komplexe Probleme zu finden. Häufigkeit des Angebots: Wintersemester Anerkannte Module: Anerkannte anderweitige Lernergebnisse / Lernleistungen: Medienformen: Literatur: Studienbriefe in schriftlicher und elektronischer Form, Onlinematerial in Lernplattform, Übungen und Projekt über Lernplattform, Online-Konferenzen, Chat und Forum, Präsenzveranstaltung mit Rechner und Beamer • Touch of class, Betrand Mayer, 2009 • IT-Sicherheit, Claudia Eckert, 2012 • Handbuch Java Band 1, Universität Hannover, 2010 • Einführung in Java mit BlueJ, Florian Siebler, 2011 • Java lernen mit BlueJ, David J. Barnes, Michael Kölling. 2006 • Weitere Literatur wird in der Lehrveranstaltung bekannt gegeben. Studienbrief 6 Coding Conventions und Best Practices Studienbrief 6 Coding Conventions und Best Practices 6.1 Lernergebnisse Nach der Lektüre dieses Studienbriefes sind Sie mit Coding Conventions und einigen Best Practices in Java vertraut. Sie sind in der Lage, besser lesbaren und strukturierten Code zu schreiben. Darüber hinaus kennen Sie einige Fallstricke, die beim Programmieren in Java beachtet werden müssen. Sie wissen, dass es keinen perfekten Code gibt. Am Ende dieses Studienbriefes sind Sie aber in der Lage, besseren Code zu schreiben als vorher. Hierfür haben Sie einige Konventionen und Regeln kennengelernt. Um wieder warm zu werden, finden Sie im Übungsteil dieses Studienbriefes einige Aufgaben. Die Lösungen dieser Aufgaben sind am Ende des Dokuments zu finden. Durch das Bearbeiten dieser Aufgaben sind Sie wieder auf dem Wissensstand, um sich mit den restlichen Studienbriefen zu befassen. Grundlage für diesen Studienbrief sind die Bücher Clean Code - A Handbook of Agile Software Craftsmanship von Robert C. Martin (Martin [2008]) sowie das Buch Weniger Schlecht Programmieren von Katrin Passig und Johannes Jander (Passig [2013]). Für die Coding Conventions wurde auf den Google Java Style (Google [2014]) und die Code Conventions for the Java Programming Language von Oracle (Oracle [1999]) zurückgegriffen. 6.2 Advance Organzier In den bisherigen Studienbriefen lag der Fokus darauf, zu lernen, wie Sie Anforderungen an ein Programm in Java umsetzen. Sie haben die allgemeine Syntax von Java kennengelernt, mit ihren Einschränkungen und Besonderheiten. Damit der Compiler Ihr Programm versteht, müssen Sie sich lediglich an die korrekte Syntax halten. Dem Compiler ist es egal, ob Sie Ihren Code dokumentiert haben, ob Sie aussagekräftige Namen benutzen usw. Sie könnten Ihren Programmcode in eine einzige Zeile schreiben, mit einbuchstabigen Variablennamen und ohne mehrere Klassen zu verwenden. Solange die Syntax stimmt, wird der Compiler ihr Programm übersetzen können. In diesem Studienbrief sollen Sie lernen, Programme zu schreiben, die von anderen Programmierern gelesen und genutzt werden können. Ziel dabei soll es sein, leicht verständlichen und übersichtlichen Code zu produzieren, der sich an gewisse Regeln hält. 6.3 Coding Conventions Coding Conventions (auch: Code Conventions, dt.: Programmierstil) sind Regeln und Richtlinien, die vorgeben, wie programmiert wird. Es wird beispielsweise festgelegt, welche Konventionen für Namen existieren, wie Zeilenumbrüche erfolgen oder wie lang eine Zeile sein darf. Ziel dieser Konventionen ist es, eine einheitliche Struktur in Ihren Code zu bringen. Dies ist besonders wichtig, wenn mehrere Programmierer an dem selben Projekt arbeiten. Seite 11 Seite 12 Studienbrief 6 Coding Conventions und Best Practices Coding Conventions sollten vor einem Projekt festgelegt werden. Das Team sollte sich verpflichten, sich an diese zu halten. Sollte dies nicht geschehen, wird jeder Entwickler seine eigenen Konventionen nutzen – eine einheitliche Struktur des Codes ist so nicht möglich. Die Konventionen, die wir Ihnen auf den nächsten Seiten vorstellen, sind individuell für ein Projekt anpassbar. Bei den Namenskonventionen sollten Sie jedoch nicht abweichen. Was aber Zeilenlänge, Zeilenumbrüche oder Dokumentation angeht: Hier können für jedes Projekt individuelle Anpassungen vorgenommen werden. Wichtig ist lediglich, sich vorab auf Coding Conventions zu einigen. 6.3.1 Namenskonventionen Mit den Namenskonventionen haben wir uns bereits in Studienbrief 2 befasst. Dort haben wir die üblichen Konventionen vorgestellt, als wir die jeweiligen Programmkonstrukte, wie Variablen oder Methoden, eingeführt haben. In diesem Abschnitt fassen wir diese Namenskonventionen noch einmal zusammen und ergänzen, wo nötig, einige Regeln. Allgemeines Ziel von Namenskonventionen ist es, auf den ersten Blick zu sehen, um welche Art von Programmierbaustein es sich bei dem vorliegenden Konstrukt handelt. Stellen Sie sich einmal vor, Sie betrachten fremden Quellcode und wissen nicht, ob im vorliegenden Programmteil eine Methode oder eine Variable benutzt wird. Sicherlich lassen sich Methoden daran erkennen, dass sie am Ende runde Klammern besitzen – aber das sollte nicht das einzige sein, was eine Methode von einer Variablen unterscheidet. Neben diesem offensichtlichen Ziel gibt es aber noch ein weitaus wichtigeres. Durch Namenskonventionen, an die sich alle Entwickler halten, kann der Code leichter von anderen Personen verstanden werden. Diese benutzen die selben Konventionen – und fühlen sich so in Ihrem Code nicht gänzlich unwohl. Klassen Bei der Wahl eines Klassennamens greift man für gewöhnlich auf Substantive oder eine Kombination von Substantiven zurück. Dies lässt sich dadurch begründen, dass eine Klasse eine allgemeine Beschreibung eines Objekts ist. Wenn Sie also Objekte anlegen bzw. beschreiben möchten, die geometrische Formen abbilden, so würden Sie als Klassennamen etwa GeometricalForm wählen. Bei der Benennung von Klassen wird auf den Upper Camel Case zurückgegriffen. Wenn der Klassennamen aus mehreren Wörtern besteht, werden alle Wörter hintereinander, ohne Leerzeichen, geschrieben. Der erste Buchstabe jedes Wortes wird groß geschrieben. Dateinamen Der Dateiname richtet sich nach dem Namen der Klasse, die sich darin befindet. Um den Dateinamen zu bilden, wird an den Klassennamen die Dateiendung .java angehängt. Wenn sich mehrere Klassen in einer Datei befinden, ist es an der Zeit, diese Klassen in jeweils eine eigene Datei zu speichern. Eine Ausnahme hiervon bilden innere Klassen. Auf diese gehen wir gesondert in Studienbrief 11 ein, wo wir uns mit GUI-Programmierung beschäftigen. 6.3 Coding Conventions Seite 13 Interfaces Bei der Bezeichnung von Interfaces greift man auf Substantive oder Adjektive zurück. Im Falle eines Adjektivs enden diese meist auf ~able. Im Deutschen würden wir solche Adjektive mit dem Suffix ~bar übersetzen. Beispiel 6.1: Namenskonventionen bei Interfaces B Einige Namen von Interfaces, wie sie in der Standard-Java-API enthalten sind: Comparable Iterable Drawable List Map Serializable Wie zuvor bei den Klassen, wird auch bei Interfaces der Upper Camel Case benutzt. Damit sind Klassen und Interfaces auf den ersten Blick nicht immer sofort voneinander unterscheidbar. Man trifft daher häufig auf Interfaces, an denen das Wort Interface am Ende angehängt wurde. Beispiel 6.2: Interface als Suffix Beachten Sie: Nur weil Interface in einem Namen vorkommt, handelt es sich nicht zwangsläufig um ein Interface. Die Klasse NetworkInterface1 der Android-API beispielsweise ist eine Klasse. Methoden In einem Methodennamen sollte immer ein Verb vorkommen. Schließlich sind Methoden Dinge, die ein Objekt tun kann oder die wir mit einem Objekt tun können. Bei der Wahl des Namens sollte darauf geachtet werden, dass der Klassenname bei der Benennung berücksichtigt wird. Wenn Sie den Namen der Methode festlegen, denken Sie sich den Klassennamen vor dem Methodennamen. Zum Verständnis ein Beispiel: class Car { public void driveCar() // code } Das obere Beispiel ist ein schlechtes. Wenn Sie jemand fragen würde, was man mit einem Auto tun kann, würden Sie wohl kaum mit „autofahren“ antworten. Die natürlichere Antwort wäre: „fahren“. 1 http://developer.android.com/reference/java/net/NetworkInterface.html B Seite 14 Studienbrief 6 Coding Conventions und Best Practices Bei der Benennung einer Methode sollten Sie dieses Prinzip ebenfalls berücksichtigen. Der Klassenname gehört zum Methodennamen dazu. Wenn Sie von der Methode sprechen, würden Sie sagen: „Die Methode drive der Klasse Car“. Damit sollte auch klar sein, wie wir den oben stehenden Code verbessern können: class Car { public void drive() // code } Wie Sie dem ersten, schlechten Beispiel wahrscheinlich schon entnommen haben, werden Methodennamen, falls sie aus mehreren Wörtern bestehen, in lower Camel Case geschrieben. Hier wird der erste Buchstabe des ersten Wortes klein geschrieben. Der jeweils erste Buchstabe aller folgenden Wörter wird groß geschrieben. B Beispiel 6.3: Namenskonventionen bei Methoden Einige Beispiele für Methodennamen: getIndex() run() validatePassword() split() create() deleteAll() getColorInRGB() Variablen Für Variablennamen werden meist Substantive gewählt. Alternativ können bei boolean-Variablen auch Adjektive gewählt werden, denen sich die Werte true oder false zuordnen lassen. Variablennamen werden, wie Methoden, in lower Camel Case geschrieben. Sie sollten detailliert beschreiben, für welchen Zweck die Variable verwendet wird oder was in ihr gespeichert ist. Allgemeine Namen, wie array oder map, sollten dagegen nicht verwendet werden. Die oben beschriebenen Richtlinien gelten sowohl für lokale Variablen als auch für Parameter und Attribute (die auch als Membervariablen bezeichnet werden). Daher stellt sich die Frage, wie diese im Code unterschieden werden können. Hier gibt es mehrere Möglichkeiten, u. a: 1. Die Methode ist so kurz, dass sofort ersichtlich ist, um was es sich handelt 2. Für Membervariablen durchgehend this. verwenden 3. Nicht auf Membervariablen direkt zugreifen, sondern Getter und Setter verwenden Möglichkeit 1 ist ganz klar präferiert. Methoden sollten so kurz wie möglich gehalten werden. Bei Methoden, die Maximal 10 bis 20 Zeilen umfassen, ist meist 6.3 Coding Conventions sehr schnell klar, ob es sich um eine Membervariable oder um eine lokale Variable handelt. Möglichkeit 2 bietet Sicherheit, was die Verwendung von Membervariablen angeht. So kann es nicht passieren, dass aus Versehen eine Membervariable in Ihrem Wert verändert wird oder man einen Wert aus einer lokalen Variable liest, die ein paar (hundert) Zeilen zuvor definiert wurde und die man für eine Membervariable gehalten hat. Möglichkeit 3 ist ebenso eine gute Option. Besonders, wenn man ohnehin Getter und Setter definiert hat, sollte man von diesen auch Gebrauch machen. Konstanten Mit Konstanten haben wir uns in den bisherigen Studienbriefen noch nicht beschäftigt. Konstanten sind genau genommen lediglich Membervariablen, die ihren Wert niemals ändern. Das beste Beispiel für eine solche Konstante ist die Zahl Pi. Um diese Zahl nicht in jeder Formel neu definieren zu müssen, würden Sie die Kreiszahl vermutlich als Membervariable speichern. Aber sie wird dieser Bezeichnung nicht wirklich gerecht: Sie ist nicht variabel, sondern hat einen konstanten Wert. Um festzulegen, dass der Wert in einer Variable niemals verändert werden kann, wird das Schlüsselwort final verwendet. Einer Membervariablen, die als final markiert wurde, kann nach der ersten Wertzuweisung ein anderer Wert zugeordnet werden. Folgendes ist demnach nicht möglich: class Example { final int value = 1; public Examle() { this.value = 2; } } Im Konstruktor versuchen wir, einer final-Variablen einen neuen Wert zuzuweisen, nachdem dieser bereits ein Wert zugewiesen wurde. Dieser Code würde so nicht kompilieren. Folgender Code dagegen ist okay: class Example { final int value; public Example() { this.value = 2; } } Seite 15 Seite 16 Studienbrief 6 Coding Conventions und Best Practices Hier wird der final-Variablen zum ersten Mal im Konstruktor ein Wert zugewiesen. Dies ist ein völlig legitimer Einsatz einer solchen Variablen. M Merksatz 6.1: Zuweisung von final-Variablen Der Wert einer mit final deklarierten Variable muss nach dem Anlegen des Objekts, sprich nach dem Aufruf des Konstruktors, feststehen. Es ist also nicht legitim, in einer Methode den Wert einer solchen Variable zu setzen. Tun Sie es dennoch, wird der Code nicht kompilieren. Um wieder auf Konstanten zurückzukommen: Eine Konstante wird als immer als final deklariert. Da die Konstante immer für alle Instanzen der Klasse gilt, wird sie zudem noch als static deklariert. Um Konstanten von anderen Membervariablen unterscheiden zu können, hat sich eine bei vielen Programmiersprachen folgende Notation durchgesetzt: Konstanten werden komplett in Großbuchstaben geschrieben. Wenn der Name der Konstanten aus mehreren Wörtern besteht, so werden diese durch einen Underscore (Unterstrich) voneinander getrennt. Die Kreiszahl Pi würde also im Code folgendermaßen aussehen: public final static double PI = 3.14; Natürlich müssen Sie Pi nicht selbst definieren. Die Klasse Math bietet Zugriff diese und andere Konstanten. In Beispiel 6.4 sehen weitere mögliche Namen für Konstanten. B Beispiel 6.4: Namenskonvention bei Konstanten Neben der Konstanten PI bietet die Klasse Math noch eine weitere Konstante: Math.E für die eulersche Zahl. Hier noch einige weitere Beispiel für Konstanten: NUMBER_OF_PLANETS INITIAL_VALUE MIN_SIZE MAX_SIZE SECRET_KEY ERROR_MARGIN 6.3 Coding Conventions Kontrollaufgabe 6.1: Syntaxbausteine erkennen Um welche Syntaxbausteine handelt es sich bei den folgenden Konstrukten? Bei Methoden wurden, um es nicht trivial zu machen, die Klammern weggelassen. run isValid SPEED_OF_LIGHT Browser file Deletable deleteLast firstValue 6.3.2 Klammersetzung Bei der Frage, wie Sie die geschweiften Klammern zu setzen haben, werden Sie auf zwei Fraktionen stoßen. Die eine Fraktion setzt die öffnende geschweifte Klammer in der selben Zeile wie den vorangegangenen Befehl: public int getNumber() { // Code with return } Die andere Fraktion setzt die öffnende Klammer in eine eigene Zeile. Die Einrückungstiefe ist dabei identisch mit der darüber stehenden Zeile: public int getNumber() { // Code with return } Jede Fraktion wird Ihnen die Vor- und Nachteile ihrer Variante erklären. Im Grunde ist aber reine Geschmackssache. Wichtig ist lediglich, sich für eine Variante zu entscheiden und diese konstant zu benutzen. In diesen Studenbriefen verwenden wir die zweite Variante, da der Autor diese privat benutzt. Bei der ersten Variante gibt es eine weitere Besonderheit: Bei if-else-Anweisungen erfolgt die öffnende Klammer des else-Teils in der selben Zeile, in der der if-Teil beendet wurde: if(condition) { //Code } else { // other Code } 6.3.3 Einrückung Wie schon bei der Klammersetzung, gibt es auch bei der Einrückung von Code mehrere Möglichkeiten. Was aber alle gemeinsam haben: Nach jeder öffnenden geschweiften Klammer wird der Code um eine weitere „Einheit“ eingerückt. Seite 17 K Seite 18 Studienbrief 6 Coding Conventions und Best Practices Unterschiede gibt es lediglich bei den Ausmaßen dieser Einheit. Viele Leute verwenden ein Tab (also die Tabulatortaste), um den Code einzurücken. Je nach Entwicklungsumgebung oder persönlichen Einstellungen bewirkt diese Taste entweder das Einfügen eines Tabs (als Escape Sequence im Text \t geschrieben) oder einer gewissen Anzahl an Leerzeichen. Problematisch dabei ist, dass die Länge eines Tabs nicht fest vorgeschrieben ist. Häufig entspricht die Länge 4 Leerzeichen, manchmal auch mehr oder weniger. Probleme können dann auftreten, wenn diese Taste dazu genutzt wird, um Code an eine bestimmte Stelle zu rücken, um die Leserlichkeit zu erhöhen. Wenn ein anderer Ihren Code öffnet und eine andere Tabulatorbreite hat, ist die Formatierung kaputt. Aus diesem Grund machst es Sinn, direkt Leerzeichen zu verwenden und beim Druck der Tabulatortaste entsprechend Leerzeichen einzusetzen. Aber auch hier ist wichtiger, dass sich für eine Variante entschieden und diese konstant umgesetzt wird. E Exkurs 6.1: Einrückung in Python In der Programmiersprache Python werden keine geschweiften Klammern genutzt, um einzelne Blöcke voneinander abzugrenzen. Stattdessen wird zu diesem Zweck die Einrückung genutzt. Sollte die Einrückung nicht stimmen (oder man verwendet sowohl Tabs als auch Leerzeichen), dann kann der Code nicht ausgeführt werden. Ein kleines Beispiel in Python: for i in range(0,20): print(i) print("done") Ohne die Einrückung von print(i) würde es zu einem Fehler kommen, da nach einem Doppelpunkt zumindest eine Zeile eingerückt werden muss. Um die for-Schleife zu beenden, reicht es aber, die Einrückung zu beenden. Der Befehl print("done") wird erst nach der Schleife ausgeführt. 6.3.4 Zeilenlänge Bei der Zeilenlänge galt früher die Devise: Eine Zeile sollte nicht mehr als 80 Zeichen haben. Grund dafür waren alte, terminalbasierte Editoren, die maximal eine Zeilenlänge von 80 Zeichen auf dem Bildschirm darstellen konnten. Wenn die Zeile länger war, musste man umständlich scrollen, um den Rest der Zeile zu sehen. Heute ist dieses Problem nicht mehr so gegenwärtig: Moderne IDEs und Editoren können beliebig viele Zeilen pro Zeile darstellen, je nach eingestellter Schriftgröße und Auflösung. Dennoch sollten Sie Ihre Zeilenlänge begrenzen. Bei sehr langen Zeilen kann es auch heute noch passieren, dass diese nicht auf den Bildschirm passen. Zudem sinkt mit zunehmender Zeilenlänge das Codeverständnis und die allgemeine Übersicht. 6.3 Coding Conventions Seite 19 Und auch wenn sie nicht mehr allgegenwärtig sind: Auch heute werden noch terminalbasierte Editoren verwendet, die eine Zeilenlänge von 80 Zeichen darstellen. Besonders bei der Serverkonfiguration greift man häufig auf solche Editoren zurück. Sie sollten also bei einem Projekt von vornherein festlegen, welche Länge die Zeilen maximal haben dürfen. Der Google Java Style (Google [2014]) beispielsweise schreibt eine Länge von 80 oder 100 Zeichen vor. Was aber, wenn die Zeile oder der Methodenaufruf mehr als diese Zeichenzahl benötigt? Dann muss auf Zeilenumbrüche zurückgegriffen werden. 6.3.5 Zeilenumbrüche Bei der richtigen Anwendung von Zeilenumbrüchen unterscheidet man zwischen dem Zeilenumbruch bei Nicht-Zuweisungsoperatoren und dem Zeilenumbruch bei Zuweisungsoperatoren. Zu den Nicht-Zuweisungs-Operatoren gehören beispielsweise der Punkt beim Aufruf einer Methode oder die logischen Operatoren bei einer if-Abfrage. Wenn bei solchen Konstrukten ein Zeilenumbruch nötig ist, dann erfolgt der Umbruch vor dem Operator. Ein Beispiel für einen solchen Zeilenumbruch finden Sie in Beispiel 6.5. Beispiel 6.5: Zeilenumbruch bei Nicht-Zuweisungsoperatoren if(firstConditionMet && secondConditionMet) { redCar.getColor() .getRGB() .getRed(); } In dem oberen Beispiel haben wir mehrere Zeilenumbrüche eingefügt, um die Leserlichkeit zu erhöhen und die Zeilenlänge zu reduzieren. Beachten Sie, dass der Zeilenumbruch vor dem jeweiligen Operator erfolgt. Bei Zuweisungsoperatoren erfolgt der Zeilenumbruch dagegen nach dem Operator. Außer dem Zuweisungsoperator selbst gibt es aber noch weitere Operatoren, bei denen der Zeilenumbruch nachträglich erfolgt: So zum Beispiel das Komma oder das Semikolon in einer for-Schleife. B Seite 20 Studienbrief 6 Coding Conventions und Best Practices In Beispiel 6.6 finden Sie zur Verdeutlichung ein Codebeispiel. B Beispiel 6.6: Zeilenumbruch bei Zuweisungsoperatoren public int longMethodName(int firstParameter, int secondParameter, int thirsParamter) { int longVariableName = 2 * this.getVeryLongName(); for(int i = longVariableName; i > 0; i--) { // Do something } } In obigem Code sind mehrere Beispiele für Zeilenumbrüche zu finden, die nach dem Operator erfolgen. Dies tritt am Häufigsten bei dem Zuweisungsoperator sowie dem Semikolon und dem Komma auf. Einrückung nach Zeilenumbruch Bei der Einrückung nach einem Zeilenumbruch gibt es eine Besonderheit: Das gebrochene Codefragment wird doppelt eingerückt. Wenn also die normale Einrückung 4 Leerzeichen beträgt, wird der Code um 8 Leerzeichen eingerückt. Auf diese Weise hebt sich der Code besser von dem darauf folgenden Block ab. Zeilenumbruch bei Importen und Paketnamen Importe und Paketnamen bilden eine Ausnahme beim Zeilenumbruch. Hier darf kein Zeilenumbruch erfolgen, da sonst der Befehl vom Compiler nicht korrekt erkannt wird. 6.3.6 Encoding Wenn Sie eine Textdatei speichern, wird diese auf Ihrer Festplatte in Form von Bits und Bytes abgespeichert. Ebenso verhält es sich mit .java-Dateien, die ebenfalls einfache Textdateien sind. Wenn Sie die Datei öffnen wollen, müssen Sie die Bits als Buchstaben interpretieren. Diese Interpretation ist das so genannte Encoding, auf deutsch Zeichenkodierung. Es gibt verschiedene Möglichkeiten, eine Zahl (also mehrere Bits) als Buchstaben zu interpretieren bzw. eine Buchstaben als Bits zu speichern. Geläufig ist die ASCIIKodierung. ASCII umfasst 127 Zeichen, wandelt also 8 Bits in einen Buchstaben um. Problem hierbei: Das reicht bei weitem nicht aus, um alle Sprachen, u. a. deutsch, abzubilden. Mit ASCII lassen sich beispielsweise keine Umlaute darstellen. Wir wollen an dieser Stelle nicht zu tief in die Thematik des Encoding eintauchen. Es reicht aus, wenn Sie sich an folgende Regel halten: Benutzen Sie immer UTF-8 als Encoding für Ihre Dateien. So können Sie alle geläufigen Zeichen in allen 6.3 Coding Conventions Seite 21 Sprachen (auch Klingonisch) in Ihrem Code verwenden. Beachten Sie aber hierzu noch Merksatz 6.2. Merksatz 6.2: Encoding M Nutzen Sie für die Benennung von Variablen, Klassen usw. nur Buchstaben, die im ASCII-Encoding vorkommen. Im Grunde sind dies die Buchstaben von A bis Z (klein und groß), die Zahlen von 0 bis 9, sowie Sonderzeichen wie der Underscore. Bei Strings und Kommentaren bleibt es Ihnen überlassen, welche Sprache Sie verwenden. Lesen Sie dazu den nächsten Abschnitt. Exkurs 6.2: Was jeder über Encoding wissen sollte E Der Softwareentwickler und Blogger Joel Spolsky hat 2003 den Artikel The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) verfasst (Spolsky [2003]). Lesen Sie diesen Artikel2 , wenn auch Sie das absolute Minimum über Encoding wissen wollen. Kontrollaufgabe 6.2: Encoding K Legen Sie eine neue Textdatei an, schreiben Sie einige Umlaute in diese Datei und speichern Sie dann mit einem beliebigen Encoding außer UTF-8 ab. Öffnen Sie nun die Datei mit einem anderen Texteditor. Wird Ihnen der Text korrekt angezeigt? 6.3.7 Sprache Zur Sprache, in dem der Code verfasst wird, gibt es nicht viel zu sagen: Da die Befehle auf Englisch sind, sollten Ihre Namen für Variablen, Methoden usw. ebenfalls aus der englischen Sprache stammen. Auf keinen Fall sollte eine Mischform wie getFarbe oder setWert gewählt werden! Exkurs 6.3: T_PAAMAYIM_NEKUDOTAYIM (entnommen aus Passig [2013]) Bei der PHP-Programmierung konnte es passieren, dass Sie auf den Fehler error: syntax error, unexpected T_PAAMAYIM_NEKUDOTAYIM gestoßen sind. Sie sehen wahrscheinlich aufgrund der Schreibweise, dass es sich um eine Konstante handeln muss. Wenn Sie aber nicht des hebräischen mächtig sind, werden Sie wohl nicht darauf kommen, dass hier zwei aufeinanderfolgende Doppelpunkte angeprangert werden. 2 http://www.joelonsoftware.com/articles/Unicode.html E Seite 22 Studienbrief 6 Coding Conventions und Best Practices 6.4 Dokumentation Beim Programmieren verbringen Sie gewöhnlich mehr Zeit damit, Code zu lesen, als selbst Code zu verfassen. Durch die bisherigen Coding Conventions können Sie imemrhin sicherstellen, dass ein anderer Programmierer Ihren Code strukturell nachvollziehen kann und sich nicht komplett „fremd“ fühlt. Um den Code aber in seiner Gänze nachvollziehen zu können, sind Kommentare und vor allem eine ausführliche Dokumentation notwendig. 6.4.1 Kommentare In Studienbrief 2 haben Sie bereits gelernt, wie in Java Kommentare formuliert werden können: Einzeilige Kommentare werden mit einem doppelten Slash (//) eingeleitet. Mehrzeilige Kommentare beginnen mit /* und enden mit */. // one line commentary /* multi line commentary / * Wie aber können Sie Kommentare schreiben, die anderen Programmierern und Ihnen selbst helfen, den Code besser zu Verstehen? Zunächst eine Grundregel: Sie können davon ausgehen, dass derjenige, der Ihren Code betrachtet, die Programmiersprache beherrscht. Das bedeutet, dass Sie nicht Offensichtliches kommentieren müssen. Folgendes Beispiel ist also unsinnig: int value = 5; // sets value to 5 Jeder Programmierer erkennt sofort, was sie hier tun. Dafür benötigt er keinen Kommentar. Viel wichtiger, als zu beschreiben, was der Code tut, ist es, zu beschreiben, warum der Code sich so verhält. Folgendes ist also durchaus als Kommentar angebracht: int value = 5; // number of fingers Dieses Beispiel führt uns zu einer weiteren, wichtigen Grundregel: Guter Code dokumentiert sich selbst. Durch die geschickte Namenswahl bei Variablen und Methoden sowie durch Refactoring und kurzen Methoden bedarf es häufig keinem Kommentar. Anstatt also zu kommentieren, dass eine Hand 5 Finger hat, schreiben Sie Folgendes: int numberOfFingers = 5; Durch die Wahl eines aussagekräftigen Namens erübrigt sich ein Kommentar. Bevor wir uns weiter mit Kommentaren und Dokumentation befassen, müssen wir eine Unterscheidung treffen. Einerseits gibt es die Möglichkeit, durch Kommentare unseren Code verständlicher zu machen. Anderseits gibt es die Möglichkeit, eine ausführliche Dokumentation zu schreiben. 6.4 Dokumentation Seite 23 Kommentare im Quelltext Was wir bisher betrachtet haben, waren Kommentare direkt im Quelltext. Sie werden direkt an die Stelle geschrieben, auf die sie Bezug nehmen. Diese Kommentare sind für andere Entwickler bestimmt, die Ihren Quelltext unkompiliert vorliegen haben. Ziel dieser Gruppe ist es, den Ablauf innerhalb Ihres Codes zu verstehen, um beispielsweise Änderungen vorzunehmen oder Fehler auszubessern. Die Kommentare sollten sie dabei unterstützen: Beschreiben Sie keine Trivialitäten, sondern ergänzen Sie den Code um wertvolle Hinweise, die zum Verständnis beitragen. Bei tief verschachtelten Methoden können Sie beispielsweise das Verständnis erhöhen, indem Sie bei schließenden Klammern notieren, welche for-Schleife gerade geschlossen wurde. Bei komplizierten if-Bedingungen können Sie in einfacher Form schreiben, welche Bedingung abgeprüft wird. Aber auch hier gilt: Guter Code dokumentiert sich selbst. Anstatt also komplizierte Dinge mit Kommentaren zu versehen, sollten Sie immer versuchen, den Code zu vereinfachen und besser zu strukturieren. Bevor Sie also bei einer if-Abfrage den Kommentar „Checks if user is logged in“ schreiben, sollten Sie die Abfrage lieber in eine eigene Methode mit dem Namen isLoggedIn auslagern. Exkurs 6.4: Tasks in Eclipse E Eclipse hat ein nützlicher Feature, um Stellen im Code zu markieren, an denen noch einige Dinge zu tun sind. Wenn Sie in Ihrem Code einen Kommentar mit // TODO: beginnen, so wird dieser Kommentar gesondert hervorgehoben. Wenn Sie nun die View Tasks öffnen (Window → Show View → Tasks), sehen Sie alle TODOs in einer Liste. Neben TODO gibt es noch weitere „Tags“, wie FIXME: oder XXX:. Beispiel 6.7: Zweierpotenz Folgendes Codefragment stammt aus dem Sourcecode der Klasse Random3 if ((n & -n) == n) // i.e., n is a power of 2 Dies ist ein sehr gutes Beispiel für einen Kommentar, der dem Leser beim Verständnis des Codes hilft. Oder wären Sie direkt darauf gekommen, dass hier geprüft wird, ob n eine Zweierpotenz ist? Man könnte sich natürlich überlegen, diese Abfrage als eigene Methode isPowerOfTwo auszulagern. B Seite 24 K Studienbrief 6 Coding Conventions und Best Practices Kontrollaufgabe 6.3: Zweierpotenz Versuchen Sie nachzuvollziehen, warum folgender Code abprüft, ob n eine Zweiterpotenz ist: if ((n & -n) == n) // i.e., n is a power of 2 Sie müssen es nicht mathematisch nachweisen, sondern einfach logisch nachvollziehen können. Beachten Sie, dass durch -n das Zweiterkomplement gebildet wird. Javadoc Neben der Möglichkeit, Code für andere Entwickler zu kommentieren, gibt es bei Java von Haus aus die Möglichkeit, ausführliche Dokumentationen komfortabel zu schreiben. Durch eine spezielle Notation und das Tool javadoc können aus diesen Dokumentationen HTML-Seiten generiert werden, wie Sie sie von der Java-API kennen.4 Die Dokumentation mittels javadoc ist zum einen für Sie und andere Entwickler gedacht. Zum anderen ist diese Dokumentation aber auch für Nutzer Ihrer Software gedacht, denen der Quellcode nicht vorliegt. Das beste Beispiel hierfür ist die Java-API. Ohne sich mit dem Quellcode zu befassen, können Sie durch die Dokumentation leicht feststellen, was eine Methode tut. Dafür können Sie entweder die Dokumentation auf der Webseite lesen oder aber direkt in Ihrer IDE ansehen (falls diese dies unterstützt). Bevor wir detailliert auf javadoc eingehen, betrachten wir den Code in Quelltext 6.1. Q Quelltext 6.1: "Javadoc" 1 /** * short Description 3 * <p> 4 * Here is a long, detailed description. 2 5 6 7 8 9 10 11 12 13 * * @param numberOfTimes How often something is done. If negative, nothing happens * @param what What should be done. If null, nothing happens. * / ** public void doSomething ( int numberOfTimes , String what ) { // code } In dem oberen Quelltext ist die Methode doSomething mittels Javadoc dokumentiert. Um einen Javadoc-Kommentar zu beginnen, wird ein Slash mit zwei Sternen (/**) genutzt. Die Kombination von zwei Sternen, gefolgt von einem Slash (**/), 3 http://developer.classpath.org/doc/java/util/Random-source.html 4 http://docs.oracle.com/javase/7/docs/api/ 6.4 Dokumentation Seite 25 beendet den Javadoc-Kommentar. Jede Zeile des Kommentars beginnt mit einem Stern (*). Wenn Sie das javadoc-Tool über diesen Code laufen lassen, erhalten Sie eine Dokumentation im HTML-Format. Abbildung 6.1 zeigt die Übersicht über alle Methoden, wie Sie in der Dokumentation zu finden ist. Hier finden Sie auch unsere gerade dokumentierte Methode doSomething(). Abb. 6.1: Javadoc: Zusammenfassung aller Methoden. Method Summary Methods Modifier and Type Method and Description void doSomething (int numberOfTimes, java.lang.String what) short Description Methods inherited from class java.lang.Object clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait Sie sehen als Beschreibung der Methode den Text „Short Description“. Diesen finden Sie auch in Quelltext 6.1 als erste Zeile unserer Dokumentation wieder. Generell sollten Sie immer eine Kurzbeschreibung verfassen, die dann in der Method Summary angezeigt wird. Um diese Beschreibung zu beenden, können Sie das HTML-Tag <p> nutzen, wie wir es im Quelltext getan haben. Neben den Methoden, die Sie für eine Klasse selbst definiert und dokumentiert haben, sehen Sie in der Method Summary auch die Methoden, die von der Oberklasse geerbt wurden. Neben der Zusammenfassung der Methoden gibt es in der HTML-Dokumentation noch den Abschnitt Method Detail. Hier finden Sie zu jeder Methode die detaillierte Dokumentation. Abbildung 6.2 zeigt die von uns verfasste Dokumentation der Methode doSomething. Method Detail doSomething public void doSomething(int numberOfTimes, java.lang.String what) short Description Here is a long, detailed description. Parameters: numberOfTimes How often something is done. If negative, nothing happens what What should be done. If null, nothing happens. Neben der kurzen Beschreibung finden Sie auch die lange Beschreibung, die wir im Quelltext eingetragen haben. Des Weiteren bietet Javadoc über so genannte Tags einige nützliche Funktionen. Durch den Tag @param haben wir die Parameter, die der Funktion übergeben werden, genauer dokumentiert. Nach dem Tag folgt Abb. 6.2: Javadoc: Details einer Methode. Seite 26 Studienbrief 6 Coding Conventions und Best Practices hier zunächst der Name des Parameters (numberOfTimes bzw. what). Darauf folgt die Beschreibung des Parameters. Hier können Sie nun näher beschreiben, was übergeben werden soll und wie sich der Code bei gewissen Eingaben verhält. Was in der HTML-Dokumentation aus den @param-Tags geworden ist, sieht man in Abbildung 6.2. Unter Paramters sind hier alle Parameter mitsamt ihrer Dokumentation zusammengefasst. Wo kann Javadoc verwendet werden? Bisher haben wir nur betrachtet, wie eine Methode mittels Javadoc dokumentiert werden kann. Neben Methoden können aber noch folgende Dinge mit Hilfe von Javadoc dokumentiert werden: • Klassen • Interfaces • Konstruktoren • Membervariablen • Konstanten Beachten Sie, dass innerhalb einer Methode kein Javadoc verwendet werden kann! Falls Sie innerhalb einer Methode etwas kommentieren oder dokumentieren möchten, nutzen Sie Kommentare (siehe dazu auch Abschnitt 6.4.1 auf Seite 22). Tags Neben dem @param-Tag gibt es noch weitere Tags, die Sie in Javadoc verwenden können. Manche davon sind auf bestimmte Konstrukte, wie Klassen oder Methoden, beschränkt. Tabelle 6.4.1 bietet Ihnen eine Übersicht über die die wichtigsten Tags. Tabelle 6.1: Die wichtigsten Javadoc-Tags Wo verwendet Methoden, Konstruktoren Syntax Bedeutung @param <Parametername> <Beschreibung> Zur Beschreibung von Parametern @author Klasse, Interface @author <Autorname> @version Klasse, Interface Um den Autor einer Klasse zu ermitteln. Gut, wenn um einen Ansprechpartner zu finden. @return Methode @version <Versionsnummer> @return <Beschreibung> @see Überall Tag @param @see <Referenz> Die Version der Klasse. Zur Beschreibung des Rückgabewertes Um auf andere Klassen oder Methoden zu verweisen. Sollten sich diese in anderen Dateien befinden, muss eine Raute genutzt werden, um den „Pfad“ anzugeben: Klassenname#Methodenname 6.5 Best Practices Seite 27 Eine komplette Liste der Tags können Sie auf der Wikipedia-Seite von Javadoc ansehen.5 Was schreibe ich in eine Dokumentation? Wir haben uns jetzt ausführlich damit befasst, wie eine Dokumentation mit Javadoc geschrieben wird. An dieser Stelle gehen wir kurz auf den Inhalt einer Dokumentation ein. Die Dokumentation sollte beschreiben, was die Methode (oder Klasse, Interface usw.) tut. Dabei sollte die Beschreibung aber allgemein gehalten werden. Wichtig ist, auf eventuelle Ausnahmen und Sonderfälle einzugehen. Was passiert, wenn eine negative Zahl übergeben wird? Was bedeutet der Rückgabewert? Solche Dinge sollten für den Nutzer Ihrer Software dokumentiert werden. Es sollte auch darauf eingegangen werden, warum sich z. B. eine Methode so verhält. Gibt es Eingaben, die einen Fehler auslösen? Falls Sie den Fehler nicht beseitigen können, sollten Sie ihn zumindest dokumentieren. Es ist auch die Unterscheidung zu treffen, für welche Zielgruppe Sie die Dokumentation schreiben. Sie sollten zunächst immer sich selbst als Zielgruppe einschließen! Denn wenn Sie Ihren eigenen Code nach einigen Wochen/Monaten betrachten, werden Sie kaum noch Erinnerungen an Details der Implementierung haben. Als weitere Zielgruppen gibt es Nutzer, denen der Sourcecode nicht vorliegt. Hier sollte die Dokumentation so geschrieben werden, dass eindeutig ist, wie sich Ihr Code verhält. Durch die Dokumentation mit Javadoc haben Nutzer die Möglichkeit, Ihre Dokumentation direkt in der IDE zu lesen, wenn Sie den Mauszeiger über die Methode bewegen. Ebenfalls als Zielgruppe zu betrachten sind Kollegen oder andere Entwickler, denen Ihr Quellcode vorliegt. Diese Personen wollen häufig Ihren Quelltext verstehen und verändern, sodass die Dokumentation Sie bei diesem Vorhaben unterstützen sollte. Beispiel 6.8: Dokumentation der Java-API Betrachten Sie die Dokumentation und den Quelltext der Klasse Random.6 Die gesamte Klasse ist ausführlich mit Javadoc dokumentiert. Wo nötig, sind im Code selbst noch kleine Anmerkungen. Die Länge der Dokumentation übersteigt den eigentlichen Quelltext deutlich. 6.5 Best Practices Auf den nächsten Seiten werden wir Ihnen einige Best Practices für Java vorstellen. Hierunter verstehen wir gewisse Vorgehensweisen, Methodiken und Denkweisen, die Ihnen helfen, besseren Code zu schreiben. 5 http://de.wikipedia.org/wiki/Javadoc 6 http://developer.classpath.org/doc/java/util/Random-source.html B Seite 28 Studienbrief 6 Coding Conventions und Best Practices 6.5.1 Testing Dieser Punkt ist so wichtig, dass wir ihm im letzten Semester einen kompletten Studienbrief gewidmet haben. Sie sollten Ihren Code immer testen, bevor Sie ihn ausliefern, abgeben oder in einem Programm verwenden. Das muss nicht unbedingt heißen, dass Sie mittels JUnit Tests schreiben. Ihren Code in der main-Methode zu testen, ist bereits ein Anfang. Das Problem an der main-Methode ist aber, dass Sie den Code dort immer wieder löschen und umschreiben werden. Wie können Sie da sicher gehen, dass nach einer Änderung alles noch so funktioniert, wie bisher? Die bessere Lösung ist daher, von Anfang an Junit zu verwenden. Der Vorteil davon liegt auf der Hand: Sie können die Tests immer wieder verwenden. Sie haben die Sicherheit, dass ihr Code für die im Test benutzten Parameter korrekt funktioniert. Wenn Sie Änderungen am Code vornehmen, haben Sie die Gewissheit, dass alle Funktionen erhalten geblieben sind. Damit bilden Tests die theoretische Grundlage für das Refactoring, mit dem wir uns im nächsten Abschnitt beschäftigen. Wann sollte ich Tests schreiben? Wichtig ist, dass Sie überhaupt Tests schreiben. Es gibt die Möglichkeit, die Tests vor dem zu testenden Code zu schreiben. Im Anschluss schreiben Sie den minimalen Code, der erforderlich ist, um die Tests zu bestehen. Dieses Vorgehen wird auch als Test Driven Development bezeichnet. Sie können die Tests natürlich auch erst schreiben, nachdem Sie den zu testenden Code geschrieben haben. Hier macht einem aber häufig die eigene Bequemlichkeit zu schaffen: Der Code, auf den es ankommt, existiert bereits. Warum sollen Sie Ihre Zeit damit verschwenden, Code zu schreiben, der Sie bei der Lösung eines Problems nicht voran bringt? Aus diesen und anderen Gründen wird das Schreiben von Tests gerne nach hinten verschoben und eventuell ganz vergessen. Tests für den gesamten Code zu schreiben, kostet am Anfang häufig Überwindung. Sie werden sich manchmal fragen, warum jede Kleinigkeit getestet werden muss. Wenn jedoch nach dem Refactoring plötzlich ein Test fehlschlägt, werden Sie für diesen Test dankbar sein. 6.5.2 Refactoring Refactoring gehört zum Entwicklungsprozess dazu. Zur Erinnerung: Unter Refactoring versteht man das Verbesserern des Codes (Struktur, Laufzeit, Leserlichkeit) ohne die Funktion zu verändern. Nach dem Refactoring sollte das Programm also das selbe tun wie vorher. Und hier kommen wir an den Punkt, den wir im letzten Abschnitt angesprochen haben: Wie können Sie sicher sein, dass sich Ihr Programm genau so verhält wie vorher? Wenn Sie keine Tests haben, müssten Sie theoretisch alles noch einmal von Hand testen. Automatische Tests sind somit theoretisch eine Notwendigkeit für erfolgreiches und effektives Refactoring. Wie gehe ich beim Refactoring vor? Diese Frage lässt sich nicht gezielt beantworten. Generell sollten Sie, nachdem Sie eine Methode oder Klasse implementiert haben, sich Ihren Code erneut ansehen. 6.5 Best Practices Seite 29 Erfüllt Ihr Code die Coding Conventions? Sind Best Practices, wie Sie in den folgenden Abschnitten vorgestellt werden, berücksichtigt? Lässt sich das Problem effektiver lösen? Kann ich Methoden auslagern oder sind manche Methoden in anderen Paketen/Klassen besser aufgehoben? Diese Fragen sollten Sie im Hinterkopf haben, wenn Sie sich Ihren Code ansehen. Generell muss man aber sagen: Es gibt nicht den perfekten Code. Refactoring sieht bei jedem Programmierer anders aus. Beherzigen Sie einfach Folgendes: Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live.7 Vergessen Sie beim Refactoring nicht, die Dokumentation anzupassen! 6.5.3 Don’t repeat yourself (DRY) Die Regel Don’t repeat yourself ist eine Regel, die Sie auf jeden Fall beherzigen sollten. Folgendes ist damit gemeint: Wenn an mehreren Stellen in Ihrem Code exakt die selben (oder sehr ähnliche) Befehle stehen, dann ist es an der Zeit für Refactoring. Sie sollten den gemeinsamen Code dann in eine eigene Methode auslagern. Dabei können Sie auf eine weitere Regel zurückgreifen: Die Rule of three. Sie besagt, dass wenn Sie den selben Code an drei verschiedenen Stellen im Programm nutzen, Sie diesen Code in eine Methode auslagern sollten. Einmaliges Kopieren ist okay, aber bei der zweiten Kopie (also dem dritten Auftreten) sollten Sie Refactoring betreiben. Generell weist die Verwendung von Copy-Paste darauf hin, dass an dieser Stelle Refactoring angebracht ist. Beim Kopieren von Code sollten Sie immer äußerste Vorsicht walten lassen. Man vergisst gerne einmal, eine Variable umzubenennen oder den Code überall abzuändern. Beispiel 6.9: DRY: Matrixoperationen Erinnern Sie sich noch an die Aufgabe aus dem letzten Semester, in der Sie Methoden geschrieben haben, die Matrixoperationen durchführen? Vermutlich haben Sie zu diesem Zeitpunkt die komplette Logik in jeweils eine Methode geschrieben. Aber wenn man sich die einzelnen Methoden (add und multiply) betrachtet, findet man Gemeinsamkeiten: Bei beiden Methoden müssen die übergeben Matrizen „rechteckig“ sein. Bei beiden Methoden dürfen die Matrizen nicht leer sein, also in einer Dimension die Ausmaße 0 haben. Bei diesen Abfragen handelt es sich eindeutig um Methoden, die man auslagern kann. Sie könnten also eine Methode isRectangular() und eine Methode isEmpty() schreiben, die diese Abfragen vornehmen. 7 Genaue Herkunft nicht bekannt. Eventuell John F. Woods. B Seite 30 Studienbrief 6 Coding Conventions und Best Practices 6.5.4 Methods should do one thing (nach Martin [2008] Eine weitere wichtige Regel, die Sie beherzigen sollten, ist die folgende: Eine Methode sollte immer nur eine konkrete Logik (oder einen konkreten Algorithmus) implementieren. Wenn Ihre Methode mehrere Dinge implementiert, sollten Sie Refactoring betreiben und die Methode in einzelne Methoden aufteilen. Das Originalzitat, das dieser Regel voraus geht, stammt aus dem Buch Clean Code: A Handbook of Agile Software Craftsmanship von Robert C. Martin: Functions should do one thing. They should do it well. They should do it only. — Martin [2008] Es ist selbstverständlich in Ordnung, wenn in Ihrer Methode weitere Methoden aufgerufen werden, die unterschiedliche Dinge tun. Dies lässt sich beim Programmieren auch gar nicht vermeiden. Die Regel bezieht sich auf die Implementierung unterschiedlicher Dinge in der selben Methode. Wie kann man feststellen, ob sich eine Methode an diese Regel hält? Hierzu können Sie sich folgende Frage stellen: „Gibt es in meiner Methode einzelne Abschnitte, die sich konkret benennen kann?“ Falls Sie diese Frage mit ja beantworten können, dann können Sie den entsprechenden Code in eine eigene Methode auslagern. B Beispiel 6.10: One method, one thing: Matrixoperationen Betrachten wir wiederum die Aufgabe, Matrixoperationen zu implementieren. Zunächst nur die add-Methode: public static int[][] add(int[][] first, int[][] second) { if ( !(first != null && second != null && first.length > 0 && first.length == second.length && first[0].length > 0 && second[0].length > 0)) { return null; } int numRows = first.length; int numCols = first[0].length; // check if every row has same length // length should be identical to length of first row for(int i = 0; i<first.length; i++) { if (first[i] != numCols || second[i] != numCols) { return null; } } 6.5 Best Practices // add matrizes int[][] result = new int[numRows][numCols]; for (int row = 0; row < numRows; row++) { for (int col = 0; col < numCols; col++) { result[row][col] = first[row][col] + second[row][col]; } } return result; } Der Code hat einige offensichtliche Schwachstellen: Er ist relativ lang, es gibt sehr viele Abfragen, es existiert keine Dokumentation und man ist nicht sicher, warum manche Abfragen gemacht werden. Was ebenso auffällt: Die Methode heißt add, sollte also zwei Matrizen addieren. Hauptanteil an der Methode hat aber der Code, der prüft, ob sich diese zwei Matrizen überhaupt addieren lassen. Wir können diesem Codeteil einen konkreten Namen geben: canAddMatrizes (Oder ein ähnlicher Name). Wir sollten diesen Code daher auslagern: public static int[][] add(int[][] first, int[][] second) { if (!(canAddMatrizes(first, second))) { return null; } int numRows = first.length; int numCols = first[0].length; int[][] result = new int[numRows][numCols]; for (int row = 0; row < numRows; row++) { for (int col = 0; col < numCols; col++) { result[row][col] = first[row][col] + second[row][col]; } } return result; } Allein durch diese Ausgliederung ist der Code deutlich übersichtlicher geworden. Die Methode implementiert nun wirklich nur die Addition selbst – die Abfrage, ob addiert werden kann, ist ausgelagert. Seite 31 Seite 32 Studienbrief 6 Coding Conventions und Best Practices An dieser Stelle ist aber nicht Schluss: In der Methode canAddMatrizes sind ebenfalls Kandidaten, denen man eine eigene Methode geben kann. Überlegen Sie dafür, was genau abgeprüft wird: • Ob die Matrizen rechteckig sind (also in jeder Zeile die gleiche Anzahl von Einträgen ist) • Ob die zwei Matrizen die gleichen Dimensionen haben • Ob die Matrizen in jeder Dimension einen Eintrag haben (also keine Spalten/Zeilen mit Länge 0) Die Methode canAddMatrizes kann also folgendermaßen aussehen: private static boolean canAddMatrizes(int[][] first, int[][] second) { if(first != null && second != null && isRectangular(first) && isRectangular(second) && areSameDimensions(first, second) && hasEntryInEveryDimension(first) && hasEntryInEveryDimension(second)) { return true; } else { return false; } } Dieser Code lässt sich deutlich einfacher lesen als der bisherige, da man direkt erkennt, was abgefragt wird und welche Voraussetzungen für die Addition notwendig sind. K Kontrollaufgabe 6.4: MatrixOperation: Multiply Betrachten Sie Ihren eigenen Code bzw. konkret Ihre eigene Implementierung der Methode multiply. Führen Sie ein Refactoring durch, indem Sie Methoden auslagern und ggf. Variablen umbenennen. Prüfen Sie auch, ob es Gemeinsamkeiten zur Methode add gibt. 6.5.5 Magic Numbers Magic Numbers sind Nummern, die in Methoden auftauchen. Ihr Zweck bzw. ihre Herkunft sind nicht immer sofort eindeutig. Solche Nummern werden gerne als Kodierung benutzt, aber auch für z. B. für Einstellungen. Als Beispiel soll die Überprüfung einer Passwortlänge dienen: public boolean isValidPassword(String password) { if (password.length() >= 8) { 6.5 Best Practices Seite 33 return true; } else { return false; } } Bei der Zahl 8 handelt es sich um eine typische Magic Number. Die Zahl scheint es der Luft gegriffen. Wenn man die valide Passwortlänge anpassen möchte, muss man erst umständlich im Code suchen. Es kann auch noch schlimmer sein: Die Passwortlänge kann an mehreren Stellen im Code auftauchen. So könnten wir beim Anpassen der Länge theoretisch eine Zahl vergessen. Solche Nummern sollten immer als Konstanten gesetzt werden. So ist es möglich, schnelle Anpassungen an der Passwortpolicy vorzunehmen, ohne den gesamten Code zu durchsuchen: private static final int MIN_PASSWORD_LENGTH = 8; // ... public boolean isValidPassword(String password) { if (password.length() >= MIN_PASSWORD_LENGTH) { return true; } // etc. } 6.5.6 Strings bauen mit StringBuilder Es kommt oft vor, dass man für Anwendungen im Terminal Strings aneinanderreiht, um eine große Zeichenkette zu erzeugen. Als Beispiel betrachten wir eine Methode, die ein übergebenes Array in folgender Schreibweise darstellt: [1,2,4,8]. Den Code finden Sie in Quelltext 6.2. Quelltext 6.2: "Die Methode arrayToString()" 1 2 3 4 5 6 7 8 9 10 11 12 public String arrayToString(int[] array) { String arrayAsString = "["; for(int i = 0; i < array.size; i++) { if (i != 0) { arrayAsString = arrayAsString + ","; } arrayAsString = arrayAsString + array[i]; } arrayAsString = arrayAsString + "]"; 13 14 15 return arrayAsString; } Q Seite 34 Studienbrief 6 Coding Conventions und Best Practices Der obere Quelltext ist korrekt und funktioniert ohne Einschränkungen. Das Problem, das wir an dieser Stelle ansprechen wollen, hat etwas mit dem Arbeitsspeicher zu tun. Ein String-Objekt ist immutable (dt. : unveränderbar. Der Wert bzw. die Zeichenkette, die in einem String-Objekt gespeichert ist, kann sich, nachdem Sie festgelegt wurde, nicht mehr verändern. Jedes mal, wenn wir mit dem Zuweisungsoperator dem String einen neuen Wert zuweisen, wird ein komplett neues Objekt anlegt. Dies führt natürlich zu einem erhöhten Rechenaufwand, da der Inhalt des String-Objekts immer kopiert werden muss. Wenn Sie eine Zeichenkette bauen möchten, sollten Sie dazu dazu Klasse StringBuilder verwenden. Diese verfügt (unter anderem) über eine Methode append, mit der Sie Zeichenketten direkt anhängen können. StringBuilder ist nicht immutable, sodass die Laufzeit besser ist und nicht unnötig Objekte angelegt werden. Wenn Sie am Ende das StringBuilder-Objekt in einen String umwandeln möchten, nutzen Sie die Methode toString(). In Quelltext 6.3 finden Sie wiederum die Methode arrayToString(), diesmal unter Verwendung von StringBuilder. Q Quelltext 6.3: "Die Methode arrayToString() unter Verwendung von StringBuilder" 1 2 3 4 5 6 7 8 9 10 11 12 public String arrayToString(int[] array) { StringBuilder arrayAsString = new StringBuilder("["); for(int i = 0; i < array.size; i++) { if (i != 0) { arrayAsString.append(","); } arrayAsString.append(array[i]); } arrayAsString.append("]"); 13 14 15 K return arrayAsString.toString(); }] Kontrollaufgabe 6.5: Immutable Strings Was glauben Sie: Wie wird verhindert, dass der Wert eines Strings nach der ersten Zuweisung verändert werden kann? 6.5.7 Wenn es sicher sein soll: SecureRandom Verwenden Sie niemals die Klasse Random, im kritische Abschnitte oder Verfahren durch Zufallszahlen sicher zu machen. Benutzen Sie stattdessen immer die Klasse SecureRandom.8 8 https://docs.oracle.com/javase/8/docs/api/java/security/SecureRandom.html 6.5 Best Practices Seite 35 Wenn Sie der Hintergrund interessiert, können Sie Exkurs 6.5 lesen. Exkurs 6.5: SecureRandom und Random Was genau ist der Unterschied zwischen SecureRandom und Random? Warum ist SecureRandom besser? Wir werden in diesem Exkurs kurz versuchen, diese Fragen zu beantworten. Zunächst einmal sollten Sie wissen, wie die Zufallszahlen in Java zustande kommen. Grundlage hierzu ist ein so genannter Seed. Dieser wird genutzt, um die Zufallszahlen zu berechnen. Wenn ein Angreifer den seed kennt, kann er Ihre Zufallszahlen selbst errechnen. Sie können zum Testen bei einer Instanz der Klasse Random den seed selbst angeben (siehe API) und zwei Zufallszahlen ausgeben lassen. Wenn Sie das Programm erneut aufrufen, werden exakt die selben „Zufallszahlen“ berechnet. Wenn Sie selbst keinen seed angeben, nutzt der Konstruktor von Random die aktuelle Systemzeit in Millisekunden. Um aus dem seed eine Zufallszahl zu generieren, wird ein Generator genutzt. Wir gehen an dieser Stelle nicht tiefer darauf ein, wie so ein Generator funktioniert. Wichtig ist aber: Bei der Klasse Random wird zum Erzeugen der Zufallszahlen ein linearer Kongruenzgenerator genutzt. Dieser ist für sicherheitskritische Anwendungen absolut ungeeignet, da anhand von nur zwei Zufallszahlen der seed in relativ kurzer Zeit berechnet werden kann. Die Klasse SecureRandom erzeugt kryptographisch starke Zufallszahlen, die in kritischen Anwendungen verwendet werden können. Für weitere Details können Sie einen Blogeintrag von James Roper (Roper [2010]) lesen, der das Thema kurz und leicht verständlich erklärt.9 6.5.8 Eindeutigkeit Seien Sie in Ihrem Code immer so spezifisch wie möglich. Vermeiden Sie Mehrdeutigkeiten und Interpretationsspielraum. Betrachten wir dazu ein kurzes Beispiel: public class Human { private int height; public void setHeight(int height) { this.height = height; } } Das Problem an diesem Code ist: Es ist nicht zu erkennen, in welcher Einheit die Größe des Menschen gespeichert ist. Es könnten Meter, Zentimeter, Millimeter oder Zoll sein. Ein Nutzer, der die Methode setHeight() aufruft, kann nicht wissen, in welcher Einheit die Höhe übergeben werden muss. Ihr erster Gedanke könnte sein, die Einheit als Kommentar einzufügen: E Seite 36 Studienbrief 6 Coding Conventions und Best Practices private int height; // in cm Diese Lösung hilft vielleicht Ihnen selbst und anderen Programmierern weiter, aber nicht dem Nutzer Ihres Codes. Dieser sieht weiterhin nur die Methode setHeight(). Durch geschickte Variablen- und Parameterbenennung können Sie dieses Problem lösen: private int heightInCm; public void setHeight(int heightInCm) // ... Durch diese Benennung ist eindeutig, in welcher Einheit die Größe gespeichert ist und was der Methode übergeben werden muss. Es gibt allerdings noch eine bessere Lösung: public void setHeightInCm(int heightInCm) { // ... } Auf diese Weise erkennt der Nutzer bereits anhand des Methodennamens, in welcher Einheit die Größe übergeben werden muss. Zudem gibt diese Lösung Ihnen die Möglichkeit, weitere Methoden zu implementieren, wie beispielsweise setHeightInMm(). Die interne Speicherstruktur ist so vor dem Nutzer versteckt. 6.5.9 Das Rad nicht neu erfinden Bei einem größeren Projekt ist man immer versucht, jeden Teil des Programms selbst zu schreiben. Stellen Sie sich einmal vor, Sie möchten eine Webseite programmieren, die einen Chat, ein Forum und einen kleinen Shop bietet. Wenn Sie dieses Projekt angehen, um etwas zu lernen, ist es völlig legitim, jede Komponente selbst zu schreiben. Wenn Sie allerdings schnell Resultate sehen möchten oder die Webseite professionell nutzen möchten, sollten Sie lieber auf bereits existente Programme zurückgreifen. Bei fast allen Programmen gilt: Irgendjemand hat so etwas bereits vor Ihnen gemacht. Vermutlich hat er es besser gemacht. Eventuell existiert dieses Programm bereits seit Jahren, hat mehrere Tausend Anwender, eine aktive Community und hat bereits geschlossene Sicherheitslücken. In so einem Fall ist es durchaus angebracht, bereits existierende Lösungen zu nutzen und, sofern es die Lizenz erlaubt, Ihren Bedürfnissen anzupassen. Dieser Ratschlag gilt insbesondere bei sicherheitsrelevanten Komponenten: Versuchen Sie nicht, einen Verschlüsselungsalgorithmus selbst zu implementieren (höchstens zu Übungszwecken). Greifen Sie auf ausgereifte, bereits existierende Lösungen zurück. 6.5 Best Practices Seite 37 6.5.10 Usereingaben validieren Folgen Sie immer dem Grundsatz: Traue niemals den Eingaben eines Users. Gehen Sie immer vom Schlimmsten aus. Vertrauen Sie nicht darauf, dass der Nutzer Ihr Programm so verwendet, wie Sie es möchten. Zunächst muss ganz klar gesagt werden: Geben Sie nicht dem Nutzer die Schuld. Bei einer Sicherheitslücke oder einem ungeplanten Systemabsturz ist die Schuld zunächst beim Programmierer zu suchen. Natürlich gibt es Nutzer, die in böswilliger Absicht Ihr Programm zum Absturz bringen oder Daten stehlen wollen. Aber auch hier muss es zunächst eine Sicherheitslücke existieren, die ein Programmierer verursacht hat. Deshalb: Sobald der Nutzer Einfluss auf Ihr Programm nehmen kann – sei es durch direkte Eingaben oder durch das Verändern einer Datei, aus die Sie lesend zugreifen – müssen die Daten validiert werden. Validierung bedeutet, zu prüfen, ob die Daten gültig sind. Sind die Daten nicht valide, bleibt Ihnen überlassen, wie Sie damit umgehen. Bei manchen Fehlern kann es durchaus legitim sein, das Programm zu beenden. Von anderen Fehlern kann sich erholt werden, sodass das Programm weiterlaufen kann. Man muss sich jedoch eingestehen, dass es das perfekte Programm nur in der Theorie gibt. Man kann leider nicht an alle Eventualitäten und Sonderfälle denken, sodass Sicherheitslücken nie ausgeschlossen sind. Aber Sie können es eventuellen Angreifern schwer machen – indem Sie zumindest die offensichtlichen und einfachen Angriffsszenarien berücksichtigen und verhindern. Beispiel 6.11: Bankkonto Teil 1 In Studienbrief 5 mussten Sie ein Bankkonto implementieren, auf dem Geld eingezahlt und abgeholt werden konnte. Betrachten wir im folgenden nochmal ein vereinfachtes Beispiel. Der Geldbetrag wird nur in Euro angegeben, und der Dispo spiegelt den minimalen, negativen Betrag wieder, der auf dem Konto sein darf: public class BankAccount() { private int euro; private int dispo; private static final int DEFAULT_DISPO = -500; public BankAccount(euro, dispo) { this.euro = euro; this.dispo = dispo; } public BankAccount(euro) { this(euro, DEFAULT_DISPO); } B Seite 38 Studienbrief 6 Coding Conventions und Best Practices public void deposit(int euro) { this.euro += euro; } public void draw(int euro) { this.euro -= euro; } } Der Code ist, was die Eingaben der Methoden angeht, viel zu gutgläubig. Was würde passieren, wenn jemand bei der Methode deposit() einen negativen Betrag angibt? Das Konto des Empfängers würde dann um den Geldbetrag verringert werden. Beim Abheben haben wir auch nicht auf den Dispo geachtet! B Beispiel 6.12: Bankkonto Teil 2 Wir müssen also gewisse Abfragen in den Code einbauen. Zudem müssen wir uns entscheiden, was passiert, wenn eine falsche Eingabe vorgenommen wird. Wir entscheiden uns hier dafür, den Erfolg oder Misserfolg der Transaktion als boolean-Wert zurückzugeben: public void deposit(int euro) { if(euro < 0) { return false; } this.euro += euro; return true; } public void draw(int euro) { if(euro < 0) { return false; } if(this.euro - euro < this.dispo) { return false; } this.euro -= euro; return true; } Jetzt haben wir darauf geachtet, dass keine negativen Beträge übergeben 6.5 Best Practices Seite 39 werden können. Zudem haben wir den Dispo beim Geldabheben mit einbezogen. Aber hier enden die möglichen Falscheingaben noch nicht. Beispiel 6.13: Bankkonto Teil 3 B Wir wir in Studienbrief 1 gelernt haben, wird für eine Variable ein gewisser Speicherplatz reserviert. Bei einer int-Variable sind dies 32 Bit. Eine solche Variable hat also einen Maximal- und einen Minimalwert. Wenn wir über den Maximalwert hinaus addieren, kommt es zum zu genannten Overflow. Das oberste Bit wird auf 1 gesetzt, der Geldbetrag wird negativ. Probieren wir dies einmal aus. Angenommen, wir haben ein Konto, auf dem sich 500 Euro befinden: account.deposit(Integer.MAX_VALUE); Hier kommt es garantiert zum Overflow, da MAX_VALUE die maximal darstellbare positive Zahl ist, die in einer int-Variable gespeichert werden kann. Wir müssen also darauf achten, dass so etwas nicht passieren kann: public void deposit(int euro) { if(euro < 0) { return false; } if(this.euro + euro < euro) { return false; } this.euro += euro; return true; } Mit dieser einfachen Abfrage haben wir einen Overflow verhindert. Wenn wir etwas einzahlen, darf der neue Betrag nicht kleiner sein als der ursprüngliche Betrag. Beispiel 6.14: Bankkonto Teil 4 Aber was ist mit dem umgekehrten Fall? Was, wenn wir soviel Geld abheben, dass wir einen positiven Betrag auf dem Konto haben? Hier sprechen wir von einem Underflow. Und so verhindern wir ihn: public void draw(int euro) { if(euro < 0) B Seite 40 Studienbrief 6 Coding Conventions und Best Practices { return false; } if(this.euro - euro < this.dispo) { return false; } if(this.euro - euro > euro) { return false; } this.euro -= euro; return true; } 6.6 Zusammenfassung In diesem Studienbrief sind wir ausführlich auf Coding Conventions und einige Best Practices für Java eingegangen. Zunächst haben wir wiederholt, wie Klassen, Methoden und Variablen in Java zu benennen sind. Im Anschluss wurde darauf eingegangen, wie lang beispielsweise eine Codezeile sein sollte und wie der Zeilenumbruch am besten umzusetzen ist. Um Ihr Programm zu dokumentieren, sollten Sie bevorzugt Javadoc einsetzen. Durch die einfach zu erlernende Notation und dem gleichnamigen Tool können Sie aus Ihrer Dokumentation ein interaktives HTML-Dokument erzeugen, wie Sie es bereits von der Java-API kennen. Über Tags können Sie beispielsweise Parameter näher dokumentieren oder auf andere Methoden und Klassen verweisen. Bei den Best Practices haben wir eine Vielzahl von Empfehlungen betrachtet, durch deren Beherzigung Sie besseren Code verfassen können. Wir mussten uns an dieser Stelle aber auch eingestehen, dass es nicht den perfekten Code gibt. Das sollte Sie aber nicht davon abhalten, Ihren Code so gut wie möglich zu verfassen. Wenn wir diesen Studienbrief in einem Satz zusammenfassen müssten, würden wir uns wiederum des verbreiteten Zitats bedienen: Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live.10 10 Genaue Herkunft nicht bekannt. Eventuell John F. Woods. 6.7 Übungen Seite 41 6.7 Übungen Den Lösungscode zu den Übungen 6.1 bis 6.4 finden Sie am Ende dieses Dokuments sowie im einfprog-Git-Repository. Falls Sie es im letzten Semester noch nicht ausgecheckt haben, müssen Sie Folgendes im Terminal eingeben: git clone ir99ijoj:einfprog Anstelle von ir99ijoj müssen Sie den Hostnamen eintragen, den Sie für den Gitserver gewählt haben. Wenn Sie sich an die Videos gehalten haben, sollte der Hostname Ihrem Nutzernamen entsprechen. Die Übungen 6.1 bis 6.5 sind optional. Nutzen Sie diese Übungen, um Ihr Wissen wieder aufzufrischen und routinierter beim Programmieren zu werden. Die Übung 6.6 ist nicht optional. Übung 6.1: FizzBuzz Ü Eine äußerst beliebte Testfrage bei Bewerbungsgesprächen ist der so genannte FizzBuzz-Test. Die Aufgabenstellung ist dabei Folgende: Schreiben Sie ein Programm, das die Zahlen von 1 bis 100 ausgibt. Wenn die Zahl durch 3 teilbar ist, geben Sie statt der Zahl „Fizz“ aus. Wenn die Zahl durch 5 teilbar ist, geben Sie statt der Zahl „Buzz“ aus. Wenn die Zahl durch 3 und durch 5 teilbar ist, geben Sie „FizzBuzz“ aus. Lösen Sie gerade gestellte Aufgabe. Geben Sie dabei jede Zahl oder Zeichenkette in einer eigenen Zeile aus. Übung 6.2: The Programmer’s Drinking Song Der „Programmer’s Drinking Song“ hat folgenden Text: 99 little bugs in the code, 99 bugs in the code, Fix one bug, compile it again, 100 little bugs in the code. (go to start if bugs>0) Implementieren Sie ein Programm, das den Song so lange singt, bis die Anzahl der Bugs < 0 ist. 10 https://jazzy.id.au/2010/09/20/cracking_random_number_generators_part_1.html Ü Seite 42 Ü Studienbrief 6 Coding Conventions und Best Practices Übung 6.3: Pferderennen Implementieren Sie ein Pferderennen, bei denen die Pferde nacheinander eine zufällige Anzahl an Schritten (zwischen -2 und 6) vor oder zurück gehen. Die Rennstrecke soll dabei eine gewisse Länge und eine gewisse Anzahl an Pferden haben. Die Länge der Strecke wird beim Anlegen übergeben. Die Pferde können extra angelegt und über eine Methode addHorse übergeben werden. Die Methode start startet das Rennen. In der Methode wird auf jedem Pferd in einer Schleife die Methode start aufgerufen. Dies geschieht so lange, bis ein Pferd die Distanz zurückgelegt hat. Beim Aufruf jeder run-Methode gibt das Pferd in dem Terminal aus, was es gerade tut. Denken Sie sich einige Sätze dazu aus. Mit einer 30%igen Chance bewegt es sich gar nicht, sondern frisst Gras! Jedes Pferd hat natürlich einen Namen, damit man Wetten abschließen kann. Die Wetten implementieren wir an dieser Stelle nicht. Achten Sie darauf, dass die Pferde aus Boxen starten und dadurch nicht beliebig rückwärts gehen können. Angenommen, wir haben eine Rennstrecke der Länge 8 und 3 teilnehmende Pferde. Folgende Beispielausgabe könnte im Terminal erscheinen: Diamond Star takes 3 steps forward. Go, Diamond Star! Heisses Eisen eats gras. Pacific Popsicle tries to step backwards. But there is the box! Diamond Star takes 4 steps forward. Go, Diamond Star! Heisses Eisen takes 6 steps forward. Go, Heisses Eisen! Pacific Popsicle eats gras. Diamond Star takes 2 steps backwards! It seems confused. Heisses Eisen takes 3 steps forward. Go, Heisses Eisen! Heisses Eisen wins the race! Ü Übung 6.4: Fibonacci Die Fibonacci-Folge ist eine unendliche Reihe von Zahlen. Die nächste Zahl in der Reihe wird aus der Summe ihrer beiden Vorgänger berechnet.Die Folge beginnt mit den Zahlen 1,1. Hier ein kleiner Ausschnitt: 1, 1, 2, 3, 5, 8, 13, 21, 34 Schreiben Sie ein Programm, das die Fibonacci-Reihe bis zum n-ten Element ausgibt. Das n wird der jeweiligen Methode als Parameter übergeben. 6.7 Übungen Übung 6.5: Code verstehen Seite 43 Ü Was tut folgender Code? // a and b are integers a = a - b; b = b + a; a = b - a; Übung 6.6: Codeverständnis, Dokumentation und Testing (25 Punkte) Deadline für Bonuspunkte: 05. Juni 2015 Spätestens jetzt müssen Sie das Reposity einfprog auschecken. Eine Anleitung dazu finden Sie auf Seite 41. In diesem Repository finden Sie unter SB6/Aufgabe_6 eine .java-Datei. Versuchen Sie, den Code zu verstehen. Ihre Aufgaben sind die Folgenden: • Dokumentieren Sie den Code • Führen Sie ein Refactoring durch • Schreiben Sie für den Code im Anschluss Tests (JUnit) Laden Sie das Ergebnis in Ihr Repository unter SB6/Aufgabe_6/ hoch. Schreiben Sie im Anschluss eine Email an [email protected]. Ihr Code wird manuell bewertet. Ü 6.7 Übungen Verzeichnisse Seite 45 Liste der Lösungen zu den Kontrollaufgaben Liste der Lösungen zu den Kontrollaufgaben Lösung zu Kontrollaufgabe 6.1 auf Seite 17 run -- Methode isValid -- Methode SPEED_OF_LIGHT -- Konstante Browser -- Klasse (o. Interface) file -- Variable Deletable -- Interface deleteLast -- Methode firstValue -- Variable Lösung zu Kontrollaufgabe 6.3 auf Seite 24 Das Zweierkomplement wird folgendermaßen gebildet: Alle Bits negieren, dann plus Eins rechnen. Wenn die Zahl eine Zweierpotenz ist, steht nur an einer einzigen Stelle eine Eins, sonst nur Nullen. Bei der Negation steht nun an der Stelle, wo vorher die Eins stand, eine Null. Sonst besteht die negierte Zahl nur aus Einsen. Wenn Sie nun plus Eins rechnen, steht an der Stelle, an der im Original eine Eins stand, wieder eine Eins. Durch den bitweisen Und-Operator kommt nun wieder die OriginalZahl heraus. Wenn die Zahl keine Zweierpotenz ist, entsteht bei der Verundung nicht die Originalzahl. Probieren Sie dies einfach an einigen Beispielen aus. Lösung zu Kontrollaufgabe 6.5 auf Seite 34 Die Länge und der Inhalt des Strings sind final. Nach dem Aufruf des Konstruktors können die Werte nicht mehr verändert werden. Seite 47 Verzeichnisse Seite 49 Verzeichnisse I. Beispiele Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel Beispiel 6.1: 6.2: 6.3: 6.4: 6.5: 6.6: 6.7: 6.8: 6.9: 6.10: 6.11: 6.12: 6.13: 6.14: Namenskonventionen bei Interfaces . . . . . . Interface als Suffix . . . . . . . . . . . . . . . . Namenskonventionen bei Methoden . . . . . . Namenskonvention bei Konstanten . . . . . . . Zeilenumbruch bei Nicht-Zuweisungsoperatoren Zeilenumbruch bei Zuweisungsoperatoren . . . Zweierpotenz . . . . . . . . . . . . . . . . . . . Dokumentation der Java-API . . . . . . . . . . . DRY: Matrixoperationen . . . . . . . . . . . . . One method, one thing: Matrixoperationen . . . Bankkonto Teil 1 . . . . . . . . . . . . . . . . . Bankkonto Teil 2 . . . . . . . . . . . . . . . . . Bankkonto Teil 3 . . . . . . . . . . . . . . . . . Bankkonto Teil 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 14 16 19 20 23 27 29 30 37 38 39 39 . . . . . . . . . . . . aus Passig . . . . . . . . . . . . . . . . . . . . . . [2013]) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 21 21 23 35 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 21 24 32 34 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 II. Exkurse Exkurs Exkurs Exkurs Exkurs Exkurs 6.1: 6.2: 6.3: 6.4: 6.5: Einrückung in Python . . . . . . . . . . . Was jeder über Encoding wissen sollte . . T_PAAMAYIM_NEKUDOTAYIM (entnommen Tasks in Eclipse . . . . . . . . . . . . . . SecureRandom und Random . . . . . . . . III. Kontrollaufgaben Kontrollaufgabe Kontrollaufgabe Kontrollaufgabe Kontrollaufgabe Kontrollaufgabe 6.1: 6.2: 6.3: 6.4: 6.5: Syntaxbausteine erkennen Encoding . . . . . . . . . . Zweierpotenz . . . . . . . MatrixOperation: Multiply . Immutable Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IV. Tabellen Tabelle 6.1: Die wichtigsten Javadoc-Tags V. Literatur Google. Google java style, 2014. URL http://google-styleguide.googlecode.com/svn/trunk/javaguide. html. Martin Hitz, Gerti Kappel, Elisabeth Kapsammer, and Werner Retschitzegger. UML@Work. dpunkt Verlag, 2005. Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008. Oracle. Code conventions for the java programming language, 1999. URL http://www.oracle.com/ technetwork/java/codeconvtoc-136057.html. Johannes Passig, Katrin Jander. Weniger schlecht programmieren. O’ Reilly, 2013. Seite 50 Verzeichnisse James Roper. Cracking random number generators - part 1, 2010. URL https://jazzy.id.au/2010/09/20/ cracking_random_number_generators_part_1.html. Joel Spolsky. The absolute minimum every software developer absolutely, positively must know about unicode and character sets (no excuses!), 2003. URL http://www.joelonsoftware.com/articles/Unicode.html. Liste der Lösungen zu den Übungen Liste der Lösungen zu den Übungen Übungsaufgaben zu Studienbrief 6 Lösung zu Übung 6.1 auf Seite 41 public static void doFizzBuzz() { for(int i = 1; i<101; i++) { if(i % 15 == 0) { System.out.println("FizzBuzz"); } else if(i % 5 == 0) { System.out.println("Buzz"); } else if(i % 3 == 0) { System.out.println("Fizz"); } else { System.out.println(i); } } } Lösung zu Übung 6.2 auf Seite 41 public static void doDrinkingSong() { int numberOfBugs = 99; while (numberOfBugs > 0) { sing(numberOfBugs); numberOfBugs++; } } public static void sing(int numberOfBugs) { StringBuilder songBuilder = new StringBuilder(); songBuilder.append(String.format( "%d little bugs in the code,\n", numberOfBugs)); songBuilder.append(String.format( "%d bugs in the code,\n", numberOfBugs)); songBuilder.append(String.format( "Fix one bug, compile it again,\n")); Seite 51 Seite 52 Liste der Lösungen zu den Übungen songBuilder.append(String.format( "%d bugs in the code,\n", numberOfBugs + 1)); System.out.println(songBuilder.toString()); } Lösung zu Übung 6.3 auf Seite 42 Klasse HorseRace import java.util.ArrayList; public class HorseRace { private ArrayList<Horse> horses; private int trackLength; public HorseRace(int trackLength) { this.trackLength = trackLength; horses = new ArrayList<Horse>(); } public void addHorse(Horse horse) { horses.add(horse); } public void start() { boolean isFinished = false; while(!(isFinished)) { for(Horse horse : horses) { horse.run(); if(horse.passedGoal(trackLength)) { isFinished = true; String winningString = String.format("%s wins the race", horse.getName()); System.out.println(winningString); break; } } System.out.println(); } } public static void main(String[] args) { HorseRace race = new HorseRace(12); Horse diamond = new Horse("Diamond Star"); Horse eisen = new Horse("Heisses Eisen"); Liste der Lösungen zu den Übungen race.addHorse(diamond); race.addHorse(eisen); race.start(); } } Klasse Horse import java.util.Random; public class Horse { private String name; private int position; private private private private private final final final final final String String String String String BOX_STRING; EATING_STRING; CONFUSED_STRING; FORWARD_STRING; STAND_STRING; private static final int GRAS_EATING_PERCENT = 30; private static final int MAX_STEP_VALUE = 6; private static final int MIN_STEP_VALUE = -2; public Horse(String name) { this.name = name; this.position = 0; BOX_STRING = String.format( "%s tries to step backwards. But there is the box!", this.name); EATING_STRING = String.format( "%s eats gras.", this.name); CONFUSED_STRING = String.format( "%s takes STEPS steps backwards! It seems confused." , this.name); FORWARD_STRING = String.format( "%s takes STEPS steps forward. Go, %s!", this.name, this.name); STAND_STRING = String.format( "%s does not move. Did it forget how to run?", this.name); } public String getName() { return this.name; } public boolean passedGoal(int trackLength) Seite 53 Seite 54 Liste der Lösungen zu den Übungen { if (this.position >= trackLength) { return true; } else { return false; } } public void run() { if(eatsGras()) { System.out.println(EATING_STRING); } else { int steps = getSteps(); int newPosition = this.position + steps; if(newPosition < 0) { System.out.println(BOX_STRING); this.position = 0; } else if(steps < 0) // horse goes backwards { System.out.println(getConfusedString(steps)); this.position = newPosition; } else if(steps == 0) { System.out.println(STAND_STRING); } else { System.out.println(getForwardString(steps)); this.position = newPosition; } } } public boolean eatsGras() { Random random = new Random(); // value between 0 and 100 int randomValue = (int) (random.nextDouble() * 100); if (randomValue <= GRAS_EATING_PERCENT) { return true; // horse eats gras } else { Liste der Lösungen zu den Übungen return false; } } private int getSteps() { Random random = new Random(); int stepSum = Math.abs(MIN_STEP_VALUE) + MAX_STEP_VALUE ; // random number of steps int randomValue = (int) (random.nextDouble() * stepSum) ; // return random number of steps between minvalue und maxvalue return randomValue + MIN_STEP_VALUE; } public String getConfusedString(int steps) { return getStringWithReplacedSteps(CONFUSED_STRING, steps); } public String getForwardString(int steps) { return getStringWithReplacedSteps(FORWARD_STRING, steps ); } /** * Replaces the String "STEPS" within a given string with the the number of steps. * / ** private String getStringWithReplacedSteps(String oldString, int steps) { return oldString.replace("STEPS", Integer.toString(Math. abs(steps))); } } Lösung zu Übung 6.4 auf Seite 42 public static void doFibonacci(int numberOfEntries) { int firstValue = 0; int secondValue = 1; System.out.println(secondValue); for(int i = 1; i < numberOfEntries && i > 0 ; i++) { int newValue = firstValue + secondValue; Seite 55 Seite 56 Liste der Lösungen zu den Übungen System.out.println(newValue); firstValue = secondValue; secondValue = newValue; } } Lösung zu Übung 6.5 auf Seite 43 Die Werte in a und b werden vertauscht. Anhang Anhang Seite 57