Optionale Argumente in Java

Eclipse Editor Es ist schon viele Jahre her, dass ich in einer internen Schulung meine Kollegen auf die Annehmlichkeiten einer Closure hingewiesen habe. Viele Dinge können eleganter und effizienter formuliert und auch die nebenläufige Programmierung kann strukturierter werden. Leider hat es aber sehr lange gedauert, bis mit den Lambdas dieses Feature auf der Java Plattform angekommen ist. Und es gibt noch mehr Sprachfeatures aktueller dynamischer Sprachen, die wünschenswert sind.

Die Closure wurde schon ewig durch anonyme innere Klassen abgebildet. Bereits damit wird der Programmcode mobiler und gestattet die Gestaltung von Codefragmenten, die in einem alternativen Programmkontext zur Ausführung gebracht werden können. Ein großer Teil der gewünschten Funktionalität konnte damit, gerade auch für eine nebenläufige Programmierung1, auch innerhalb der Java-Sprache abgebildet werden. Vielleicht, so dachte ich schon lange, funktioniert das ähnlich praktikabel auch für die optionalen Argumente?

Optionale Funktionsargumente

Was sind eigentlich diese optionalen Argumente? In Groovy kann eine Funktion Argumente definieren, die nur optional übergeben werden müssen. Über Schlüsselworte werden diese Argumente identifiziert und so reduziert sich gerade bei breiten Funktionsaufrufen die Anzahl unterschiedlicher Signaturen.

