Verwalten von Projekten |
Ziel von Softwareentwicklung ist in der Regel die Erstellung von Artefakten
Jedes Artefakt kann andere Artefakte als Abhängigkeiten (dependencies) benötigen
Leiningen ist ein Tool um Artefakte zu managen.
Zusätzlich zum verwalten von Artefakten kann Leiningen auch:
Die Funktionalität kann zusätzlich mit Plugins erweitert werden.
Clojure läuft auf der Java Virtual Machine (JVM)
JVM Artefakte sind JAR Dateien.
Das Java Artefakt Ökosystem ist um das Tool Maven herumgewachsen.
Clojure Programme können beliebige JARs als Dependencies verwenden.
Wir können also beliebige Java Libraries nutzen.
Ein Maven Artefakt wird identifiziert durch jeweils eine
Group ID
Ein Namespace für alle Artefakte einer Organisation.
In der Regel der umgekehrte Domain Name einer Domain, welche die Organisation kontrolliert. Alternativ der Firmenname oder ein Github Nutzername.
Alles was kein gültiger Java Paketname ist, muss ersetzt werden.
Beispiele:
In Java Paketnamen sind keine Bindestriche (-) erlaubt. Immer wenn das relevant wird, werden diese in Underscores (_) übersetzt.
Artifact ID
Ein lower case Name, der innerhalb der Gruppe eindeutig ist.
Versionsnummer
Wir können selber ein Versionierungsschema mit Zahlen und Punkten wählen
(z.B.: 2, 2.1, 2.1.2)
Die Versionsnummer besteht aus drei Teilen:
MAJOR.MINOR.PATCH2.1.2
Patch Version
2.1.2 $\to$ 2.1.3
Wird erhöht bei rückwärtskompatiblen Bug Fixes.
Minor Version
2.1.2 $\to$ 2.2.0
Wird erhöht bei rückwärtskompatiblen neuen Features.
Major Version
2.1.2 $\to$ 3.0.0
Wird erhöht bei inkompatiblen Änderungen.
Semantic Versioning sagt einem Nutzer der Software:
Zu jeder Version darf es nur genau ein Artefakt geben.
Wenn wir erstmal eine Version veröffentlicht haben, so können wir diese nicht mehr überschreiben.
So kann ich das Artefakt als Dependency verwenden und managen, wann ich Updates davon übernehme.
Wenn eine Version noch Work-In-Progress ist, fügen wir das Suffix -SNAPSHOT zur Version hinzu.
2.1.2-SNAPSHOT
Diese Version können wir beliebig oft überschreiben.
Wir erstellen ein neues Projekt mit Leiningen durch:
lein new app my-cool-project
Das "app" Template ist ein guter Startpunkt für ausführbare Anwendungen.
Das Kommando erzeugt folgende Verzeichnisse und Dateien:
my-cool-project/
├── doc/
│ └── intro.md
├── resources/
├── src/
│ └── my_cool_project/
│ └── core.clj
├── test/
│ └── my_cool_project/
│ └── core_test.clj
├── .gitignore
├── .hgignore
├── CHANGELOG.md
├── LICENSE
├── README.md
└── project.clj
Einen ähnlichen Projektaufbau gibt es in vielen Sprachen und Tools.
.gitignore und .hgignore ignorieren temporäre Dateien und Folder in Git und MercurialREADME.md ist der Einstieg in die Projekt Dokumentation (wird von Gitlab automatisch gerendert)LICENSE, CHANGELOG.md und doc/ enthalten mehr Dokumentation zu Code, Entwicklung, etc.resources ist für Dateien wie Bilder.project.clj
(defproject my-cool-project "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.11.1"]]
:main ^:skip-aot my-cool-project.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})
Der erste Parameter von defproject ist GroupID/ArtifactID
de.th-bingen.inf.ki/my-cool-project
Wenn GroupID und ArtifactID gleich sind, reicht es, einen anzugeben.
my-cool-project
Unter :dependencies können wir alle Artefakte listen, von denen wir abhängen.
Notation ist ein Vektor: [GroupID/ArtifactID "Versionsnummer"]
Wenn GroupID und ArtifactID gleich sind, reicht es, einen anzugeben.
[[org.clojure/clojure "1.11.1"]
[clj-time "0.14.0"]]
Standardquelle für Clojure Dependencies ist clojars.org
Für Java Dependencies ist das Maven Central
Um sicherzustellen, dass wir keine Namenskonflikte haben
(z.B. zwei Libraries die eine Funktion from-file definieren) gibt es Namespaces
Üblicherweise steht am Anfang jeder Datei der Namespace für alle Namen der Datei:
(ns my-cool-project.core)
Jedes folgende def erzeugt einen Namen innerhalb des Namespace
Wir können Namen aus anderen Namespaces verwenden indem wir den fully-qualified name verwenden:
(clojure.string/join ", " [1 2 3])
Um einen Namen aus einem anderen Namespace zu verwenden, muss dieser erst geladen werden:
(require 'clj-time.core)
(clj-time.core/date-time 2023 4 28)
Wir können dem Namespace ein Alias geben um ihn kürzer zu machen:
(require '[clj-time.core :as t])
(t/date-time 2023 4 28)
Dem ns Macro kann direkt übergeben werden, welche Namespaces wir laden wollen:
(ns my-cool-project.core
(:require
[clj-time.core :as t]))
Damit das Laden eines Namespaces funktioniert muss der Pfad und Dateiname dem Namespace entsprechen:
Aus Namespacecom.some-example.my-app
wird die Dateicom/some_example/my_app.clj
Wir können aus einem Namespace auch einzelne oder alle Namen importieren:
(ns my-cool-project.core
(:require
[clj-time.core :refer [now]]))
(ns my-cool-project.core
(:require
[clj-time.core :refer :all]))
Alle Namen sollte man nur selten importieren.
Wenn wir def verwenden, so definieren wir einen Namen im aktuellen Namespace
Wenn wir Namen nur lokal brauchen können wir let verwenden:
(let [x 1
y 2
z (+ x y)]
(println z))
Wir können im Project eine Read-Evaluate-Print-Loop (REPL) starten:
lein repl
Die REPL hat Zugriff auf all unseren Code und die Dependencies.
Wenn unser Projekt ausführbar ist können wir es laufen lassen:
lein run
Es wird die Funktion mit Namen
-main
in dem Namespace ausgeführt, der in project.clj unter
:main konfiguriert ist.
:gen-class ist wichtig, damit die Entrypoint Klasse erstellt wird.
Um unser Artefakt zu erstellen verwenden wir:
lein uberjar
Es wird eine große JAR Datei erstellt, die alle Dependencies enthält und alleine lauffähig ist (in der JVM).
java -jar target/uberjar/my-cool-project-0.1.0-SNAPSHOT-standalone.jar
Leiningen hat auch Funktionen, um eigene Libraries nach Clojars zu deployen.
Plugins
Wir können in project.clj Plugins konfigurieren, um Leiningen zu erweitern
:plugins [[jonase/eastwood "1.4.0"]]
installiert den eastwood Linter
Durch Plugins gibt es neue Kommandos oder anderes Verhalten existierender Kommandos
lein eastwood
Eine mögliche Alternative zu Leiningen ist Boot.
Testing |
Unser eigentlicher Code befindet sich im src Folder
Ausserdem gibt es noch den test Folder für Test Code.
Tests sind Code, der (in der Regel) nicht Teil des Artefakts wird, sondern uns bei der Entwicklung helfen soll.
Tests werden mit dem deftest und dem is Macro definieren:
(deftest my-first-test
(is (= 4 (+ 2 2))))
Das is Macro prüft, ob der folgende Parameter truthy ist und generiert eine Fehlermeldung andernfalls.
Wir können auch eigene Fehlermeldungen definieren:
(deftest my-first-test
(is (= 4 (+ 2 2)) "2 + 2 should be 4"))
Wir können auch mehrere Tests in einem deftest definieren:
(deftest my-first-test
(is (= 4 (+ 2 2)) "2 + 2 should be 4")
(is (= 5 (+ 2 2)) "2 + 2 should be 5"))
Mit dem testing Macro können wir Tests zu logischen Gruppen zusammenfassen.
(deftest my-first-test
(testing "Addition"
(is (= 4 (+ 2 2)))
(is (= 5 (+ 2 2))))
(testing "Subtraction"
(is (= 0 (- 2 2)))))
Tests kommen in einen eigenen Namespace mit dem -test Suffix.
Test-Driven Development (TDD)
Wir schreiben zuerst den Test, den unser Code erfüllen soll.
Anschließend schreiben wir den Code, um den Test "grün" zu machen.
Tests erfüllen mehrere Rollen:
"Schreibe Code so, dass er einfach zu testen ist"
führt oft zu modularerem Code
Pure Functions sind oft einfacher zu testen als komplexe Objekte.
Ohne Tests hat ein Projekt mit
kaum eine Chance gut zu funktionieren.
Gängiges Setup: Für jeden Merge Request laufen in GitLab automatisch Tests und Linter. Nur wenn es keine Probleme gibt, darf ein Merge erfolgen.