Pythonは、現在、世界で最も使われているプログラミング言語のひとつです。言語仕様に絞りをかけていて、Python言語そのものには必要な機能しか搭載しないようにしているため、とても分かりやすい(しかし冗長にも見える)コードにしやすい言語です(結局はプログラマー次第)。後発のGo言語などを触ると「もう少し何とかしてほしい」という部分もあるのですが、強力なモジュール化機能を備えているため、他の人が作ったライブラリを導入しやすく、パッケージマネージャを使うなどして、様々な機能を比較的簡単に利用できます。最近では人工知能分野での利用が多くなっています。
Fletは、Googleが開発したマルチプラットフォーム向けのGUIアプリケーション開発環境FlutterをPythonで利用できるようにするパッケージです。
Flutterは、アプリケーションをDartというプログラミング言語で書けば、WindowsでもmacOSでも他のOSでも、同じように動作させることができる、というGUIアプリケーション開発環境です。これは実に素晴らしいものなのですが、使うためにはDart言語を学ばなければならないため、DartはJavaScriptを踏襲したものであるとはいっても(DartプログラムはJavaScriptに変換できる)、二の足を踏んでしまいがちです(踏みました)。
そこでFletの出番です。Fletならば、世界中に普及して広く利用されているPythonを使ってアプリケーションを作ることができます。また、PythonでマルチプラットフォームなGUIアプリケーションを作ろうとすると、選択肢がPyQtくらいしかなく、Qtも独自の世界を持っているので学習コストが高くなってしまいがちという問題があったのですが、Flutterは世界中に普及しているWeb技術を使って作られているので、学習コストは比較的低いものです。
つまり、マイナー(Dartプログラマーの皆さま申し訳ありません)な技術や知識を使うより、メジャーな技術や知識を使ってGUIアプリケーションを作りたい、という場合、FletとPythonの組み合わせはそのニーズにバッチリと刺さってくるのです。
しばらくFletを使い倒し、ようやく、様々なノウハウが蓄積してきたので、投稿にしたいと思います。
Fletの基本的な構造
正確な内部構造まで把握しているわけではないのですが、私の理解としては、Fletを使ったアプリケーションは以下のような構造になります。
アプリケーションはFlet(及びユーザー作成のコントロール)だけにアクセスします。Fletは内部でFlutterにアクセスし、最終的にFlutterが必要なHTMLやCSSを作成してくれているようです。
ここで気をつけなければならないのは、Fletを使ったアプリケーションにおいて、アプリケーションはFlutterが提供する機能の範囲を超えることができない、という点です。HTMLやCSSを使えば実現できるようなことでも、FletはHTMLやCSSに直接アクセスする機能を備えていないので、どんなに頑張っても実現できません。Flet(Flutter)の範囲内で解決する必要があります。そのため、Web開発の知識があればどうにかなるというものではなく、どうしても「Fletの作法」というものの理解が必要になり、学習コストは生じます。とはいえ、HTMLやCSSの技術がベースになっているので、機能的な概念やプロパティ名などは共通しているものが多く、大きな負担にはならないはずです。
もう一つ、FletはPythonのオブジェクトをFlutter上のオブジェクトに変換しなければならず、その作業を完全に自動で行うことができないことにも気をつけなければなりません。どこかで、アプリケーションからFletに対して、PythonのオブジェクトをFlutterのオブジェクトに変換するよう指示を出さなければならないのです。これを忘れると、アプリケーション側でデータ更新をしても、その情報がFlutterに伝わらず、スクリーンに表示されないという現象が発生します。
Fletを使ったアプリケーションの設計
Fletに限らず、GUIアプリケーションの作成で困るのは、テストです。実際の操作を再現して、期待する結果になるか自動でテストする(使ったことがないので、そのようなものだと思います)フレームワークもありますが(Squishなどがあるそうです)、作成するGUIのすべてにテストを作成するのは骨の折れる作業です。また、アプリケーションの設計によっては、純粋なGUIの動作テストになっておらず、GUI部分と論理部分が混在したテストになっていることもあり、非効率的なものになっていることもあります。
テストコードは、アプリケーションの品質を保つために必須です。リファクタリングなどでコードを変更したとき、はたして以前と同じようにコードが動くのか、細かい内容であっても、確かめておく必要があります。それを積み重ねていくことで、品質が保証された、バグの少ないアプリケーションを作り上げることができます。また、テストコードは仕様書の代わりにもなります。具体的な値を使って動作結果が記述されるので、これ以上分かりやすい仕様書はないと言えるでしょう(テストコードの書き方にもよりますが)。
Pythonではpytestというテスティングフレームワークが有名で、開発環境でもサポートされています。VisualStudio Codeからも簡単にテストを実行できるようになっています(実行ボタンを押すだけ)。そこで、このようなフレームワークで実行できるようなテストを作るためにはどうすればいいのか、ということを考えていきます。
テストを作りやすくするため、アプリケーションをいくつかの機能的な部分に分けます。このような設計方法にはMVCなどがありますが、ここで紹介する方法は、それらに若干の修正を加えたものになります。各部分に対応するディレクトリと初期化ファイル(__init__.py)を作成し、Pythonのパッケージとして使えるようにします。なお、変数名と衝突するのを避けるため、各パッケージの名前は複数形(schemaならばschemas)にします。
schemas
schemasパッケージは、アプリケーションで使用するオブジェクトの型を提供します。これは機能というより基盤なので、すべてのパッケージで使用できるようにします。
MVCにおけるModelに相当しますが、提供するのは型定義やデータ演算機能だけであり、ストレージから構築する機能は提供しません。それはstoragesパッケージで提供します。
クラスに演算機能を備えた場合、それらのテストコードを作成します。
storages
storagesパッケージは、具体的なストレージへのアクセス機能を提供します。MySQLやPostgreSQLなどのデータベースやファイルシステムなど、具体的なストレージからデータを読み込んでschemasで定義されるデータ型のオブジェクトを構築したり、逆にストレージにデータを書き込みます。
抽象クラス(インターフェイス・プロトコル)を用意し、各種ストレージにアクセスするクラスで実装します。テストコードは、抽象クラスに対して作成しても意味がないので、各種ストレージ用のクラスに対して作成します。たとえば、データベースに対応したストレージについては、与えたschemaに対して正しいSQLが生成されるかをチェックします。データベースに接続するテストを作成してもよいのですが、テスト実行にかかるコストが大きくなるので、SQLiteのように簡単にデータベースを作成できるようなものでもなければ、生成されるSQLのチェックをすれば十分だと思います。
ただ、テストで正解としていたSQLが間違えており、アプリケーションを使ってみると「おかしいな?」となることもあったので、アプリケーションのテストとは別にSQLのチェックをしておいた方がいいかもしれません。
agents
agentsパッケージは、storageに書き込むべきデータの処理機能を提供します。agentは、利用する場面ごとにクラスを作成します。ひとつのagentで様々なviewに対応するのではなく、ひとつのviewに対してひとつのagentを作成した方が良いだろうと思います。アプリケーションの規模などにもよりますが、利用場面ごとにagentを作成すると、作成するクラスが持つデータを最小限にすることができます。そのため、時間が経ってからコードを見直しても、内容を把握することが容易になります。
agentは、storageにしかアクセスしないようにしなければなりません。viewのオブジェクトは使わないようにし、必要であればメソッド引数や属性に設定された値だけを使うようにします。複数のagentを束ねるagentを作ることも可能です。
agentのテストは、特定のstorageを与えた場合に、期待する結果を返すかどうか確認するものになります。このとき、agentに与えるstorageは、テスト用に機能が制限されたもので構いません。また、データベースを利用するようなものの場合、特定の操作で必ず例外が発生するなど、テストしたい局面を実現するstorageを与えることもできます(テスト用storageのテストコードを作成するのも忘れずに)。
views
viewsパッケージは、GUIアプリケーションの論理部分を提供します。Fletでは、テキスト表示エリアにText、テキスト入力エリアにTextField、ボタンにElevatedButtonなどのクラス(Fletコントロール)が対応しています。viewsパッケージは、それらの具体的なクラスを消し去った、抽象的なGUIアプリケーションを提供します。
たとえば、価格を入力できるようにするとき、FletではTextFieldオブジェクトを使いますが、viewsではstr型の値を使うようにします。どのように値を入力するか、どこに配置するのか、などは考えません。ただ単にstr型のpriceという属性がある、という情報だけを使ってGUIアプリケーションを作成していきます。
viewsは、agentにしかアクセスしないようにしなければなりません。storageやflet_viewのクラスを使わないようにし、storageにアクセスしたいのであれば、対応するagentを受け取るようにします。本質的な部分に必要のないGUIの要素は省くようにします。複数のviewを束ねるviewを作ることもできます。
viewsのテストは、特定の操作を行ったとき、viewsの属性が期待された値になっているかどうかを確認するものになります。たとえば、priceに100を設定したとき、taxに10が設定されるか(10%消費税を想定)を確認します。agentsのときと同様に、viewsに与えるagentはテスト用に機能制限されたagentでも構いません。
flet_views
flet_viewsパッケージは、GUIアプリケーションのFlet実装を提供します。その内部は、viewsパッケージで定義された各クラスに対するラッパーのようになります。flet_viewsパッケージは、Fletコントロールの情報とviewsパッケージの情報を相互変換する機能を提供します。
この部分は実際に動作させながらテストすることになるのですが、情報の変換が正しくできていることが確認できれば、その背後にあるGUIアプリケーションの論理部分はviewsのテストで期待通りに動作することが保証されているので、GUIアプリケーションの動作を保証することができます。動作が何かおかしい、という場合でも、viewsのテストが通っているのであれば、flet_viewsの部分だけをチェックすれば良いので、問題の切り分けが楽になります。
flet_viewは、viewにしかアクセスできないようにしなければなりません。情報変換以外の機能を実装するとバグの温床になりかねませんので、絶対にやめましょう。
Fletを使ったアプリケーションの作成
アプリケーションの具体的なコーディングについては、次回以降の投稿で紹介していきたいと思います。(きっと書くはずです。)