🤏

ダイアグラムアプリのためのstate pattern実践

2023/11/25に公開

この記事は下記記事をAIにガバっと投げて内容が違わないよう細部を調整したものです。

https://miyanokomiya.tokyo/2023/11/state-pattern-for-diagraming-app/

以下本文


このプレゼンテーションに触発され(実際にはイベントに参加していませんでしたが)、そのプレゼンテーションで特集されていたものにそれなりに似ているダイアグラムアプリについてのアイデアをいくつかメモしました。決定版と言えるような効果的な方法は明らかにありませんが、この種のアプリに関する知識と実践が増えれば増えるほど、我々のアプリはより優れたものになるでしょう。それならば、私の実験的な実践を誰かのために書き留めようではないでしょうか。

https://speakerdeck.com/seanchas116/gong-tong-bian-ji-toroturunozuo-rifang

Where we are

HTML Canvasを使用して様々なアプリケーションを作成できます。これは少なくとも同じだけのバラエティのstate patternバラエティが存在することを意味します。ここでは、もう少し事を簡単にしましょう。私たちが作成したいのはダイアグラムアプリです。

なぜstate patternに従うべきか、またはstate patternとは何かといったトピックについてはこの記事では話しません。この記事の主題は、実際にstate patternをダイアグラムアプリにどのように実践的に適用するかです。

state patternに慣れるために、もしまだならば、事前にこの傑作記事を読むことを強くお勧めします。何度かゼロから自分自身でダイアグラムアプリを作成することも、ダイアグラムアプリの広いアイデアを把握する素晴らしい方法です。

https://gameprogrammingpatterns.com/state.html

What's our goal

私たちが作ろうとしているアプリで何ができるのでしょうか?以下にそのリストを示します。

  • ビューポートをパンする
  • shapeを選択および非選択にする
  • 新しいshapeを配置する
  • shapeを削除する
  • shapeを移動する
  • 線を描く
  • 線の頂点を移動する

もう少し気後れしてきましたか?これらの機能を実装することは、おそらくほとんどの人にとって不可能な挑戦ではないと思います。ただし、最初から考慮する必要がある重要なことは スケーラビリティ です。その理由は明らかですね?私達はこれらの機能に満足することはありません。変形、レイアウト、スナッピングなどの機能をさらに求めるでしょう。円、矢印、さらにはテーブルなど、特定の機能を備えたさらなる種類のshapeも欲しいかもしれません。

ところで、 スケーラビリティ とは何でしょうか?それは明らかに論争のある言葉ですね。この記事での意味を多少なりとも明確にしておきましょう。新しい機能を追加するときにアプリの複雑さが線形に増加するとき、そのアプリは優れたスケーラビリティを持っていると言えます。複雑さが何を意味し、どのように測定できるかについての質問があるかもしれません。直感を信じてください。

States

Default state

さて、とにかくアプリには既にキャンバスがあるとします。では、私たちは今何をしているのでしょうか?もちろん何もしていません。それが私たちのdefault stateです。このstateは、ここに戻る必要があるまでそのままにしておきましょう。

Pan state

では、最も原始的な機能から始めましょう:パンニングです。キャンバスに例としてshapeがあると仮定しましょう。次に何をしたいですか?もちろん、shapeを観察する自由を楽しみたいですね。私はBlenderの大ファンなので、中ボタンのドラッグによるパンニング推進者です。したがって、中ボタンを押した状態でpan stateをアクティブにしたいと思われます。

今のところかなりシンプルですね。

Select state

stateの名前をつけることは、私たち人間にとって最も難しい問題かもしれません。panまたはpanning、selectまたはselectedなど。もちろん、それは完全にあなたの好みに依存しますが、今回はできるだけ短い形を使用することで、タイポを減らすのに役立つだろうと考えています。

