2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

jsonをデコードしてクラスに割り当てたい【JavaScript編】

Last updated at Posted at 2019-07-02

概要

  • やったこと

    • Pythonで書いたこの記事のnode.js版です。
    • jsonをparseした結果を基に、任意のクラスのインスタンスを生成します。
    • Javascriptもjsonを扱う機会は沢山あるよなと思ったので書いてみました。
      • そもそもJavaScript Object Notationなんだし…
  • 得られたこと

    • Python版とほぼ同じ動作をするNode.js版を作成した。
    • 試してないけど、ブラウザ上でも動くと思う(Babel等で変換したらほぼ問題なさそう)。
      • でも、ブラウザ側でどんなクラスを扱うのかは考えどころ。

得られる成果の違い

概要で触れたとおり、今回はPythonでやったことをnode.jsでやっただけ、もっと直接的に言えばJSON.parseの結果を任意のクラスのインスタンスとして再構築するという記事です。
JavaScriptの場合、Pythonとは違って、JSON.parseして得られた結果のオブジェクトにアクセスするために、最初からobj.key.valueのような構文が使えます。
よって、値へのアクセス手段を簡略化する効果はないのですが、振舞いを持つクラスにjsonの中身を割り当てるための仕組みとしては、今回の試みはPython版と同じく有効です。

設計

では、前回と同じように、json中の任意のキーをクラスにマッピングする仕組みを作ります。以下はテストに使うクラスと、jsonとクラスのマッピング定義です。
jsonのルート要素には名前がないので、仮にROOT_CLASSというプロパティを充てます。ここはPython版とやや違います。1

テスト用クラスとマッピング
// テスト用クラスその1
class TestObject{
    test_method(){
        return 'return:' + this.value1
    }
}
// テスト用クラスその2
class NestedObject{
    test_method(){
        return 'NestedObject.test_method'
    }
}
// マッピング定義
let object_mapping = {
    ROOT_CLASS : TestObject,
    nested : NestedObject
}

これをこんな風に使って、jsonからクラスのインスタンスを生成するコードを書きます。

usage
// ソースjson
let source_json = JSON.parse(`{
    "value1" : "string value 1",
    "nested" : {
        "value" : "nested value 1"
    }
}`)

let builder = new ObjectBuilder(object_mapping)
let result = builder.build(source_json)
console.log(result.nestedd.test_method()) // 'NestedObject.test_method'

実装

こんなコードになりました。

object_builder.js
class ObjectBuilder {

    constructor(mapping) {
        this.mapping = mapping
    }

    build(src) {
        let rootInstance = null

        if (src instanceof Array){
            rootInstance = this.buildSequence(src, this.mapping.ROOT_CLASS)
        }
        else {
            rootInstance = this.buildObject(new this.mapping.ROOT_CLASS(), src)
        }
        return rootInstance
    }

    buildObject(current, src) {
        for (let key in src){
            if (key in this.mapping){
                current[key] = this.buildValue(src[key], this.mapping[key])
            }
            else {
                current[key] = src[key]
            }
        }
        return current
    }

    buildSequence(src, func) {
        let list = []
        for (let value of src){
            list.push(this.buildValue(value, func))
        }
        return list
    }
    
    buildValue(value, func) {
        if (value instanceof Array) {
            return this.buildSequence(value, func)
        }
        else if (value instanceof Object){
            return this.buildObject(new func(), value)
        }
        else {
            // 値のみ引数として渡す
            return func(value)
        }
    }
}

module.exports = ObjectBuilder

ほとんどPython版と変わらないのですが、JavaScriptでクラスのインスタンスを生成するにはnew演算子が必要です。これにより、Python版で実装していた。「数値と文字列を変換する」部分などはnew演算子を使わない関数呼び出しをしています。しかし、この機能はこの記事の目的には蛇足な気もしてきました。2

使い方の紹介を兼ねたユニットテスト

長いので折りたたみます。
テストにはmochaを使っています。
test_object_builder.js
const ObjectBuilder = require('../object_builder')
const assert = require('assert')

// テスト用クラスその1
class TestObject{
    test_method(){
        return 'return:' + this.value1
    }
}

// テスト用クラスその2
class NestedObject{
    test_method(){
        return 'NestedObject.test_method'
    }
}

