Comment implémenter l'idiome de paramètre nommé en Java? (surtout pour les constructeurs)
Je suis à la recherche d'une syntaxe semblable à Objective-C et non semblable à celle utilisée dans JavaBeans.
Un petit exemple de code serait bien.
Merci.
Le meilleur langage que je connaisse pour simuler les arguments de mots clés dans les constructeurs est le modèle Builder, décrit dans Effective Java 2nd Edition .
L'idée de base est de disposer d'une classe Builder comportant des setters (mais généralement pas des getters) pour les différents paramètres du constructeur. Il y a aussi une méthode build()
. La classe Builder est souvent une classe imbriquée (statique) de la classe pour laquelle elle est construite. Le constructeur de la classe externe est souvent privé.
Le résultat final ressemble à quelque chose comme:
public class Foo {
public static class Builder {
public Foo build() {
return new Foo(this);
}
public Builder setSize(int size) {
this.size = size;
return this;
}
public Builder setColor(Color color) {
this.color = color;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
// you can set defaults for these here
private int size;
private Color color;
private String name;
}
public static Builder builder() {
return new Builder();
}
private Foo(Builder builder) {
size = builder.size;
color = builder.color;
name = builder.name;
}
private final int size;
private final Color color;
private final String name;
// The rest of Foo goes here...
}
Pour créer une instance de Foo, vous écrivez quelque chose comme:
Foo foo = Foo.builder()
.setColor(red)
.setName("Fred")
.setSize(42)
.build();
Les principales mises en garde sont les suivantes:
Vous pouvez également vouloir consulter cet article de blog (pas par moi).
Cela vaut la peine de mentionner:
Foo foo = new Foo() {{
color = red;
name = "Fred";
size = 42;
}};
le soi-disant initialisateur à double accolade . C'est en fait une classe anonyme avec un initialiseur d'instance.
Vous pouvez également essayer de suivre les conseils d’ici: http://www.artima.com/weblogs/viewpost.jsp?thread=118828
int value; int location; boolean overwrite;
doIt(value=13, location=47, overwrite=true);
Il est détaillé sur le site d’appel, mais donne globalement les frais généraux les plus bas.
Java 8 style:
public class Person {
String name;
int age;
private Person(String name, int age) {
this.name = name;
this.age = age;
}
static PersonWaitingForName create() {
return name -> age -> new Person(name, age);
}
static interface PersonWaitingForName {
PersonWaitingForAge name(String name);
}
static interface PersonWaitingForAge {
Person age(int age);
}
public static void main(String[] args) {
Person charlotte = Person.create()
.name("Charlotte")
.age(25);
}
}
Si vous utilisez Java 6, vous pouvez utiliser les paramètres de variable et importer static pour obtenir un résultat bien meilleur. On en trouve des détails dans:
http://zinzel.blogspot.com/2010/07/creating-methods-with-named-parameters.html
En bref, vous pourriez avoir quelque chose comme:
go();
go(min(0));
go(min(0), max(100));
go(max(100), min(0));
go(Prompt("Enter a value"), min(0), max(100));
Voici une petite variation de la technique donnée dans Effective Java de Joshua Bloch. Ici, j'ai tenté de rendre le code client plus lisible (ou peut-être plus DSLish).
/**
* Actual class for which we want to implement a
* named-parameter pseudo-constructor
*/
class Window{
protected int x, y, width, height;
protected boolean isResizable;
protected String title;
public void show(){
// Show the window
System.out.printf("Window \"%s\" set visible.%n",title);
}
/**
* This class is only used to set the parameter values
*/
static class HavingProperties extends Window{
public HavingProperties x(int value){
this.x=value;
return this;
}
public HavingProperties y(int value){
this.y=value;
return this;
}
public HavingProperties width(int value){
this.width=value;
return this;
}
public HavingProperties height(int value){
this.height=value;
return this;
}
public HavingProperties resizable(boolean value){
this.isResizable=value;
return this;
}
public HavingProperties title(String value){
this.title=value;
return this;
}
}
}
public class NamedParameterIdiomInAction {
public static void main(String... args){
Window window=new Window.HavingProperties().x(10).y(10).width(100).
height(100).resizable(true).title("My App");
window.show();
}
}
Veuillez noter qu'avec cette variante, vous pouvez également donner des noms significatifs à vos pseudo-constructeurs.
Qu'en est-il de
public class Tiger {
String myColor;
int myLegs;
public Tiger color(String s)
{
myColor = s;
return this;
}
public Tiger legs(int i)
{
myLegs = i;
return this;
}
}
Tiger t = new Tiger().legs(4).color("striped");
Je tiens à souligner que ce style concerne à la fois le paramètre named et les properties features sans les préfixes get et set que l 'autre langue possède. Ce n'est pas conventionnel en Java, mais c'est simple, facile à comprendre, surtout si vous avez déjà utilisé d'autres langages.
public class Person {
String name;
int age;
// name property
// getter
public String name() { return name; }
// setter
public Person name(String val) {
name = val;
return this;
}
// age property
// getter
public int age() { return age; }
// setter
public Person age(int val) {
age = val;
return this;
}
public static void main(String[] args) {
// Addresses named parameter
Person jacobi = new Person().name("Jacobi").age(3);
// Addresses property style
println(jacobi.name());
println(jacobi.age());
//...
jacobi.name("Lemuel Jacobi");
jacobi.age(4);
println(jacobi.name());
println(jacobi.age());
}
}
Java ne prend pas en charge les paramètres nommés de type Objective-C-like pour les constructeurs ou les arguments de méthodes. De plus, ce n'est vraiment pas la manière de faire de Java. En Java, le modèle typique est constitué de classes et de membres nommés de manière verbale. Les classes et les variables doivent être des noms et la méthode nommée doit être un verbe. Je suppose que vous pouvez faire preuve de créativité et vous écarter des conventions de dénomination Java et émuler le paradigme Objective-C de manière simpliste, mais cela ne serait pas particulièrement apprécié du développeur Java moyen chargé de maintenir votre code. Lorsque vous travaillez dans n'importe quelle langue, il vous incombe de respecter les conventions de la langue et de la communauté, en particulier lorsque vous travaillez en équipe.
Toute solution en Java sera probablement assez détaillée, mais il convient de noter que des outils tels que Google AutoValues et Immutables généreront automatiquement des classes de générateur à l'aide du traitement des annotations au moment de la compilation JDK.
Dans mon cas, je souhaitais que les paramètres nommés soient utilisés dans une énumération Java. Par conséquent, un modèle de générateur ne fonctionnerait pas car les instances enum ne peuvent pas être instanciées par d'autres classes. J'ai trouvé une approche similaire à celle de @ deamon mais ajoute une vérification au moment de la compilation du classement des paramètres (au détriment de davantage de code)
Voici le code client:
Person p = new Person( age(16), weight(100), heightInches(65) );
Et la mise en place:
class Person {
static class TypedContainer<T> {
T val;
TypedContainer(T val) { this.val = val; }
}
static Age age(int age) { return new Age(age); }
static class Age extends TypedContainer<Integer> {
Age(Integer age) { super(age); }
}
static Weight weight(int weight) { return new Weight(weight); }
static class Weight extends TypedContainer<Integer> {
Weight(Integer weight) { super(weight); }
}
static Height heightInches(int height) { return new Height(height); }
static class Height extends TypedContainer<Integer> {
Height(Integer height) { super(height); }
}
private final int age;
private final int weight;
private final int height;
Person(Age age, Weight weight, Height height) {
this.age = age.val;
this.weight = weight.val;
this.height = height.val;
}
public int getAge() { return age; }
public int getWeight() { return weight; }
public int getHeight() { return height; }
}
Vous pouvez utiliser le projet annotation @Builder du projet Lombok pour simuler des paramètres nommés en Java. Cela générera pour vous un générateur que vous pourrez utiliser pour créer de nouvelles instances de toute classe (les classes que vous avez écrites et celles provenant de bibliothèques externes).
Voici comment l'activer sur une classe:
@Getter
@Builder
public class User {
private final Long id;
private final String name;
}
Ensuite, vous pouvez utiliser ceci en:
User userInstance = User.builder()
.id(1L)
.name("joe")
.build();
Si vous souhaitez créer un tel générateur pour une classe provenant d'une bibliothèque, créez une méthode statique annotée comme celle-ci:
class UserBuilder {
@Builder(builderMethodName = "builder")
public static LibraryUser newLibraryUser(Long id, String name) {
return new LibraryUser(id, name);
}
}
Cela générera une méthode nommée "constructeur" qui peut être appelée par:
LibraryUser user = UserBuilder.builder()
.id(1L)
.name("joe")
.build();
Je pense que la "solution de contournement" mérite sa propre réponse (cachée dans les réponses existantes et mentionnée dans les commentaires ici).
someMethod(/* width */ 1024, /* height */ 768);
En utilisant les lambdas de Java 8, vous pouvez vous rapprocher encore plus des paramètres nommés real.
foo($ -> {$.foo = -10; $.bar = "hello"; $.array = new int[]{1, 2, 3, 4};});
Notez que cela enfreint probablement une vingtaine de "meilleures pratiques Java" (comme tout ce qui utilise le symbole $
).
public class Main {
public static void main(String[] args) {
// Usage
foo($ -> {$.foo = -10; $.bar = "hello"; $.array = new int[]{1, 2, 3, 4};});
// Compare to roughly "equivalent" python call
// foo(foo = -10, bar = "hello", array = [1, 2, 3, 4])
}
// Your parameter holder
public static class $foo {
private $foo() {}
public int foo = 2;
public String bar = "test";
public int[] array = new int[]{};
}
// Some boilerplate logic
public static void foo(Consumer<$foo> c) {
$foo foo = new $foo();
c.accept(foo);
foo_impl(foo);
}
// Method with named parameters
private static void foo_impl($foo par) {
// Do something with your parameters
System.out.println("foo: " + par.foo + ", bar: " + par.bar + ", array: " + Arrays.toString(par.array));
}
}
Avantages:
Les inconvénients:
Vous pouvez utiliser un constructeur habituel et des méthodes statiques qui donnent un nom aux arguments:
public class Something {
String name;
int size;
float weight;
public Something(String name, int size, float weight) {
this.name = name;
this.size = size;
this.weight = weight;
}
public static String name(String name) {
return name;
}
public static int size(int size) {
return size;
}
public float weight(float weight) {
return weight;
}
}
Usage:
import static Something.*;
Something s = new Something(name("pen"), size(20), weight(8.2));
Limitations par rapport aux paramètres nommés réels:
/*name*/ "pen", /*size*/ 20, /*weight*/ 8.2)
)Si vous avez le choix, regardez Scala 2.8. http://www.scala-lang.org/node/2075
Ceci est une variante du modèle Builder
décrit par Lawrence ci-dessus.
Je me retrouve souvent à l'utiliser (aux endroits appropriés).
La principale différence est que, dans ce cas, le constructeur est immuatable. Cela a l’avantage de pouvoir être réutilisé et d’être thread-safe.
Vous pouvez donc l'utiliser pour créer un constructeur default puis, aux différents endroits où vous en avez besoin, vous pouvez le configurer et construire votre objet.
Cela semble logique si vous construisez le même objet encore et encore, car vous pouvez alors rendre le générateur statique et ne pas avoir à vous soucier de la modification de ses paramètres.
D'un autre côté, si vous devez construire des objets avec des paramètres changeants, cela entraîne une légère surcharge. (mais bon, vous pouvez combiner la génération statique/dynamique avec des méthodes personnalisées build
)
Voici l'exemple de code:
public class Car {
public enum Color { white, red, green, blue, black };
private final String brand;
private final String name;
private final Color color;
private final int speed;
private Car( CarBuilder builder ){
this.brand = builder.brand;
this.color = builder.color;
this.speed = builder.speed;
this.name = builder.name;
}
public static CarBuilder with() {
return DEFAULT;
}
private static final CarBuilder DEFAULT = new CarBuilder(
null, null, Color.white, 130
);
public static class CarBuilder {
final String brand;
final String name;
final Color color;
final int speed;
private CarBuilder( String brand, String name, Color color, int speed ) {
this.brand = brand;
this.name = name;
this.color = color;
this.speed = speed;
}
public CarBuilder brand( String newBrand ) {
return new CarBuilder( newBrand, name, color, speed );
}
public CarBuilder name( String newName ) {
return new CarBuilder( brand, newName, color, speed );
}
public CarBuilder color( Color newColor ) {
return new CarBuilder( brand, name, newColor, speed );
}
public CarBuilder speed( int newSpeed ) {
return new CarBuilder( brand, name, color, newSpeed );
}
public Car build() {
return new Car( this );
}
}
public static void main( String [] args ) {
Car porsche = Car.with()
.brand( "Porsche" )
.name( "Carrera" )
.color( Color.red )
.speed( 270 )
.build()
;
// -- or with one default builder
CarBuilder Assembly_LINE = Car.with()
.brand( "Jeep" )
.name( "Cherokee" )
.color( Color.green )
.speed( 180 )
;
for( ;; ) Assembly_LINE.build();
// -- or with custom default builder:
CarBuilder MERCEDES = Car.with()
.brand( "Mercedes" )
.color( Color.black )
;
Car c230 = MERCEDES.name( "C230" ).speed( 180 ).build(),
clk = MERCEDES.name( "CLK" ).speed( 240 ).build();
}
}
L'idiome supporté par la bibliothèque karg peut être utile:
class Example {
private static final Keyword<String> GREETING = Keyword.newKeyword();
private static final Keyword<String> NAME = Keyword.newKeyword();
public void greet(KeywordArgument...argArray) {
KeywordArguments args = KeywordArguments.of(argArray);
String greeting = GREETING.from(args, "Hello");
String name = NAME.from(args, "World");
System.out.println(String.format("%s, %s!", greeting, name));
}
public void sayHello() {
greet();
}
public void sayGoodbye() {
greet(GREETING.of("Goodbye");
}
public void campItUp() {
greet(NAME.of("Sailor");
}
}
@irreputable a proposé une solution intéressante. Cependant, cela pourrait laisser votre instance de classe dans un état non valide, car aucune validation ni vérification de la cohérence ne sera effectuée. Par conséquent, je préfère combiner cela avec la solution Builder, en évitant la création d’une sous-classe supplémentaire, bien que cela reste une sous-classe de la classe Builder. De plus, comme la classe de construction supplémentaire la rend plus détaillée, j'ai ajouté une méthode supplémentaire utilisant un lambda. J'ai ajouté certaines des autres approches de constructeur pour la complétude.
Commençant par une classe comme suit:
public class Foo {
static public class Builder {
public int size;
public Color color;
public String name;
public Builder() { size = 0; color = Color.RED; name = null; }
private Builder self() { return this; }
public Builder size(int size) {this.size = size; return self();}
public Builder color(Color color) {this.color = color; return self();}
public Builder name(String name) {this.name = name; return self();}
public Foo build() {return new Foo(this);}
}
private final int size;
private final Color color;
private final String name;
public Foo(Builder b) {
this.size = b.size;
this.color = b.color;
this.name = b.name;
}
public Foo(Java.util.function.Consumer<Builder> bc) {
Builder b = new Builder();
bc.accept(b);
this.size = b.size;
this.color = b.color;
this.name = b.name;
}
static public Builder with() {
return new Builder();
}
public int getSize() { return this.size; }
public Color getColor() { return this.color; }
public String getName() { return this.name; }
}
Puis en utilisant ceci en appliquant les différentes méthodes:
Foo m1 = new Foo(
new Foo.Builder ()
.size(1)
.color(BLUE)
.name("Fred")
);
Foo m2 = new Foo.Builder()
.size(1)
.color(BLUE)
.name("Fred")
.build();
Foo m3 = Foo.with()
.size(1)
.color(BLUE)
.name("Fred")
.build();
Foo m4 = new Foo(
new Foo.Builder() {{
size = 1;
color = BLUE;
name = "Fred";
}}
);
Foo m5 = new Foo(
(b)->{
b.size = 1;
b.color = BLUE;
b.name = "Fred";
}
);
Cela ressemble en partie à une arnaque totale de ce que @LaurenceGonsalves a déjà publié, mais vous verrez la petite différence de convention choisie.
Je me demande si JLS implémenterait jamais les paramètres nommés, comment ils le feraient? Seraient-ils étendus à l'un des idiomes existants en fournissant un support abrégé pour cela? Aussi, comment Scala prend-il en charge les paramètres nommés?
Hmmm - assez pour faire des recherches et peut-être une nouvelle question.