2017-01-25 4 views
13

Я пытаюсь создать DSL для создания JSONObjects. Вот класс строитель и использование образца:Kotlin DSL для создания объектов json (без создания мусора)

import org.json.JSONObject 

fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { 
    val builder = JsonObjectBuilder() 
    builder.build() 
    return builder.json 
} 

class JsonObjectBuilder { 
    val json = JSONObject() 

    infix fun <T> String.To(value: T) { 
     json.put(this, value) 
    } 
} 

fun main(args: Array<String>) { 
    val jsonObject = 
      json { 
       "name" To "ilkin" 
       "age" To 37 
       "male" To true 
       "contact" To json { 
        "city" To "istanbul" 
        "email" To "[email protected]" 
       } 
      } 
    println(jsonObject) 
} 

Выход выше код:

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true} 

Он работает, как ожидалось. Но он создает дополнительный экземпляр JsonObjectBuilder каждый раз, когда он создает объект json. Можно ли написать DSL для создания объектов json без лишнего мусора?

+3

Kotlin должен создать объект функции, который должен быть передан в 'json {...}', поэтому проблема «создать лишние объекты» является ошибочной с самого начала. JVM достаточно эффективен в оптимизации коротких объектов. Если вы не сравните свой код с кодом и на 100% уверены, что создание экземпляров 'JSONObjectBuilder 'является узким местом вашей производительности, я бы не стал об этом беспокоиться. (Личное примечание: я бы сделал ваш строитель интерфейсом и спрятал фактическую реализацию в частном классе, поэтому вы не открываете поле json.) –

+1

Yep, 'json {...}' должно быть, должно быть 'inline' – voddan

ответ

11

Вы можете использовать Deque в качестве стека для отслеживания текущего JSONObject контекст с одним JsonObjectBuilder:

fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { 
    return JsonObjectBuilder().json(build) 
} 

class JsonObjectBuilder { 
    private val deque: Deque<JSONObject> = ArrayDeque() 

    fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { 
     deque.push(JSONObject()) 
     this.build() 
     return deque.pop() 
    } 

    infix fun <T> String.To(value: T) { 
     deque.peek().put(this, value) 
    } 
} 

fun main(args: Array<String>) { 
    val jsonObject = 
      json { 
       "name" To "ilkin" 
       "age" To 37 
       "male" To true 
       "contact" To json { 
        "city" To "istanbul" 
        "email" To "[email protected]" 
       } 
      } 
    println(jsonObject) 
} 

Пример вывода:

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true} 

Calling json и build через несколько потоков на один JsonObjectBuilder будет проблематичным, но это не должно быть проблемой для вашего случая использования.

+0

Мне нравятся стеки, но разве это не страдает от одной и той же проблемы (т. Е. Создает JSONObjectBuilder и JSONObject при каждом вызове 'json()')? Я думаю, что вы на правильном пути - вам просто нужно запомнить результат последнего вызова 'JSONObject.put()', чтобы вы могли называть 'put()' on _that_ для вложенных полей вместо создания новый 'JSONObject'. Надеюсь это имеет смысл! :) –

+0

В приведенном выше примере создается только один JsonObjectBuilder и два JSONObject (s), который намного лучше, чем решение, которое я придумал. Единственное, что конечная строка json находится в обратном порядке. – ilkinulas

+0

@HoundDog. В API 'org.json.JSONObject' вам нужно создать новый' JSONObject', чтобы вставить его внутри другого, поэтому я не верю, что здесь есть дополнительные объекты. Я надеюсь, что в этом есть смысл. :-) – mfulton26

6

Вам нужна DSL? Вы теряете возможность принудительного String ключей, но ваниль Котлин не так уж плохо :)

JSONObject(mapOf(
     "name" to "ilkin", 
     "age" to 37, 
     "male" to true, 
     "contact" to mapOf(
       "city" to "istanbul", 
       "email" to "[email protected]" 
     ) 
)) 
+2

Согласен, эта версия уже похожа на DSL. Но он создает временные массивы и пары. – ilkinulas

+0