describe('jsonから任意のクラスのインスタンスに変換', function(){

    it('ルートになるクラスのプロパティを設定する', function(){
        // マッピング定義
        let object_mapping = {
            ROOT_CLASS : TestObject,
        }
        // ソースjson
        let source_json = JSON.parse(`{
            "value1" : "string value 1"
        }`)
        let builder = new ObjectBuilder(object_mapping)
        let result = builder.build(source_json)
        assert.equal(result.value1, 'string value 1')

        // 生成したクラスのメソッドを呼んでみる
        assert.equal(result.test_method(), 'return:string value 1')
    })
    it('ネストしたクラスを生成する', function(){
        // マッピング定義
        let object_mapping = {
            ROOT_CLASS : TestObject,
            nested : NestedObject
        }
        // ソースjson
        let source_json = JSON.parse(`{
            "value1" : "string value 1",
            "nested" : {
                "value" : "nested value 1"
            }
        }`)

        let builder = new ObjectBuilder(object_mapping)
        let result = builder.build(source_json)
        assert.equal(result.value1, 'string value 1')
        assert(result.nested instanceof NestedObject)
        assert.equal(result.nested.value, 'nested value 1')
    })
    it('マッピングを指定しない場合はただのobject', function(){
        // マッピング定義
        let object_mapping = {
            ROOT_CLASS : TestObject,
        }
        // ソースjson
        let source_json = JSON.parse(`{
            "value1" : "string value 1",
            "nested" : {
                "value" : "nested value 1"
            }
        }`)
        let builder = new ObjectBuilder(object_mapping)
        let result = builder.build(source_json)
        assert.equal(result.value1, 'string value 1')
        assert(!(result.nested instanceof NestedObject)) // 当たり前だけど
        assert.equal(result.nested.value, 'nested value 1')
    })

    it('リストのプロパティを持つ場合', function(){
        // マッピング定義
        let object_mapping = {
            ROOT_CLASS : TestObject,
            nestedObjects : NestedObject,
        }

        // ソースjson
        let source_json = JSON.parse(`{
            "value1" : "string value 1",
            "nestedObjects" : [
                {
                    "value" : "0"
                },
                {
                    "value" : "1"
                },
                {
                    "value" : "2"
                }
            ]
        }`)

        let builder = new ObjectBuilder(object_mapping)
        let result = builder.build(source_json)
        assert.equal(result.value1, 'string value 1')
        assert(result.nestedObjects instanceof Array)
        assert.equal(result.nestedObjects.length, 3)
        
        for (let i = 0; i<result.nestedObjects.length; i++) {
            assert(result.nestedObjects[i] instanceof NestedObject)
            assert.equal(result.nestedObjects[i].value, i)
        }
    })
    
    it('ルート要素自体がリストの場合', function(){
        // マッピング定義
        let object_mapping = {
            ROOT_CLASS : TestObject,
        }

        // ソースjson
        let source_json = JSON.parse(`[
            {
                "value" : "0"
            },
            {
                "value" : "1"
            },
            {
                "value" : "2"
            }
        ]`)

        let builder = new ObjectBuilder(object_mapping)
        let result = builder.build(source_json)
        assert(result instanceof Array)
        assert.equal(result.length, 3)

        for (let i = 0; i<result.length; i++) {
            assert(result[i] instanceof TestObject)
            assert.equal(result[i].value, i)
        }
    })
    
    it('jsonとクラスを相互に変換する', function(){
        // マッピング定義
        let object_mapping = {
            ROOT_CLASS : TestObject,
            nested : NestedObject
        }
        // ソースjson - 比較の都合のため一行で
        let source_json = JSON.parse(`{"value1": "string value 1", "nested": {"value": "nested value 1"}}`)

        let builder = new ObjectBuilder(object_mapping)
        let result = builder.build(source_json)
        assert.equal(result.value1, 'string value 1')
        assert(result.nested instanceof NestedObject)
        assert.equal(result.nested.value, 'nested value 1')

        // 再変換しても結果が同じこと
        result = builder.build(JSON.parse(JSON.stringify(result)))
        assert.equal(result.value1, 'string value 1')
        assert(result.nested instanceof NestedObject)
        assert.equal(result.nested.value, 'nested value 1')
    })
    it('型変換に使ってみる', function(){
        // マッピング定義
        let object_mapping = {
            ROOT_CLASS : TestObject,
            value1 : String
        }
        // ソースjson - データは数値
        let source_json = JSON.parse(`{
            "value1" : 256
        }`)
        let builder = new ObjectBuilder(object_mapping)
        let result = builder.build(source_json)
        assert.strictEqual(result.value1, '256')

        // 生成したクラスのメソッドを呼んでみる
        assert.equal(result.test_method(), 'return:256')
    })
})

相互変換について

jsonをクラスの振舞いで処理した後、インスタンスを再びjsonに変換したいことも多々あると思いますが、JavaScriptではクラスのインスタンスもそのままJSON.stringifyで変換することができます。

ブラウザでの利用を考える

今回はNode.jsで動かしてみましたが、JavaScriptの主要な用途としては他にブラウザ上での利用が考えられます。たぶん、このコードはES6が動く環境なら動くと思います(module周りだけ自信なし)。ES6が動かないなら、Babel等で変換してしまえば問題ないと思います。3
しかし、フロントエンドでクラスが振舞いを持つことには良し悪しがあると思います。例えばフロントエンドがビジネスロジックを持ったりしてしまうと、フロントとバックの両方にビジネスロジックが散ったりして、コードが混乱する恐れがあります。4
ユーザ定義のフロントエンドにクラスを持たせるなら、外観に特化した振舞いだけを持つくらいに抑えておくのが無難かと思います。5

  1. 本当はなにか定数をキーに充てて、ObjectBuilderのモジュールから取得させた方がバグは少なくなりそうです。Mapの使用も考えましたが、記事の目的からは外れるのでやめました。

  2. 高機能に作るなら、呼び出される関数がコンストラクタであるか否か判別することもできそうですが、記事で書くには複雑です。他に、全ての個所でnew演算子の使用をやめて、ファクトリメソッド的な関数を渡してもいい。しかし、本題とは逸れちゃいますね。

  3. そもそもES5だとclassがないからそのままでは意味がなさそうです。

  4. フロント側でも書けるロジックというのはあっても、たとえばDB周りとか、バック側じゃないと書けないロジックというのも通常は多々あるはずです。フロント側も概観のためにかなりの記述量が必要なので、ビジネスロジックはバック側にまとめた方が無難かと思います。

  5. しかし、Vue.jsとかフロントエンドのフレームワークが、ユーザ定義のクラスと噛み合って動作するか、ちょっと調べてみないと解らないですね。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?