СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H BadDesign: именуем параметры в Java в черновиках

В некоторых языках программирования (например, в Scala) есть именованные параметры. Именованные параметры позволяют контролировать присвоение значений, дополнительным бонусом выступает возможность использовать свой порядок указания параметров, нежели описано в сигнатуре функции. В рамках цикла статей BadDesign, предложу урезанный механизм именованных параметров для Java.

Следующий пример демонстрирует описанный механизм именованных параметров.

Пусть есть функция, которая печатает имя и фамилию.
def printName(first:String, last:String) = {
  println(first + " " + last)
}

Вызов функции без использования именованных параметров:
printName("John","Smith")
// Prints "John Smith"

Вызов функции с именованными параметрами:
printName(first = "John",last = "Smith")
// Prints "John Smith"

Вызов функции с именованными параметрами и измененным порядком указания параметров:
printName(last = "Smith",first = "John")
// Prints "John Smith"


Рассмотрим класс прямоугольник с двумя полями: ширина и высота. Дополнительно предположим, что прямоугольники используются где-то в приложении и имеют дефолтные значения для ширины и высоты.

Простой пример реализации мог бы выглядеть так:
public class Rectangle {

    public static final int DEFAULT_HEIGHT = 50;

    public static final int DEFAULT_WIDTH = 200;

    private int height;

    private int width;

    public Rectangle() {
        this(DEFAULT_HEIGHT, DEFAULT_WIDTH);
    }

    public Rectangle(int height, int width) {
        this.height = height;

        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }
}

Далее рассмотрим несколько примеров.

// Создание прямоугольника с дефолтными значениями для высоты и ширины.
Rectangle rectangle = new Rectangle();

// Создание прямоугольника с указанными значениями для высоты и ширины.
Rectangle rectangle = new Rectangle(27, 109);

// Создание прямоугольника с дефолтным значением для ширины и указанным значением для высоты.
Rectangle rectangle = new Rectangle();
rectangle.setHeight(27);

// Создание прямоугольника с дефолтным значением для высоты и указанным значением для ширины.
Rectangle rectangle = new Rectangle();
rectangle.setWidth(109);

Было бы очень здорово, если можно было бы использовать только конструктор для выполнения выше указанных операций, т.е. хочется что-то типа такого:
public class Rectangle {
    ...

    public Rectangle(int height) {
        this(height, DEFAULT_WIDTH);
    }

    public Rectangle(int width) {
        this(DEFAULT_HEIGHT, width);
    }

    ...
}

Однако, такой код не компилируется в Java (что, в общем-то, логично).

Обойти эту проблему можно создав маленькие обертки над высотой и шириной.

Обертка над высотой:
public class RectangleHeight {

    private int height;

    private RectangleHeight(int height) {
        this.height = height;
    }

    public static RectangleHeight rectangleHeight(int height) {
        return new RectangleHeight(height);
    }

    public int getValue() {
        return height;
    }
}

Обертка над шириной:
class RectangleWidth {

    private int width;

    private RectangleWidth(int width) {
        this.width = width;
    }

    public static RectangleWidth rectangleWidth(int width) {
        return new RectangleWidth(width);
    }

    public int getValue() {
        return width;
    }
}

Теперь добавим в класс Rectangle два конструктора:
public class Rectangle {
    ...

    public Rectangle(RectangleHeight rectangleHeight) {
        this(rectangleHeight.getValue(), DEFAULT_WIDTH);
    }

    public Rectangle(RectangleWidth rectangleWidth) {
        this(DEFAULT_HEIGHT, rectangleWidth.getValue());
    }


    ...
}

Теперь есть некая имитация именованных параметров.

Вместо
// Создание прямоугольника с дефолтным значением для ширины и указанным значением для высоты.
Rectangle rectangle = new Rectangle();
rectangle.setHeight(27);

теперь можно сделать так:
// Создание прямоугольника с дефолтным значением для ширины и указанным значением для высоты.
Rectangle rectangle = new Rectangle(rectangleHeight(27));


Вместо:
// Создание прямоугольника с дефолтным значением для высоты и указанным значением для ширины:
Rectangle rectangle = new Rectangle();
rectangle.setWidth(109);

теперь можно сделать так:
// Создание прямоугольника с дефолтным значением для высоты и указанным значением для ширины:
Rectangle rectangle = new Rectangle(rectangleWidth(100));


Вместо:
// Создание прямоугольника с указанными значениями для высоты и ширины.
Rectangle rectangle = new Rectangle(27, 109);

теперь можно сделать так:
// Создание прямоугольника с указанными значениями для высоты и ширины.
Rectangle rectangle = new Rectangle(rectangleHeight(27), rectangleWidth(109));


Плюсы

  • Нельзя ошибиться с указанием высоты и ширины, если использовать конструктор с обертками.


Минусы

  • Слишком много классов.
  • Многословно.
  • Всё еще нельзя менять порядок указания значений параметров.

комментарии (23)

