Як реалізувати ідіому іменованого параметра в Java? (особливо для конструкторів)
Я шукаю синтаксис, схожий на Objective-C, а не такий, який використовується в JavaBeans.
Невеликий приклад коду буде непоганим.
Дякую.
Як реалізувати ідіому іменованого параметра в Java? (особливо для конструкторів)
Я шукаю синтаксис, схожий на Objective-C, а не такий, який використовується в JavaBeans.
Невеликий приклад коду буде непоганим.
Дякую.
Відповіді:
Найкращою ідіомою Java для моделювання аргументів ключових слів у конструкторах є шаблон Builder, описаний у Effective Java 2nd Edition .
Основна ідея полягає в тому, щоб мати клас Builder, який має сеттери (але зазвичай не геттери) для різних параметрів конструктора. Існує також build()метод. Клас Builder часто є (статичним) вкладеним класом класу, який він використовується для побудови. Конструктор зовнішнього класу часто є приватним.
Кінцевий результат виглядає приблизно так:
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...
}
Щоб створити екземпляр Foo, ви пишете щось на зразок:
Foo foo = Foo.builder()
.setColor(red)
.setName("Fred")
.setSize(42)
.build();
Основні застереження:
Ви також можете переглянути цю публікацію в блозі (не я).
.withFoo, а не .setFoo: newBuilder().withSize(1).withName(1).build()а неnewBuilder().setSize(1).setName(1).build()
There's no compile-time checking that all of the parameters have been specified exactly once.Цю проблему можна подолати, повернувши інтерфейси Builder1туди, BuilderNде кожен охоплює або одного із установників, або build(). Кодувати набагато детальніше, але він має підтримку компілятора для вашого DSL і робить з автозаповненням дуже приємно працювати.
Про це варто згадати:
Foo foo = new Foo() {{
color = red;
name = "Fred";
size = 42;
}};
так званий подвійний фігурний ініціалізатор . Це насправді анонімний клас з ініціалізатором екземпляра.
Ви також можете спробувати дотримуватися порад звідси: http://www.artima.com/weblogs/viewpost.jsp?thread=118828
int value; int location; boolean overwrite;
doIt(value=13, location=47, overwrite=true);
Це багатослівно на сайті дзвінків, але загалом дає найнижчі накладні витрати.
doIt( /*value*/ 13, /*location*/ 47, /*overwrite*/ true )
Стиль Java 8:
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);
}
}
create()зупинило мене на шляху. Я ніколи не бачив такого стилю лямбда-ланцюгів на Java. Ви вперше виявили цю ідею іншою мовою з лямбдами?
Java не підтримує подібні до Objective-C іменовані параметри конструкторів або аргументів методів. Більше того, це насправді не спосіб Java. У Java типовий шаблон - багатослівні імена класів та членів. Класи та змінні повинні бути іменниками, а названий метод - дієсловами. Я припускаю, що ви могли б проявити креативність і відхилитися від конвенцій про іменування Java та наслідувати парадигму Objective-C, але це не було б особливо оцінено пересічним розробником Java, відповідальним за підтримку вашого коду. Працюючи будь-якою мовою, вам слід дотримуватися умовних норм мови та спільноти, особливо під час роботи в команді.
Якщо ви використовуєте Java 6, ви можете використовувати змінні параметри та імпортувати статичні, щоб отримати набагато кращий результат. Детально про це можна ознайомитись у:
http://zinzel.blogspot.com/2010/07/creating-methods-with-named-parameters.html
Коротше, ви могли б мати щось на зразок:
go();
go(min(0));
go(min(0), max(100));
go(max(100), min(0));
go(prompt("Enter a value"), min(0), max(100));
Я хотів би зазначити, що цей стиль звертається як до названого параметра, так і до властивостей без префікса get і set, який має інша мова. Це не звично в області Java, але простіше, не важко зрозуміти, особливо якщо ви працювали з іншими мовами.
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());
}
}
Що стосовно
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");
Ви можете використовувати звичайний конструктор та статичні методи, які дають аргументам назву:
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;
}
}
Використання:
import static Something.*;
Something s = new Something(name("pen"), size(20), weight(8.2));
Обмеження порівняно з реальними іменованими параметрами:
/*name*/ "pen", /*size*/ 20, /*weight*/ 8.2))Якщо у вас є вибір, подивіться на Scala 2.8. http://www.scala-lang.org/node/2075
not really better than a comment... з іншого боку ...;)
Використовуючи лямбди Java 8, ви можете наблизитись до реальних іменованих параметрів.
foo($ -> {$.foo = -10; $.bar = "hello"; $.array = new int[]{1, 2, 3, 4};});
Зверніть увагу, що це, ймовірно, порушує пару десятків "найкращих практик Java" (як і все, що використовує $символ).
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));
}
}
Плюси:
Мінуси:
$fooніколи не втікає до абонента (якщо хтось не призначає його змінній всередині зворотного виклику), то чому вони не можуть бути загальнодоступними?
Ви можете використовувати анотацію @Builder проекту Lombok для моделювання названих параметрів у Java. Це створить для вас конструктор, який ви можете використовувати для створення нових екземплярів будь-якого класу (як класів, які ви написали, так і тих, що надходять із зовнішніх бібліотек).
Ось як увімкнути його в класі:
@Getter
@Builder
public class User {
private final Long id;
private final String name;
}
Згодом ви можете використовувати це шляхом:
User userInstance = User.builder()
.id(1L)
.name("joe")
.build();
Якщо ви хочете створити такий Builder для класу, що надходить з бібліотеки, створіть анотований статичний метод, такий як:
class UserBuilder {
@Builder(builderMethodName = "builder")
public static LibraryUser newLibraryUser(Long id, String name) {
return new LibraryUser(id, name);
}
}
Це створить метод з іменем "builder", який можна викликати:
LibraryUser user = UserBuilder.builder()
.id(1L)
.name("joe")
.build();
Я відчуваю, що "обхідний шлях" заслуговує на власну відповідь (прихований у існуючих відповідях і згаданий у коментарях тут).
someMethod(/* width */ 1024, /* height */ 768);
Це варіант BuilderВізерунка, як описано Лоуренсом вище.
Я дуже часто цим користуюся (у відповідних місцях).
Головна відмінність полягає в тому, що в цьому випадку Builder є незмінним . Це має ту перевагу, що його можна використовувати повторно і він безпечний для потоків.
Таким чином, ви можете використовувати це для створення одного конструктора за замовчуванням, а потім у різних місцях, де він вам потрібен, ви можете налаштувати його та побудувати свій об’єкт.
Це має найбільший сенс, якщо ви будуєте один і той самий об'єкт знову і знову, тому що тоді ви можете зробити конструктор статичним і вам не доведеться турбуватися про зміну його налаштувань.
З іншого боку, якщо вам доводиться будувати об'єкти зі змінними параметрами, це має деякі тихі витрати. (але привіт, ви можете поєднувати статичне / динамічне генерування зі спеціальними buildметодами)
Ось приклад коду:
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();
}
}
Будь-яке рішення в Java, швидше за все , буде досить багатослівний, але варто відзначити , що такі інструменти , як Google AutoValues і Immutables буде генерувати будівельник класи для вас автоматично з допомогою JDK час компіляції обробки анотацій.
У моєму випадку я хотів, щоб іменовані параметри використовувались у переліченні Java, тому шаблон побудови не буде працювати, оскільки екземпляри перерахування не можуть бути створені іншими класами. Я придумав підхід, подібний до відповіді @ deamon, але додає перевірку впорядкування параметрів під час компіляції (за рахунок додаткового коду)
Ось код клієнта:
Person p = new Person( age(16), weight(100), heightInches(65) );
І реалізація:
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; }
}
Можливо, варто розглянути ідіому, яку підтримує бібліотека karg :
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");
}
}
R Cashaвідповідь, але без коду, який би це пояснив.
Ось шаблон перевірки компілятора Builder. Застереження:
.build()методТож вам потрібно щось поза класом, яке зазнає невдачі, якщо не буде пройдено Builder<Yes, Yes, Yes>. Див. getSumСтатичний метод як приклад.
class No {}
class Yes {}
class Builder<K1, K2, K3> {
int arg1, arg2, arg3;
Builder() {}
static Builder<No, No, No> make() {
return new Builder<No, No, No>();
}
@SuppressWarnings("unchecked")
Builder<Yes, K2, K3> arg1(int val) {
arg1 = val;
return (Builder<Yes, K2, K3>) this;
}
@SuppressWarnings("unchecked")
Builder<K1, Yes, K3> arg2(int val) {
arg2 = val;
return (Builder<K1, Yes, K3>) this;
}
@SuppressWarnings("unchecked")
Builder<K1, K2, Yes> arg3(int val) {
this.arg3 = val;
return (Builder<K1, K2, Yes>) this;
}
static int getSum(Builder<Yes, Yes, Yes> build) {
return build.arg1 + build.arg2 + build.arg3;
}
public static void main(String[] args) {
// Compiles!
int v1 = getSum(make().arg1(44).arg3(22).arg2(11));
// Builder.java:40: error: incompatible types:
// Builder<Yes,No,Yes> cannot be converted to Builder<Yes,Yes,Yes>
int v2 = getSum(make().arg1(44).arg3(22));
System.out.println("Got: " + v1 + " and " + v2);
}
}
Пояснив застереження . Чому немає методу побудови? Проблема в тому, що він буде в Builderкласі, і він буде параметризований за допомогою K1, K2, K3і т. Д. Оскільки сам метод повинен компілювати, все, що він викликає, має скомпілювати. Отже, загалом ми не можемо поставити тест компіляції в метод самого класу.
З подібної причини ми не можемо запобігти подвійному призначенню, використовуючи модель конструктора.
@irreputable придумав гарне рішення. Однак - це може залишити ваш примірник класу в недійсному стані, оскільки перевірка та перевірка узгодженості не відбуватимуться. Тому я вважаю за краще поєднувати це з рішенням Builder, уникаючи створення додаткового підкласу, хоча це все одно буде підклас класу builder. Крім того, оскільки додатковий клас будівельника робить його більш багатослівним, я додав ще один метод, використовуючи лямбда. Для повноти я додав деякі інші підходи до будівництва.
Починаючи з класу наступним чином:
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; }
}
Потім використовуючи це, застосовуючи різні методи:
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";
}
);
Це виглядає частково повним викраданням із того, що @LaurenceGonsalves вже розмістило, але ви побачите невелику різницю у вибраній конвенції.
Мені цікаво, якби JLS коли-небудь реалізовував названі параметри, як би вони це робили? Чи будуть вони поширюватися на одну з існуючих ідіом, надаючи коротку підтримку? Також як Scala підтримує іменовані параметри?
Хммм - досить для дослідження, і, можливо, нове питання.