Мне очень нравится это решение для создания JSON на Java, потому что он использует уже чистую DSL-систему Kotlin для создания JSON-подобных данных, но у нее такие же накладные расходы, что и пример в вопросе. Каждый 'mapOf' создает новую« карту », которая затем копируется и отбрасывается для каждого' JSONObject' (включая вложенный 'JSONObject' внутри верхнего уровня). – mfulton26

1

Да, это возможно, если вам не нужно какое-либо промежуточное представление узлов, и если контекст всегда то же самое (рекурсивные вызовы не отличаются друг от друга). Это можно сделать, написав вывод сразу.

Однако это сильно увеличивает сложность кода, потому что вам необходимо немедленно обрабатывать вызовы DSL, не сохраняя их в любом месте (опять же, чтобы избежать избыточных объектов).

Пример (см ее демо here):

class JsonContext internal constructor() { 
    internal val output = StringBuilder() 

    private var indentation = 4 

    private fun StringBuilder.indent() = apply { 
     for (i in 1..indentation) 
      append(' ') 
    } 

    private var needsSeparator = false 

    private fun StringBuilder.separator() = apply { 
     if (needsSeparator) append(",\n") 
    } 

    infix fun String.to(value: Any) { 
     output.separator().indent().append("\"$this\": \"$value\"") 
     needsSeparator = true 
    } 

    infix fun String.toJson(block: JsonContext.() -> Unit) { 
     output.separator().indent().append("\"$this\": {\n") 
     indentation += 4 
     needsSeparator = false 
     block([email protected]) 
     needsSeparator = true 
     indentation -= 4 
     output.append("\n").indent().append("}") 
    } 
} 

fun json(block: JsonContext.() -> Unit) = JsonContext().run { 
    block() 
    "{\n" + output.toString() + "\n}" 
} 

val j = json { 
    "a" to 1 
    "b" to "abc" 
    "c" toJson { 
     "d" to 123 
     "e" toJson { 
      "f" to "g" 
     } 
    } 
} 

Если вам не нужны отступы, но только правильный JSON, это может быть легко хотя и упрощен.

Вы можете сделать json { } и .toJson { } функции inline избавиться даже классы лямбды и, таким образом, вы достигнете почти нулевого объект накладных расходов (один JsonContext и StringBuilder со своими буферами все еще выделяются), но это потребует вас изменить модификаторы видимости элементов, используемые этими функциями: общедоступные встроенные функции могут обращаться только к public или к @PublishedApi internal членам.

+0

Это интересное решение, но типы теряются в финальной строке json. Каждое значение преобразуется в строку. – ilkinulas

+0

@ilkinulas Вы можете заменить '\ '$ value \" 'на' $ {JSONObject.valueToString (значение)} ', чтобы сохранить типы. – mfulton26

1

Я не уверен, правильно ли задал вопрос. Вам не нужен строитель?

class Json() { 

    val json = JSONObject() 

    constructor(init: Json.() -> Unit) : this() { 
     this.init() 
    } 

    infix fun <T> String.To(value: T) { 
     json.put(this, value) 
    } 

    override fun toString(): String { 
     return json.toString() 
    } 
} 

Вы можете просто сделать это:

val json = Json { 
    "name" To "Roy" 
    "body" To Json { 
     "height" To 173 
     "weight" To 80 
    } 
} 

println(json) 

{"name":"Roy","body":"{\"weight\":80,\"height\":173}"}

+0

это очень элегантный – Greyshack

1

Найдено другое решение. Вы можете просто наследовать класс JSONObject без необходимости создавать другие объекты.

class Json() : JSONObject() { 

    constructor(init: Json.() -> Unit) : this() { 
     this.init() 
    } 

    infix fun <T> String.To(value: T) { 
     put(this, value) 
    } 
} 

fun main(args: Array<String>) { 
    val jsonObject = 
      Json { 
       "name" To "ilkin" 
       "age" To 37 
       "male" To true 
       "contact" To Json { 
        "city" To "istanbul" 
        "email" To "[email protected]" 
       } 
      } 
    println(jsonObject) 
} 

Выходной код будет таким же.

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true} 
Смежные вопросы