+4
TimReset ,  
Идея, конечно, интересная, но в Java в таких случаях, когда нужно несколько параметров указать явно по имени и часть из них может быть не обязательна, используют Builder.
stackoverflow.com/questions/5007355/builder-pattern-in-effective-java
Пример: code.google.com/p/guava-libraries/wiki/CachesExplained
Это паттерн хорошо в Effective Java описан. Помимо чёткого указания параметров по имени, есть другие преимущества — контроль заполненности обязательных полей, возможность возвращать различные реализации интерфейса (т.е. в сигнатуре метода описано возвращение не конкретного класса, а интерфейс и в зависимости от указанных в Buider'е параметров возвращаются различные имплементации).
P.S. Именованные параметры в Groovy есть. Но, насколько я знаю, там они через Map реализованы. Т.е. такой сахарок, а не native поддержка языка.
А как именованные параметры в Scala реализованы?
–3
+1 –4
jxcoder ,  
Я знаю насчет этого паттерна, просто привел еще один путь. Честно говоря не знаю как они реализованы внутри. Механизм использования описал в начале статьи.
+1
jxcoder ,  
За что минусы?
+2
dougrinch ,  
В скале они полностью через compile-time-magic сделаны (как вообще почти все в скале). В байткоде потом остается единственный конструктор со всеми аргументами, просто в местах его вызовов компилятор сам меняет порядок аргументов/подставляет дефолтные значения.

Из плюсов такого решения — вообще нет рантайм оверхеда, из минусов — при вызове потом из java все равно надо указывать все аргументы.
0
fcoder ,   * (был изменён)
del
0
jxcoder ,  
Я знаю насчет этого паттерна, просто привел еще один путь. Честно говоря не знаю как они реализованы внутри. Механизм использования описал в начале статьи.
+1
agent10 ,  
Считаю это мало полезной фичей:
1. Про использование Builder уже выше написали
2. Вспоминаю всего несколько случаев когда ошибался с указанием значений параметров. И те были не существенны…
3. IDE сейчас отлично подсказывают какие параметры в момент набора
0
jxcoder ,  
Ноги этого решения, в частности, растут из кучи примеров, когда приходилось исправлять вот такого рода глупые ошибки.
+6
MaximChistov ,  
Лайфхак для ленивых:
int width = 25, height = 109;
Rectangle rectangle = new Rectangle(width, height);
+1
+2 –1
jxcoder ,  
Ну здесь же можно потом перепутать.

Вместо
int width = 25, height = 109;
Rectangle rectangle = new Rectangle(width, height);

может случиться так:
int width = 25, height = 109;
Rectangle rectangle = new Rectangle(height, width);
0
MaximChistov ,  
разрешение ставить параметры в любом порядке — это не решение. решение — проверять порядок следования при введении кода) а названия эти для читаемости в дальнейшем только
0
jxcoder ,  
Понятно, что везде нужна аккуратность и внимательность. А проверять порядок следования, это конечно круто, но очень много натыкался на вот такие ошибки.
+1
SamSol ,  
Иногда при разработке API вдруг появляется необходимость передать кучу параметров в вызов метода:
service.convertAbc("http://converterservse.local", "abc.xml", "admin", "admin-pasword", "January Report");

В таких случаях я «вынуждаю» пользователей API использовать специальный объект с конструктором без параметров, и заполнять поля пошутчно:
ConvertAbcRequest request = new ConvertAbcRequest();
request.setServiceUrl("http://converterservse.local");
request.setFileName("abc.xml");
request.setLogin("admin");
request.setPassword("admin-password");
request.setReportTitle("January Report");
service.convertAbc(request);

Самые отъявленные любители «однострочников» «хакают» мой подход так:
service.convertAbc(new ConvertAbcRequest() {
    {
        setServiceUrl("http://converterservse.local");
        setFileName("abc.xml");
        setLogin("admin");
        setPassword("admin-password");
        setReportTitle("January Report");
    }
});

В результате уменьшается количество мест где можно ошибиться
0
jxcoder ,  
Тоже вариант. Но тоже встречал переусердствования «однострочников».
0
dougrinch ,  
Хм, что-то это мне напоминает.
+1
SamSol ,  
Ява с сахаром — это Groovy! Пользуйтесь груви!
Пример использования конструкторов в груви
@groovy.transform.EqualsAndHashCode
class Rectangle {
    int width
    int height

    Rectangle() { // Первый конструктор
        this(0,0)
    }

    Rectangle(w,h) { // Второй конструктор
        width = w
        height = h
    }
}

def r1 = new Rectangle()
r1.width = 1
r1.height = 2

assert r1 == new Rectangle(1, 2)
assert r1 == new Rectangle(width:1, height:2) // вызов третьего конструктора (его - третий конструктор - создает груви)
assert r1 == new Rectangle(height:2, width:1)
assert r1 == [1,2] as Rectangle // Преобразование массива в Rectangle (вызывается второй констрктор)
assert r1 == (Rectangle) [1,2]

Rectangle r2 = [1, 2] // вызов второго конструктора при неявном приведении типов
Rectangle r3 = [height: 2, width:1] // Вызов третьего конструктора в неявном приведении типов
assert r1 == r2
assert r1 == r3

0
knott ,  
Андроид-холопам что порекомендуете?
0
SamSol ,   * (был изменён)
Можно на груви писать андроидные приложения. Я сам этого не делал, но по отзывам вполне юзабельно. Подключаете в грэйдл android-groovy-plugin и дальше все как по маслу.
Кажется вот здесь github.com/groovy/groovy-android-gradle-plugin
+1
TimReset ,  
0
daron666 ,  
Есть и со Scala варианты Тыц
+1
sperson ,   * (был изменён)
По своему опыту скажу, что большинство случаев использование в android любого (отличного от java) языка — бесконечные костыли и «подводные камни».
+2
knott ,  
Полностью с вами согласен. Собственно вопрос был риторическим: )
Хотя, есть надежда на Kotlin.
0
fogone ,  
Когда пишешь код, то ide подскажет имена, а когда читаешь то обычно порядок вообще не важен. Если же параметров действительно много, то билдер хорошее решение. Я написал какое-то количество кода на котлине и по этому опыту могу сказать, что именованные параметры чаще используются в паре с параметрами по умолчанию, когда нужно передать только некоторые значения, а остальные не трогать. И крайне редко для разрешения неопределенности.