MVVMって、こんな感じ?


いつものことですが、シリアル通信とかファイル編集とか、ちょっとした実験や雑用をこなすためにチョチョイっとTcl/Tkでスクリプトを書き、使っているうちに「バイナリ通信もできるようにしよう」だの「このファイルフォーマットにも対応しよう」だのと手を加えているうちにスクリプトが肥大化してしまって把握しにくくなってしまいます。

そこで、何かGUIアプリに適した記述方法や動作モデルが無いかとググっていてMVVMなるものを知りました。(すみません、今更でしょうね。本職さんから見れば)

MVVMは「Model」「View」「ViewModel」という3つのブロックに分ける考え方のようです。基本的にはWindows向けソフト開発フレームワークの中の概念らしいのですが、やってみれば何かのヒントになるかも?ということでTcl/Tkでそれっぽく記述してみました。

まずは何も考えず、いつもの調子で記述してみます。

simple.tcl
#!/bin/sh
#-*-mode:tcl;tab-width:4;coding:cp932-*-\
    exec wish -encoding cp932 "$0" ${1+"$@"}

set WidNumber 0
proc Wid {{parent ""}} {
    global WidNumber
    if {[string index $parent end] ne "."} {
        append parent "."
    }
    return ${parent}wid[incr WidNumber]
}

proc Sum {} {
    global Value

    set Value(S) "?"

    foreach key {A B} {
        if {![string is double -strict $Value($key)]} {
            tk_messageBox -icon error -title Error -message "$keyは数値ではありません。"
            return
        }
    }

    set Value(S) [expr {$Value(A) + $Value(B)}]
}

set Title(A) "数値A"
set Title(B) "数値B"
set Title(S) "AとBの合計"
set Value(A) 0
set Value(B) 0
set Value(S) "?"

wm title . "Form1"
grid columnconfigure . 1 -weight 1

foreach key {A B} {
    set w [list]
    lappend w [label [Wid] -textvariable Title($key)]
    lappend w [entry [Wid] -textvariable Value($key)]
    grid {*}$w -padx 5 -pady 5 -sticky we
    grid configure [lindex $w 0] -sticky e
}

set w [list]
lappend w [button [Wid] -textvariable Title(S) -command Sum]
lappend w [entry [Wid] -textvariable Value(S) -state readonly]
grid {*}$w -padx 5 -pady 5 -sticky we
bind [lindex $w 0] <Key-Return> Sum

実行すると、こんな感じ。

これを「MVVMって、こんな感じかな~?」と考えつつ書き直してみました。
MVVMの概要を説明する文章として、

  • Viewは、ViewModelに含まれたデータをデータバインディング機構のようなものを通じて自動的に描画するだけ
  • ViewModelは、Viewから受け取った入力を適切な形に変換してModelに伝達する
  • Modelは、アプリケーションのドメイン(問題領域)を担う

といったものが多かったので、それを尊重して記述してみました。

(TclOOにはまだ慣れていないので、namespaceで記述しました)

mvvm_ns.tcl
#!/bin/sh
#-*-mode:tcl;tab-width:4;coding:cp932-*-\
    exec wish -encoding cp932 "$0" ${1+"$@"}

namespace eval ::Model {
    proc Sum {a b} {
        return [expr {$a + $b}]
    }
}

namespace eval ::ViewModel {
    variable Title
    set Title(A) "数値A"
    set Title(B) "数値B"
    set Title(S) "AとBの合計"

    variable Value
    set Value(A) 0
    set Value(B) 0
    set Value(S) "?"

    proc Sum {} {
        variable Value

        set Value(S) "?"

        foreach key {A B} {
            if {![string is double -strict $Value($key)]} {
                tk_messageBox -icon error -title Error -message "$keyは数値ではありません。"
                return
            }
        }

        set Value(S) [::Model::Sum $Value(A) $Value(B)]
    }
}

namespace eval ::View {
    variable WidNumber 0

    proc Wid {{parent ""}} {
        variable WidNumber
        if {[string index $parent end] ne "."} {
            append parent "."
        }
        return ${parent}wid[incr WidNumber]
    }

    proc Form1 {} {
        wm title . "Form1"
        grid columnconfigure . 1 -weight 1

        foreach key {A B} {
            set w [list]
            lappend w [label [Wid] -textvariable ::ViewModel::Title($key)]
            lappend w [entry [Wid] -textvariable ::ViewModel::Value($key)]
            grid {*}$w -padx 5 -pady 5 -sticky we
            grid configure [lindex $w 0] -sticky e
        }

        set w [list]
        lappend w [button [Wid] -textvariable ::ViewModel::Title(S) -command ::ViewModel::Sum]
        lappend w [entry [Wid] -textvariable ::ViewModel::Value(S) -state readonly]
        grid {*}$w -padx 5 -pady 5 -sticky we
        bind [lindex $w 0] <Key-Return> ::ViewModel::Sum
    }
}

::View::Form1

要はウィジェットがViewで、-commandに渡す処理と-stateを操作する処理がViewModelで、-commandに渡す処理の汎用サブルーチンがModelってことなのかな。

それであってるなら・・・普通にそうする・・・よねぇ。

なんかちょっと拍子抜け?