Verwalten von Projekten

Ziel von Softwareentwicklung ist in der Regel die Erstellung von Artefakten

  • Ausführbare Software
  • Bibliotheken (Libraries)

Jedes Artefakt kann andere Artefakte als Abhängigkeiten (dependencies) benötigen

Leiningen ist ein Tool um Artefakte zu managen.

  • Welche Abhängigkeiten hat mein Projekt?
  • Wie erstelle ich aus meinem Projekt das fertige Artefakt?

Zusätzlich zum verwalten von Artefakten kann Leiningen auch:

  • Tests ausführen
  • Code analysieren
  • Dokumentation erstellen

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
  • Artifact ID
  • Versionsnummer

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:

  • org.clojure
  • my-cool-project
  • de.th-bingen.inf.ki
  • de.th-bingen.inf.ki.commons

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)

Semantic Versioning

    Die Versionsnummer besteht aus drei Teilen:

  • Major Version
  • Minor Version
  • Patch Version

MAJOR.MINOR.PATCH
2.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:

  • Du solltest immer die aktuellste Patch Version nehmen. Das Upgrade macht sehr sicher keine Probleme.
  • Ein Upgrade der Minor Version sollte mit hoher Wahrscheinlichkeit keine Probleme machen.

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 Mercurial
  • README.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 Namespace
com.some-example.my-app
wird die Datei
com/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:

  • Unser Code tut was er soll
  • Unser Code tut in der Zukunft immer noch was er soll
  • Dokumentation
  • Hilft beim Strukturieren und Entkoppeln von Modulen

"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

  • vielen Entwicklern
  • langer Laufzeit
  • großem Scope
  • 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.