Go言語(golang)でTUIアプリを作ろう ( その7 プログラム構造の見直し )
tviewをベースとしたオブジェクト構造を考える。
今回は単純なアプリケーションなので、あまり気にする必要はないのかもしれませんが、少し構造を見直したいと思います。画面が複数になった場合、連携部分が重要になるわけですが、それにはプログラム構造が標準化されていないといけません。
[1] プログラム(クラス)構造の階層化
一般的にプログラム構造は階層化すると理解しやすいものです。いわゆるオブジェクト指向は、その代表的なコンセプトです。golangは「オブジェクト指向言語ではない」ので、クラスという概念はありませんが、それに近い構造を作ることができます。今回は、この機能を利用してプログラム構造を見直していきます。(以下、用語としてクラスという表現を用いることを了承ください)
(1) 階層構造
tviewをベースにしたTUIアプリケーションですから、親クラスはtviewとするのが一般的でしょう。この親クラスと実クラスの中間に共通メソッドを配置したいので、Abstract的な層を設け、その下にConcrete(実装)部分を配置する階層構造とします。(Detail部は、次回実装予定)
下記に、概念図を示します。
(2) メソッド
AbstractクラスであるMyApplicationには、3つのメソッドを配置します。
1) アプリケーションの終了処理
func (self *MyApplication) exit()
2) 画面作成、または再描画処理の呼び出し。
func (self *MyApplication) display(primitive tview.Primitive)
3) プログラムの起動。appがnilだったら、tviewの初期処理を行う。
func (self *MyApplication) run(app *tview.Application, primitive tview.Primitive)
tviewからの継承部分と、Abstract層のコード(application.go)を示しておきます。
type MyApplication struct {
app *tview.Application
}
func (self *MyApplication) exit() {
self.app.Stop()
}
func (self *MyApplication) display(primitive tview.Primitive) {
self.app.SetRoot(primitive, true)
}
func (self *MyApplication) run(app *tview.Application, primitive tview.Primitive) {
if app == nil {
self.app = tview.NewApplication()
if err := self.app.SetRoot(primitive, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
} else {
self.app = app
self.display(primitive)
}
}
:
:
[2] パッケージ(ディレクトリ)構造の見直し
golangには、プロジェクト構造に対する標準指針(Standard Go Project Layout)というものがあるようです。
さすがに、「すべてのプログラムがmainパッケージ」ではまずいと思いますが、今回のような単純なアプリケーションに全面適用するのは、いささか複雑すぎるように思われます。すこし外れているかもしれませんが、今回は下記の構成にしてみました。
エントリポイントをcmdへ、UI部分をtuiとしています。
プログラムのディレクトリ構造は下記のようになります。
cmd/
main.go
tui/
application.go
common.go
list.go
mywidget.go
go.mod
go.sum
[3] プログラムの見直し
次にプログラム個々の実装を修正していきます。
(1) application.go
先にAbstract層のコードを示しましたが、それに続けてConcrete層であるMainListの設定を行います。
- Abstract層との継承関係を確立しておき、MainList生成部分であるNewMainListを、Singletonで実装。
- 画面作成部分をdoFormatというメソッド名に統一、最終処理はAbstract層に委譲。
- Entry部分として、initメソッドを定義。
- 画面間で連携するデータは、common.goに記述。
下記に、追加コード(application.go)を示します、
:
// ------------------------------------------------------
type MainList struct {
MyApplication
}
var mainList *MainList = nil
func NewMainList() *MainList {
if mainList == nil {
abstract := MyApplication{}
mainList = &MainList{MyApplication: abstract}
}
return mainList
}
func (self *MainList) display(common *Common) {
self.MyApplication.display(self.doformat(common))
}
func (self *MainList) run(app *tview.Application, common *Common) {
self.MyApplication.run(app, self.doformat(common))
}
func (self *MainList) Init(databaseName string, connectString string, cols int, rows int) {
common := NewCommon()
common.reset()
common.databaseName = databaseName
common.connectString = connectString
common.cols = cols
common.rows = rows
self.run(nil, common)
}
(2) main.go
アプリケーションの起動は、別プログラム(main.go)に分割しました。
tcellでターミナルの画面サイズを取得後、MainListのinit処理を呼び出す形にしています。
:
func getScreenSize() (int, int) {
s, _ := tcell.NewScreen()
s.Init()
cols, rows := s.Size()
s.Fini()
return cols, rows
}
// -------------------------------------------------
func main() {
cols, rows := getScreenSize()
if len(os.Args) == 3 {
tui.NewMainList().Init(os.Args[1], os.Args[2], cols, rows)
} else {
tui.NewMainList().Init("", "", cols, rows)
}
}
(3) list.go
list.goの関数は全て、MainListのメソッドに変更します。
:
func (self *MainList) firstPage(common *Common) bool {
return common.from == 1
}
func (self *MainList) lastPage() bool {
return isLast
}
func (self *MainList) nextPage(common *Common) {
if !self.lastPage() {
common.from += (common.rows - 2)
self.display(common)
}
}
:
(4) common.go
複数画面間の連携データを格納するプログラムです。今回は、画面サイズの取得とページングコントロール部分のみ使用しています。
(5) mywidget.go
下記のメソッドを追加しました。
func myFlexFocus(app *tview.Application, flex *tview.Flex, reverse bool) {
Flexコンテナ内のPrimitiveに対し、focus移動を行うメソッドです。
[4] プログラムの実行
下記のコマンドを入力します。
go run cmd/main.go
以上で、プログラム構造の見直しは終了です。
次回は、一覧画面(list)から選択されたレコードの詳細(Detail)表示プログラムを作成、連携処理を行います。