さて、何らかの方法でshapeを選択したいですね。どうやって?もちろん、左クリックです。正確には、shape上で左ボタンをダウンしてshapeを選択します。クリックイベントは利用可能ですが、私はこの種のキャンバスアプリでクリックイベントを直接処理するのはあまり好きではありません。何かの理由でダウン、アップ、クリックを調和して処理しようとすると、少し厄介です。

さて、select stateを取得し、それに移動しました。このstateはそれから何をするべきでしょうか?実際、これは大きなトピックです。このstateはほとんど何でも処理するか、少なくともdefault stateと同様の処理するべきです。これはintransient stateであります。

intransient stateとは一体何でしょうか?良い質問であり、この記事を書いているときに思いついたばかりなので、それには確定的な定義はありません。アイデアは単純です。stateがいくつかのイベントを処理して汎用操作をアクティブにする場合、そのstateは不変です。それでは汎用操作とは一体何でしょうか?それも確定的な定義はありませんが、例として「パンニング」、「shapeの選択と解除」、「shapeの移動」、「元に戻す/やり直し」などを挙げることができます。これらの例からアイデアが見えるといいですね。

pan stateはintransientでしょうか?いいえ、transientです。本当にそれを望むなら、いくつかの汎用操作をトリガーできるかもしれませんが、ほとんどの場合、ボタンを離すと前のstateに戻り、ただキャンバスをパンさせたいと期待しています。何かを行い、すぐに次のstateに移動する、それはtransientですね。それがアイデアです。

intransient stateは私たちにとってどれくらい重要でしょうか?かなり重要です。非常に、非常に重要です。なぜなら、それは汎用操作のために多くのイベントを処理しなければならないからです。正直なところ、どのようにして効果的にintransient stateを実装するかについては素晴らしい答えがありません。クラス指向の戦略は、これらのstateの大部分を統一するのに役立つかもしれませんが、基本的に私はJavaScriptの領域ではクラス嫌いです。ここでは実装の詳細については詳しく説明しませんが、これは別の大きなトピックになるでしょう。

実装の詳細は脇か将来かあるいは過去に避けておくとして、pan stateなどのいくつかのstateは、どんなintransient stateからでもアクティブになるべきです。そうすると、state図をもう少し一般的でシンプルにすることができます。

ここまで順調ですね。intransient stateのアイデアのおかげで、各stateからいくつかの汎用遷移を抽出することができます。それにより、state図が魅力的になります。

Put new shapes

残念ながら、ここからは事が複雑になっていきます。一方でそれはより実用的な操作に入っていることを意味しています。まず第一に、新しいshapeをキャンバスに配置したいと考えています。

新しいshapeを配置するプロセスをまず定義しましょう。アプリにshape配置用のボタンがあると仮定し、そのボタンからキャンバスへのドラッグ&ドロップ操作で新しいshapeを配置できるとします。

1つの問題が検出されました。アプリにボタンがあると言いましたが、それはボタン、つまりHTMLボタン要素です。したがって、私たちのstateはキャンバス要素の外で発生するすべてのイベントも処理する必要がありますか?部分的には、はい。stateが特定のカスタムイベントを処理することがある、これはその一例です。

要するに、左ボタンがshape上でダウンした場合に新shape作成用のstateをアクティブにしたいと考えています。この左ボタンダウンはキャンバスの外で発生するため、この機能のためにカスタムイベントを作成する方が良いでしょう。そうでないと、キャンバスの外の各UIに対して何百ものカスタムイベントを作成することになります。

今回は、特定のstateをアクティブにするためのパラメータを含む「activate state」イベントを作成します。一つの注意点は、特定のstateがアクティブになるかどうかは依然として現在のstate次第ということです。現在のstateがイベントを処理しない場合、何も起こりません。

このようなイベントは、たとえば以下のようになります。実装による部分なのであくまでも一例です。

{ type: "activate-state", data: { name: "put-shape", shapes: [..] } }