def editUser(first = null, last = null, password = null) {
    ...
}
editUser(last: 'Müller')
editUser(password: 'new password)

Hier können die optionalen Argumente mit default-Werten versehen werden, die immer dann genutzt werden, wenn an der aufrufenden Stelle keine alternativen Werte angegeben wurden. Das ist schon ganz nett, entspricht jedoch auch nicht zu 100% meinen Vorstellungen.

Wichtiger als eine Vorbelegung ist die Information, ob der Wert beim Aufruf verfügbar war. So kann dann gezielt im Progammablauf der Funktion auf das Vorhandensein eines Werts reagiert werden.

So etwas müsste sich doch auch in Java implementieren lassen…. In Java gestaltet sich so etwas dann leider etwas schwieriger, denn die Syntax der Sprache lässt sich nicht so leicht ändern. Dennoch habe ich — so denke ich — einen Lösungsansatz gefunden, der sich sinnvoll einsetzen lässt.

Schlüsselworte

Kernstück in meiner Implementierung ist die Klasse Keyword, mit der die Schlüsselworte der eigentlichen Parameter definiert werden können. Jede statische Instanz beschreibt ein bestimmtes Argument und ist Teil der Metadaten eines Funktionsaufrufs.

Die Definition eines Schlüsselworts ist einfach:

public static Keyword<String> A = new Keyword<String>("a");

Auf diese Weise wird ein Schlüsselwort A definiert, das einen String als Wert aufnimmt. Der Parameter im Konstruktor ist allerdings nicht dieser Wert sondern ein Schlüssel, der bei Fehlermeldungen genutzt werden soll.

Im Moment umfasst die Klasse Keyword keine eigene Funktionalitäz. Die Implementierung ist daher recht kurz geraten.

/*
 * Copyright 2015 Ralf Edmund Stranzenbach <ralf@reswi.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.reswi.core.args;

/**
 * Schlüsselwerte der optionalen Argumente.
 * 
 * Mit den Instanzen der {@link Keyword} Klasse werden die eigentlichen
 * Schlüsselwerte für die optionalen Argumente eine Funktion definiert. Deise
 * sollten als finale Instanzen erzeugt werdne, das aus GEschwindigeitsgründen
 * ein Vergleich auf Identitity erfolgt.
 * 
 * Hinweis:
 * 
 * Diese Implementierung ist derzeit nur rudimentär ausgeführt und dient Test-
 * und Evaluationszwecken. Eine künftige weiterführende Ausgestaltung ist
 * sinnvoll. Wünschenswerte Funktionen sind dabei die Durchführung zusätzlicher
 * Validierungen und eine automatische Typumwandlung (z.B. Laden von Instanzen
 * aus der Datenbank).
 * 
 * @author Ralf Edmund Stranzenbach
 */
public final class Keyword<T> {
	/**
	 * Der Name eines {@link Keyword} - kann für Fehlermeldungen genutzt werden.
	 */
	public String name;

	/**
	 * Erzeugt ein neues {@link Keyword}.
	 * 
	 * @param name
	 *            die Bezeichnung des {@link Keyword}
	 */
	public Keyword(String name) {
		this.name = name;
	}
}

Argumente

Eine weitere Klasse wird für die Modellierung eines Arguments genutzt. Diese Klasse Arg ist ein schlichter Container, der Schlüsselwort und Wert in Beziehung setzt.

/*
 * Copyright 2015 Ralf Edmund Stranzenbach <ralf@reswi.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.reswi.core.args;

/**
 * Implementierung eiens optionalen Arguments.
 * 
 * Mit dieser Klasse werden die Argumente für die {@link Keyword} Schlüsselwerte
 * implementiert. Diese Klasse ist dabei eine "Box" um den eigentlichen Wert,
 * die zugleich auch die für die weitere Verarbeitung wichtige Typinformation
 * speichert.
 * 
 * @author Ralf Edmund Stranzenbach
 */
public class Arg<T> {
	/**
	 * Die Typinformation des {@link Arg}, repräsentiert durch ein
	 * {@link Keyword}.
	 */
	private Keyword<T> keyword;

	/**
	 * Der Wert des {@link Arg}.
	 */
	private T value;

	/**
	 * Ereugt eine neue {@link Arg} Instanz
	 * 
	 * @param keyword
	 *            die Typ- bzw. Schlüsselinformation des {@link Arg}.
	 * @param value
	 *            der Wert des {@link Arg}
	 */
	public Arg(Keyword<T> keyword, T value) {
		this.keyword = keyword;
		this.value = value;
	}

	/**
	 * Liefert die Typinformation des {@link Arg}.
	 * 
	 * Die Typinformation ist jeweils ein {@link Keyword}, das als finale
	 * Instanz angelegt werden sollte.
	 * 
	 * @return die Typ- bzw. Schlüsselinformation des {@link Arg}
	 */
	public Keyword<T> getKeyword() {
		return keyword;
	}

	/**
	 * Liefert den Inhalt des {@link Arg} zurück.
	 * 
	 * @return der Injalt ist vom Typ {@code T} und wird bei der Erzeugung des
	 *         {@link Arg} gesetz.
	 *
	 */
	public T getValue() {
		return value;
	}
}

Natürlich kann ein Argument Arg anhand seines Konstruktors erzeugt werden. Es ist jedoch bequemer und zudem drückt es den Willen des Programmieres deutlicher aus, wenn das konkrete Argumet aus seinem Keyword heraus erzeugt wird. Daher enthält die klasse Keyword eine weitere Operation, die ein bequemes und typsicheres Erzeugen von Arg Instanzen gestattet:

	/**
	 * Anlegen eines {@link Arg} zum {@link Keyword}
	 * 
	 * Basierend auf einem {@link Keyword} wird mit dieser Operation ein neues
	 * Argument angelegt, das dann als optionaler Paramezer in einem
	 * Funktionsaufruf verwendet werden kann.
	 * 
	 * @param value
	 *            der im {@link Arg} zu nutzende Wert
	 * @return die neue {@link Arg} Instanz
	 */
	public Arg<T> box(T value) {
		return new Arg<T>(this, value);
	}

Funktionssignatur und Parameterliste

Als letztes nun müssen nun noch die an eine Funktion übergebenen Parameter ausgewertet werden. Diese Aufgabe übernimmt die dritte KLasse Args im Bunde:

/*
 * Copyright 2015 Ralf Edmund Stranzenbach <ralf@reswi.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.reswi.core.args;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Hilfsklasse für die Verarbeitung und Prüfung optionaler Argumente.
 * 
 * Diese Klasse stellt Hilfsfunktionen zur Verfügung, die die Gültigkeit der ane
 * ine Funktion übergebenen Parameter sicherstellt. Zu diesem Zweck wird durch
 * die nutzenden Funktion eine neue Instanz der {@link Args} Klasse erzeugt, die
 * dann eine Prüfung der {@link Arg} Elemente durcgführt.
 * 
 * {@link Keyword} Elemente dürfen nicht doppelt auftreten, {@link Arg} Elemente
 * müssen tatsächlich zur Funkion passen.
 * 
 * Hinweis:
 * 
 * Diese Implementierung ist derzeit nur rudimentär ausgeführt und dient Test-
 * und Evaluationszwecken. Eine künftige weiterführende Ausgestaltung ist
 * sinnvoll. Wünschenswerte Funktionen sind dabei die Durchführung zusätzlicher
 * Validierungen und eine automatische Typumwandlung (z.B. Laden von Instanzen
 * aus der Datenbank).
 * 
 * Beim Auftreten von Fehlern sollte eine weitaus gesprächigere Behandlung des
 * Problems erfolgen, die über das Auslösen einer {@link RuntimeException}
 * hinaus geht.
 * 
 * @author Ralf Edmund Stranzenbach
 */
public class Args {
	/**
	 * Eine {@link Map} mit den {@link Arg} Argumenten des Funktionsaufrufs.
	 */
	Map<Keyword<?>, Arg<?>> _args;

	/**
	 * Die eigentliche Signatur der Funktion.
	 */
	Set<Keyword<?>> _signature;

	/**
	 * Prüft eine Signatur auf doppelte {@link Keyword} Instanzen.
	 * 
	 * @param signature
	 *            Ein Array mit {@link Keyword} Instanze, mit dem die Signatur
	 *            einer Funktion beschrieben wird
	 * @return die Signatur als verifitiertes {@link Set}.
	 */
	private static Set<Keyword<?>> checkSignature(Keyword<?>[] signature) {
		// Prüfen der Signatur
		Set<Keyword<?>> _sig = new HashSet<Keyword<?>>();
		for (Keyword<?> k : signature) {
			if (_sig.contains(k))
				throw new RuntimeException();
			_sig.add(k);
		}
		return _sig;
	}

	/**
	 * Anlegen einer neuen Instanz mit Funktionsargumenten
	 * 
	 * @param signature
	 *            Ein Array mit {@link Keyword} Instanze, mit dem die Signatur
	 *            einer Funktion beschrieben wird
	 * @param args
	 *            Die optionalen Argumente eines Funktionsaufrufs
	 */
	public Args(Keyword<?>[] signature, Arg<?>[] args) {
		this(checkSignature(signature), args);
	}

	/**
	 * Anlegen einer neuen Instanz mit Funktionsargumenten
	 * 
	 * @param signature
	 *            Ein {@link Set} mit {@link Keyword} Instanze, mit dem die
	 *            Signatur einer Funktion beschrieben wird
	 * @param args
	 *            Die optionalen Argumente eines Funktionsaufrufs
	 */
	public Args(Set<Keyword<?>> signature, Arg<?>[] args) {
		// die Signatur ist bereits geprüft...
		_signature = signature;
		for (Keyword<?> k : signature) {
			if (_signature.contains(k))
				throw new RuntimeException();
			_signature.add(k);
		}

		// Prüfen der Argumente
		_args = new HashMap<Keyword<?>, Arg<?>>();
		for (Arg<?> a : args) {
			// Keyword erlaubt
			if (!_signature.contains(a.getKeyword()))
				throw new RuntimeException();

			// Keyword doppelt
			if (_args.containsKey(a.getKeyword()))
				throw new RuntimeException();

			_args.put(a.getKeyword(), a);
		}
	}

	/**
	 * Ermittelt ob ein optionales Argument an die FUnktion übergeben wurde.
	 * 
	 * @param keyword
	 *            das Schlüsselwort {@link Keyword}, mit dem das Argument
	 *            identifiziert wird.
	 * @return <code>true</code>, falls das Argument Bestandteil der Argumente
	 *         des Funktionsaufrufs ist
	 * @throws RuntimeException
	 *             falls ein {@link Keyword} abgefragt ist, das nicht
	 *             Bestandteil der Funktionssignatur ist
	 */
	public boolean isPresent(Keyword<?> keyword) {
		if (!_signature.contains(keyword))
			throw new RuntimeException();

		return _args.containsKey(keyword);
	}

	/**
	 * Ermittelt den Wert eines optionalen Arguments {@link Arg}.
	 * 
	 * @param keyword
	 *            das Schlüsselwort {@link Keyword}, mit dem das Argument
	 *            identifiziert wird.
	 * @return den im Argument {@link Arg} hinterlegte Wert.
	 * @throws RuntimeException
	 *             falls ein {@link Keyword} abgefragt ist, das nicht
	 *             Bestandteil der Funktionssignatur ist oder das gewünschte
	 *             Argument im Funktionsaufruf nicht übergeben wurde.
	 */
	public <T> T value(Keyword<T> k) {
		if (!_signature.contains(k))
			throw new RuntimeException();

		@SuppressWarnings("unchecked")
		Arg<T> a = (Arg<T>) _args.get(k);
		if (a == null)
			throw new RuntimeException();
		return a.getValue();
	}
}

Diese Methode wird durch die aufgerufene Funktion mit ihrer jeweiligen Signatur versehen. Diese Signatur besteht schlich aus eine Liste erlaubter Schlüsselwerte. Die an die Funktion übergebenen Parameter werden gegen diese Liste geprüft und bei doppelten Eingaben oder nicht definierten Schlüsselwerten wird derzeit eine RuntimeException ausgelöst.

Die Verwendung in der service Funktion selbst ist ziemlich einfach:

	public static Keyword<String> A = new Keyword<String>("a");
	public static Keyword<Long> B = new Keyword<Long>("b");

	public static void service(Arg<?>... args) {
		Keyword<?>[] signature = new Keyword<?>[] { A, B };
		Args _args = new Args(signature, args);

Diese Funktion service verfügt entsprechend ihrer Signatur über zwei optionale Parameter A und B. Bereits beim Anlegen der _args Instanz erfolgt dabei eine erste Verfikation der übermittelten Informationen.

Im weiteren Verlauf der Funktion kann dann die Abfolge der Verarbeitungsschritte vom Vorhandensein von Parametern gesteuert werden:

		if (_args.isPresent(A)) 
			System.out.println(_args.value(A));
	
		if (_args.isPresent(B))
			System.out.println(_args.value(B));

Verwendung

Die Schreibweise des eigentlichen Funktionsaufrufs ist tatsächlich immer noch akzeptabel — weniger schlimm als die anonymen Klassen der letzten Jahre.

		service(A.box("hallo"));
		service(B.box(Long.valueOf(2)));

Immerhin lässt sich auf diese Weise bei einigen fachlichen Service der bisher sehr breite Umfang der Schnittstelle wesentlich reduzieren2. Dabei möchte ich hier auf jeden Fall darauf hinweisen, dass gerade in den fachlichen Service eine Vielzahl unterschiedlicher Geschäftsregeln geprüft werden müssen. Diese Prüfungen können jedoch bei einem vergleichbaren Parameter-Modell gezielt nur für die Parameter und Attribute aktiviert werden, die tatsächlich durch die fachlichen Services manipuliert werden.

Optimierungspotenzial besteht jedoch noch bei der Publizierung der Schnittstellenkontrakte. Diese werden derzeit innerhalb der Services sichergestellt und nicht dezidiert nach aussen angezeigt. Hier könne zumindest durch eine Annotation die Signatur mit der Funktion assoziiert werden.

Offene Punkte

Insbesondere die Implementierung der ‘Keyword’ Klasse beschränkt sich im Moment auf das Nötigste. Bei einem produktiven Einsatz wären folgende zusätzliche Funktionen eigentlich wünschenswert.

  • Vorgabewert für nicht übergeben Parameter. Während in diesen Fällen durchaus ein getValue() einen Wert liefert sollte, darf aber isPresent() nicht behaupten, dass der Parameter im Aufruf präsent war.

  • Verbot von null Werten3. Bereits bei der Definition des Keyword sollte es möglich sein zu hinterlegen, dass ein null Wert nicht erlaubt ist. Dabei sollte bereits die Erzeugung von Arg Instanzen für diese Klasse von Schlüsselwerte zu einem Fehler führen.

  • Transformation von Eingabewerte. Optional sollte es möglich sein, die übergebenen Were automatisch zu konvertieren[^4]. Gerade bei fachlichen Services werden häufig skalare Identifier als Parameter genutzt, während innerhalb des Service ein fachliches Objekt genutzt wird.

  • Fehlerbehandlung. Anstelle der RuntimeException sollte ein fachlicher Fehler ServiceException ausgelöst werden, dem der notwendige Kontext in Form von Service, Funktion, Parameter, Fehlertext und gegebenenfalls dem Inhalt übergeben wird. Anders als in den bisherigen Prüfungen durch die Funktionen des AbsractService können jedoch alle Parameter verifiziert werden bevor die Fehlerbehandlung eingeleitet wird.

Irgendwie hoffe ich ja, dass noch der eine oder andere Kommentar zu diesem Text erscheinen wird. Ich kann mir nicht vorstellen, dass diese kurze Anforderungsliste bereits alle sinnvollen Anwendungsmöglichkeiten umfasst.


  1. Einfacher wird das parallele Programmieren damit jedoch nicht zwangsläufig. Viele der allgemein beliebten Probleme mit unsynchronisierten Speicherzugriffen bleiben weiter akut. Doch das Beispiel von Object-C Block Elementen und den dispatch_queue unter iOS und OSX zeigt, dass bei sauberem Einsatz die Probleme beherrschbar bleiben.

  2. Ein vergleichbares Sprachfeature wäre dennoch eine hübsche Erweiterung für Java9.

  3. Entsprechend der Funktionen AbstractService.safeLoad(String parameter, K key, AbstractDAO<T,K> dao) oder alternativ AbstractService.safeLoadNull(String parameter, K key, AbstractDAO<T,K> dao).

Weitere Artikel: