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)表示プログラムを作成、連携処理を行います。


ソースコードについて

GitHubに登録しました。今回のコードは、Section07となります。