どのstateから「put shape」をアクティブにしたいですか?もちろん、答えはすでにあります。それはintransient stateです。新shape作成用ボタンは、私たちがintransient stateにいるときならいつでも使用できる状態になっているはずです。新shapeをキャンバスに配置した後、おそらく新しく作成されたshapeをさらなる操作のために選択したいと考えるでしょう。したがって、put shape stateはintransient stateであり、順次select stateに移動させましょう。

また、put shape stateを終了するときに新shapeを保存し、選択する必要があります。物事を単純にするために、私たちは主にstate間の遷移に焦点を当てています。

Delete shapes

この機能を実現するためには、既に十分なツールがあります。例えば、「Delete」キーの押下イベントによって選択されたshapeを削除したいとしましょう。リリースでもかまいません。

この操作を処理するのはselect stateであるべきでしょう。

select stateがshapeの選択状態を正しくチェックしている限り、私たちが行う必要があるのは、単に "Delete" キーの押下に対する処理を追加するだけです。その後、select stateはshapeの選択状態が変化するにつれて、stateの遷移を処理します。

Move shapes

shapeを配置するstateとほぼ同様ですが、アクティブ化トリガーは少し異なり、select stateから発生する必要があります。

順調に進んでいますね?では、少し複雑にしてみましょう。なぜなら、私たちのアプリはより便利であって欲しいからです。

想像してみてください。現在選択されていないshapeを移動したいとしましょう。これまでのところ、move stateをアクティブ化する唯一の方法は、事前にそのshapeを選択してから、そのshapeをドラッグし始めることでした。つまり、ボタンを2回押さなければならないということです。2回も押すのは面倒くさいので、選択状態に関係なくshapeをドラッグして移動させることができるようにしましょう。以下のようなstate遷移が欲しいと思われます。

待ってください、この遷移は既に存在しており、default stateはintransient stateの一つです。したがって、これらのトリガーは互いに競合しています。

単にmove stateに移動するだけでもいいかもしれませんが、おそらくいくつかの問題に直面することになるでしょう。通常、shapeを移動する際には、カーソルのスタイルやバウンディングボックスなど、特定の外観が期待されます。それらの特定の外観がshapeを選択するたびに一時的に表示されると、あまり気持ちがよくありません。したがって、shape上で左ボタンが押されたときに、select stateまたはmove stateのどちらかをアクティブにするための分岐を追加する必要があります。

とりあえず、最初にshape上で左ボタンを押します。次にドラッグすると、stateはmove stateに変わるべきです。ドラッグせずにボタンを離すと、stateはselect stateに変わるべきです。ボタンを押したままで次のアクションを待つためにフラグが必要そうです。

いや、私たちはstate patternを実践しているので、新しいstateを導入してフラグ代わりにすればいいのではないしょうか?それをleft down on shape stateと名付けましょう。長い名前です、好きなように名前を付けてください。

このフラグのためにフローチャートを描く必要はありません。専用のstateは単純な遷移を示しています。このstateの役割が少し特殊に感じられるとしても、このstateは他のstateと同様に、左ボタンアップとマウスの移動などのイベントを処理するだけで、特別なことは何もありません。したがって、この遷移に対する新しいアイデアはなく、単に新しいstateを追加してそのstateでいくつかのイベントを処理させただけです。

この例から私たちは素晴らしいインサイトを得ました。何かを処理するためにフラグが必要なら、そのフラグを既存のstateに導入せずに、新しいstateを追加することで解決できないか考えてみるべきです。フラグの数が少ない場合は何とか処理できると感じるかもしれません。しかし、フラグの数が時間とともに増えるにつれてそれは現実的ではなくなってきます。

Draw a line

私たちはダイアグラムツールを作成しているということを覚えていますか?私たちはshapeを作成し、選択し、移動し、削除することができるようになりました。では、次に直線を描いてみましょう。直線を描くためには、次の手順に従うことを想定しています。

  1. 直線を描く準備ができるようにボタンを押す
  2. 直線を描き始めるために左ボタンを押す
    • このステップで最初の頂点が配置されます
  3. マウスを移動して第二の頂点を移動する
  4. 直線の描画を終了するために左ボタンを離す
    • 新しく作成された直線を選択することが期待されます

見ての通り、これらの手順を達成するためには、2つの新しいstateが必要になりそうです。それらをline ready stateline draw stateと名付けましょう。put shape stateと同様に、最初のstateはボタンが利用可能なときにいつでもアクティブにできるはずです。つまり、そのstateは任意のintransient stateからアクティブにできるべきです。

全体の遷移は、この操作に関連する4つの異なるstateがあるため、大きく見えるかもしれませんが、実際には各遷移は非常にわかりやすいものです。基本的には片道の遷移であり、遷移のためのすべてのイベントは既に私たちにとって馴染み深いものです。

Move a vertex of a line

せっかくなら、頂点を移動したい気がしますね!問題が見つかりました。どのstateからmove vertex stateをアクティブにできるでしょうか?アクティベーションイベントは頂点上での左ボタンダウンで、それ自体はかなり簡単そうです。編集しようとしている直線は事前に選択されているべきだと仮定できます。直線はshapeの一種ですので、直線が選択されているときはselect stateにいるべきでしょう。

再び事態は複雑になりますが、始める際に頼りにすべきものはわかっています。そうです、私たちが直面している複雑さを処理するためのstateを作成します。

これはどうでしょうか?機能しますが、画像の中のintransientはすべてのintransient stateを表していることを念頭に置いておくべきです。select xx stateの数も増えるでしょう。特定のshapeタイプに対する具体的な操作を実装しようとするたびに新しいものが必要です。したがって、intransient stateとselect xx stateの関係は時間の経過とともに爆発的に増加する可能性があります。

提案できる方法の一つは、このような選択分岐を処理するためのハブstateを持つことです。

選択ハブstateの前にはany stateを置いています。これは私たちが実際に直面している状況です。選択ハブstateは、shapeの選択状態を変更できる任意のstateからアクティブにされ得ます。これまでに複数のshapeの選択について触れていなかったのは、事を単純にするためでしたが、複数選択はselect xx stateを処理する上で重要な要素となってきます。

良い機会ですので、ここでselect multiple stateを追加しましょう。直線と他の汎用shapeを選択したと仮定すると、今はselect multiple stateにいることになります。次に、汎用shapeを選択解除しました。それでは、どこにいるのでしょうか?間違いなくselect line stateにいるべきです。すでに選択ハブstateを持たないでこの状況を描くのはめんどくさいです。それでは、これが結果です。

any stateは文字通り任意のstateを表しているため、この画像は見た目ほど単純ではありません。ただし、それはあなたが想像しているほど複雑ではありません。選択ハブstateを除いて、他のすべてのstateは、とにかくshapeの選択が変更されるたびに選択ハブstateをアクティブにするだけです。選択の分岐をすべてハブstateに詰め込むことができれば、後はそのハブstateが選択遷移のすべての複雑さを処理してくれます。各分岐はそれほど複雑ではないでしょうが、一度それらがstate全体に広がるともう災難です。ですので、すべてをハブstateに詰め込んでしまいましょう。平和をとりもどしました。

私たちは素晴らしいツールを手に入れたので、最初の場所に戻りましょう。必要な残りstateはもう1つだけ、move vertex stateです。これはdraw line stateとほぼ同じですが、その状況は少し異なります。したがって、新しいstateを作成する方が、それらの微小な違いを1つのstateで処理するよりも簡単です。

Wrap up

ここまで読んでいただきありがとうございます。予想していたすべての機能をこれで網羅できました。この記事が、ダイアグラムツールのためのstate patternについてのインサイトとなることを願っています。これらはあなたの理想のツールを形作るための文字通りの第一歩であり、まだ話すべきトピックがたくさんあります。あなたはもしかしたらこの記事よりも優れた戦略をお持ちかもしれません。でかした、こっそり共有してくださいね!

Have a nice diagraming day 👋

Discussion