Статьи

Kotlin-подобные конструкторы в Java и Python, продолжение: дополнительные параметры

вступление

В сегодняшней статье мы продолжим статью на прошлой неделе о создании Kotlin-подобных компоновщиков в Java и Python , расширяя API-интерфейсы компоновщика для принятия некоторых из необязательных параметров для большей гибкости. Мы продолжаем с нашим примером HTML, пытаясь добавить атрибуты тега, такие как class, id и style.

Котлин и Питон

Kotlin настраивает использование этих параметров именно так, как я бы это делал в Python: аргументы по умолчанию и именованные аргументы. Использование Kotlin будет выглядеть примерно так:

1
2
3
4
5
6
7
html {
   body {
      p(klass="myClass", id="theParagraph") {
         + "the paragraph text"
      }
   }
}

Обратите внимание на использование «класса» вместо «класса». Классическое столкновение ключевых слов и идентификаторов. Вы можете использовать «cls», «clazz» или что угодно, если хотите. Я бы предложил отойти от всего, что обычно используется в языке для объектов класса, так как это вообще другой класс.

Это довольно большое обновление тега p с прошлой недели (которое было просто p = "text" ), изменяющее его свойство на полноценный метод. Но большинству других примеров не потребуется столько работы. Вот обновленный код Kotlin:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class Body {
   ...
   fun p(class: String="", id: String="", style: Style=Style.blank, paragraphBuilder: Paragraph.() -> Unit) {
      val p = Paragraph(class, id, style)
      paragraphs.add(p)
      p.paragraphBuilder()
   }
   ...
}
 
class Paragraph(val class: String, val id: String, val style: Style) {
   var text: String = ""
 
   operator fun plus(other: String) {
      text += other
   }
}

Обновленный код Python (все еще использующий первую версию) будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Body:
    def __init__(self):
        self.paragraphs =
    ...
    def p(self, klass='', id='', style=None):
        par = Paragraph(klass, id, style)
        self.paragraphs.append(par)
        return par
 
 
class Paragraph:
    def __init__(self, klass, id, style):
        self.klass = klass
        self.id = id
        self.style = style
        self.text = ''
 
    def __enter__(self):
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        return False
 
    def __iadd__(self, text):
        self.text += text

__iadd__() — оператор сложения на месте, позволяющий нам сказать p += 'text' . В Kotlin мы использовали + вместо += потому что нам не нужно ссылаться на объект абзаца, поэтому неправильно начинать с += , тогда как нам нужно ссылаться на p в коде Python, поэтому += выглядит более естественным, сделав так, чтобы мы могли изменить код вызова на что-то вроде этого:

1
2
3
4
5
html = Html()
with html as html:
    with html.body() as body:
        with body.p(class='myClass', id='theParagraph') as p:
            p += 'the paragraph text'

И Kotlin, и Python принимают объект Style вместо того, чтобы просто принимать другую строку, как остальные. На самом деле, я бы порекомендовал сделать то же самое для class и id, так как тогда мы передаем объекты класса и id с их настройками CSS, которые также используются с компоновщиком CSS. Я только не делал это здесь ради примеров. Я не позволил Style оставаться строкой, потому что для лучшей ясности и правильности лучше использовать его с каким-то конструктором стилей CSS

Ява

И Kotlin, и Python делают переход довольно простым. К сожалению, Java не имеет необходимого набора функций, чтобы позволить такое легкое изменение; Вы должны полагаться на старые беглые API трюки, чтобы пройти через это.

Перегрузка в изобилии!

Первая мысль состоит в том, чтобы добиться максимально возможного эквивалента преобразования с кучей перегрузок. Вы создаете быстрые и удобные обертки строк для полей класса и id, так как они являются просто строками, что затрудняет их различение в противном случае:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class Class {
   public final String text;
 
   public Class(String text) {
      this.text = text;
   }
}
 
 
class ID {
   public final String text;
 
   public ID(String text) {
      this.text = text;
   }
}

Что делает всю перегрузку похожей на это:

01
02
03
04
05
06
07
08
09
10
class Body {
   ...
   public void p(Consumer<Paragraph> paragraphBuilder) {...}
   public void p(Class klass, Consumer...) {...}
   public void p(ID id, Consumer...) {...}
   public void p(Style style, Consumer...) {...}
   public void p(Class klass, ID id, Consumer...) {...}
   // and so on... 3 more times
   ...
}

Это становится настолько утомительным, что я даже не закончил писать каждую строку, не говоря уже о начале всех строк. И это только принимает во внимание класс, идентификатор и стиль; Есть еще. Идти по этому маршруту просто бесполезно. Следовательно, я даже не покажу, как выглядит полученный код. Кроме того, что касается остальных идей, я не собираюсь показывать реализацию API, надеясь, что он достаточно очевиден. Если вам действительно интересно, как реализовать один из API, дайте мне знать.

Внутренняя настройка

Другой способ установить эти атрибуты — сделать это в компоновщике. Дайте методы Paragraph для установки этих значений. Внутри тела тег будет выглядеть примерно так:

1
2
3
4
5
html.body(body -> {
   body.p(p -> { p.klass = "myClass"; p.id = "theParagraph";
      p.addText("the paragraph text");
   });
});

Это не страшно (особенно если эти строки установщика находятся на первой строке; размещение их в последующих строках может привести к путанице в их назначении), и это, вероятно, самое простое, но вероятность плохого кода немного высока:

1
2
3
4
5
6
7
html.body(body -> {
   body.p(p -> {
      p.klass = "myClass";
      p.addText("the paragraph text");
      p.id = "theParagraph";
   });
});

Давайте посмотрим на некоторые другие варианты.

Атрибут Объекты

Только с двумя перегрузками функции p() (одна, которая принимает только функцию построителя, а другая — как объект Attributes ), мы можем создать довольно чистый API, который будет выглядеть примерно так:

1
2
3
4
5
html.body(body -> {
   body.p(Attributes.klass("myClass").id("theParagraph"), p -> {
      p.addText("the paragraph text");
   });
});

Лично это мой любимый. Требуется больше уроков и больше реальной сложности, но я чувствую, что это самый расширяемый. Самое большое неудобство в том, что разные HTML-теги имеют разные наборы атрибутов. Вероятно, должен существовать общий класс конструктора Attributes , а также класс, специфичный для тега, который увеличивает количество перегрузок до 4 (без атрибутов, только базовые, только для тегов и обоих типов). Четыре перегрузки допустимы, но, вероятно, не должно быть. Если это кажется слишком большим, вероятно, лучше придерживаться последней стратегии.

Для полноты картины у меня есть еще один, который на самом деле может работать лучше для других API, которые не имитируют HTML или XML.

Пост-Call Building

Эта последняя идея состоит в том, чтобы Body.p() возвращал Paragraph (предпочтительно, второй этап компоновщика, поскольку в противном случае эти методы были бы доступны в Body.p() -компоновщике) для вызова методов, например:

1
2
3
4
5
html.body(body -> {
   body.p(p -> {
      p.addText("the paragraph text");
   }).klass("myClass").id("theParagraph");
});

Это существенно перемещает классы Attributes до конца, как второй этап построителя Paragraph .

Outro

Это лучшее, что я могу тебе дать. Если вы заинтересованы в создании свободно распространяемых API на таких языках, как Java, вам следует ознакомиться со статьей jOOQ о том, как они это делают . Это другой способ, который даже не принимает во внимание лямбды, и это прекрасно. В любом случае, я поговорю с вами, ребята, на следующей неделе, когда я начну серию коротких рецензий на книги.