ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

ソフトウェアの設計で考えていること

世の中にはいろんな設計理論がある

SOLID原則だとか、クリーンアーキテクチャーだとか、ドメイン駆動開発だとか。どれも別に間違っているとは思わないしすごく大事なことだと思いますが、何か、それが真理と言われるとそうかな?となる。

分野に限らず、世の中のあらゆる理論について、「真理は存在するが、人間の理論ではその全てを記述できない」みたいな印象があります。もう少し違う言い方をするなら、「人間は真理の抽象的側面を切り出してその側面については理解できる」と言ってもよいです。

自分が設計の段階で考えていること

ソフトウェアというのは、ゲームも含めて、「機能」と「機能へのアクセス」で成り立っていると思います。「機能へのアクセス」は「インターフェース」と言い換えて差し支えないのですが、言語機能としてのインターフェースとごっちゃになる懸念があるので、そう表現しています。

なので、私が何か設計や実装を行う場合には、明確にこの「機能」と「機能へのアクセス」を分けているように思います。「機能」とは「外部データと純粋関数」の可能性もありますし、「状態を内包したオブジェクト」の可能性もあります。以前、関数型とかオブジェクト指向とかどうでもいいと書いたことがありましたが、この辺りがそう言った根拠になります。

この「機能」と「アクセス」の2種類に分ける考え方は別に、最初に挙げたSOLIDやクリーンアーキテクチャーやドメイン駆動開発とぶつかるものではなく、言い換えでしかないとは思っています。

SOLID原則と、「機能」「アクセス」分割

SOLID原則っていうか単一責任原則ですが。「このソフトウェアはこのような機能を持っています」と表現できる単位でモジュールやクラスの境界があると嬉しい。単一責任ってなんだよとよく言われますが、このソフトウェアとしての「意味」の分割というのが個人的には一番スッキリします。もし新しい「意味」が見出されたならそのタイミングでさらに分割すべきとなるわけで。逆に「意味」が失われるほどに分割することは間違いとなります。ソフトウェアの中でこの塊はどういう機能的意味を持っているの?というのが説明できる単位が「単一責任」ではないか、という解釈です。

クリーンアーキテクチャーと、「機能」「アクセス」分割

機能を内側に置き、その機能をどう使うかのアクセスを外に置くことで、依存方向の一方向化を達成するという点で同じでしょう。ユースケースだとか、そう言った具体的な話はクリーンアーキテクチャーの本質ではないと思うので無視します。

ドメイン駆動開発と、「機能」「アクセス」分割

サービスの根幹をドメインと呼ぶことがあるようですが、そんな限定的な意味を持たせることにはあまり意味を感じません。ドメインを「機能」と単純に言い換えれば同じことを言っていると考えています。

言い換えて何が嬉しいのか

人間は真理をそのまま記述することはできませんが、真理の抽象的側面は記述できます。様々な記述の中で、上手くハマる記述が見つけられれば十分だと思いますので、「言い換えてみる」ことには価値があると思います。

冪等性キーの正体

冪等性

ゲームクライアントから、ゲームサーバーに対して、アイテムの消費をリクエストしたとします。サーバーでは問題無く処理されましたが、何らかの理由で、サーバーからのレスポンスがクライアントに届かず、クライアントは通信が届かなかったかなと思い、もう1回リクエストを送ります。このとき、クライアントは1回だけアイテムを消費したつもりですが、サーバーはリクエストを2回受け取ります。なので、何とかしてサーバーは、2回目のリクエストを無視しなければなりません。このとき用いられるのが冪等性キーというやつです。

 

冪等性とは「複数回作用しても1回作用したのと同じになる性質」で、不安定なネットワークではこのような重複処理の可能性はよくあるため、リクエストにキーを割り振って同一性を確認する手法が取られます。これが冪等性キーです。

 

冪等性キーの正体

これってつまり、アプリケーションの層は処理の同一性を知ってるけど、トランザクションの層は知らないから、外部から教えてあげようってだけの話ですよね。よくフレームワークを作ると出てくる、ナントカContextという名前の構造体を引数に受け取るのと全く同じです。その層が持ちうる知識は何?というところと、アプリケーションの要求が分かっていれば、その差分をコンテキストとして渡して埋めればよいわけです。

 

冪等性キーは、層の間にある情報の非対称性を埋めるコンテキスト

正確にはその一種で。キャッシュ判断のETagとか、バージョン番号とか、分散DBにおける因果一貫性のためのベクトルタイムスタンプとかと本質的に同じものとして解釈できるなぁという話とともに、このように捉えたら、何を冪等性キーとして扱うか、という風なアイデアも簡単に出るのではないか。そんなことを考えました。(ユニークなキーを割り振ってもいいし、状態から計算されるハッシュでもいいかもしれない。要するにどんな知識が欲しいのか、と意味的に一致すれば良い。)

Grafanaでメトリクスを取得するのが楽でいい感じ

Grafana

多分、Graphから来てるネーミングで、DBの内容とかを可視化するのに便利なツールです。

これはClaude Codeに頼んでいい感じのサンプルデータを登録してもらったやつなので、特に意味は無いです。

サーバエンジニアがサーバの状態を監視する際の可視化ツールとしては非常によく使われているようなのですが、恥ずかしながら自分は全く知りませんでした。会社のサーバーエンジニアがこういうモニタリングをよくやっていて、カッコいいなーと思っていました。クライアントもメトリクスの収集は意義があるので、気軽に使えるものがあればいいなと思って調べている中で知りました。

Grafanaをクライアントから使うときの構成

graph LR
    subgraph グループ1[サーバー]
        A[Grafana] --取得--> B[InfluxDB]
    end
    C[ゲームクライアント]
    C --httpで計測値を送信--> B

Grafanaは飽くまでもビューでしかないので、そこに表示するための本体は別に必要です。それがInfluxDBというやつで、これはデータストレージとしての機能と、ここにデータを登録するためのHTTPサーバーを兼ね備えているようです。

なので、ゲームクライアント側で毎フレームのメモリ使用量やdelta_timeを計測、記録して、InfluxDBのエンドポイントに送信。あとはGrafana側でInfluxDBに対してクエリを叩く。それだけ。

有名な構成のようなので、docker-compose.ymlが調べれば出てきます。

Grafana Cloud

Grafana CloudはGrafanaと、バックエンドのDBなど実用上必要なものを一括で提供してくれるサービスです。ある程度の無料枠の使用量を超えると従量課金になりますが、無料枠で使える範囲であれば、自分でサーバーを用意する必要も無いし、めちゃくちゃ簡単に試せました。

メトリクスの意義

サーバーもそうですが、ゲームのクライアントも、扱うリソースが増えると同じ処理でもどんどん処理が重くなっていくということが起こります。なので、とりあえずメトリクスを収集するようにしておいて、一定値を超えたら警告する、みたいな仕組みを開発早期に構築しておいて、テストプレイのデータを無差別に収集しておくのは結構役立つと思います。

オブジェクト指向だとか関数型だとかの論争について

個人の立場としては、どっちでもいい

設計について色々書いていますが、正直オブジェクト指向だの関数型だのというのはどうでもいいなと思っておりまして……

というのも、究極的にソフトウェアにおいて必要なものって、

  1. その世界を決定付けるために必要十分な状態
  2. 1の状態を更新する方法
  3. 1の状態を使ってユーザーに見せる方法

この3つです。こう書くといかにも関数型を好んでいそうなんですけど、状態そのものと更新手段をまとめてしまうとわかりやすいケースも当然多いだろという立場でして。つまり1と2をまとめるのがオブジェクトなわけですけど、わかりやすいですよね。「敵」を関数型で表現するやつがどこにいるんだと思ってしまいます。ゲーム開発の場合は特にそう感じるんじゃないかなと思います。なんというか、素直にそのアプリケーションを見たときに、誰もがイメージする構造そのままに開発できるのが、設計の面では理想だと思っていまして。勿論現実的にはそんな上手く行かないことも多いんですけど。

アプリケーション全体を見れば、オブジェクト指向が素直だなぁと感じるところも、関数型が素直だなぁと感じるところも両方あるというのが普通ではないでしょうか。そんな細部にこだわって時間を浪費する意味があまり無い。その前に、「その世界を決定付けるために必要十分な状態」という、つまりデータの部分をどのように解釈し、分割しておくかというところに注力した方がよいと思います。

ECSについて

ゲームの場合、処理そのものは同じで扱うデータだけが異なるというのが頻出しますので、それらデータがメモリ上で近いところに存在していると嬉しいケースがあります。そういうとき、オブジェクト指向だと、一つ一つにデータに対して処理を別個に呼ばなければならなかったり、ちょっと都合が悪いんですよね。CPUの例で言われることが多いですが、GPUはもっと日常的にメモリレイアウトを意識しています。

そういう場合、「大量のオブジェクト」という単位でまとめて扱っちゃえ、という発想のECSと呼ばれるアーキテクチャが選ばれることが多いです。とはいえ、細かく多種多様なオブジェクトがある場合には恩恵がありませんし、ややこしくなってバグりやすくなるだけです。銀の弾丸は無いですね。

とはいえ、ECSに限らずメモリの状態を意識するのはゲームプログラマだったら当然ではあったりします。

読書感想文「エンジニアリング組織論への招待」

課題図書

↑ なんか綺麗に書影出そうと思ったらアフィリエイトの機能使うしかなかったので使ってます。 ご自身で検索することをお勧めします。

感想

すっっっごいいい本でした。エンジニアリング、とありますが、エンジニアだけに向けた本ではないですね。少なくとも技術詳細の本ではないので。

ITエンジニアにはもちろん刺さるでしょうが、経営者、マネージャー、その他セクション、組織に属する人は誰もが読者層だと思います。

ITエンジニアに分類できる自分は仕事の上で「情報を積極的に開示して信用を得るべきだ、逆に企画は今後の予定や意図まで含めて共有るすべきだ」ということをよく言ってきたんですが、そのような主張がこの本でもなされていて、自信が持てました。正直、開発において発生するほとんどの問題はコミュニケーション不足に端を発していると思うことが多いです。

アジャイル開発について

スクラムマスターの資格を持った方がマネジメントに就いて、毎週のベロシティを見て開発していくアジャイル開発を経験したことがあるのですが、そのお世話になったマネージャーの方は僕が思っていたよりもよほど凄いことをしていたんだなとわかりました。

チームビルディングについて

自分も、ある程度技術力がついてきてからは、チームのアウトプットを上げるべきという意識が付いてきました。みんなの知見を集めて、個人でできるよりも良い結果を得よう、というのは意識していて、設計レビューのようなことをやってみたりしています。 しかし何を考えてやるべきか、どのような方法が良いかというのは手探りで、この本の考え方は非常に参考になりました。

チームみんな読んでほしいレベル

ただ、個人が思うだけではやはりダメで、チーム全体が同じ意識を持つべきなんですよね。なので、こういう書籍こそ読書会開くとかで共有する価値が高いんだろうなと思います。

null安全の正しさと誤り

null安全

null安全とは、nullを許容しないコンパイルルールのことを指します。 null参照は世の中のソフトウェアバグの中でも最多と言われていて、null参照を許すからダメなんだ、という思想ですね。言いたいことはわかります。

バグの原因はnull参照だ、というのは本当に正しいか

ある意味では正しいのですが、自身の経験を振り返ってよく考えてみると、null参照で発生していたバグは2つのケースに分けられると思います。

  1. 正常系でもnullが想定されてたけど、それを考慮せずにnull参照してしまったケース。例えば、RPGのパーティ編成で、空欄をnullで表現していたりするとこういうことが起こりそうですね。
  2. 異常系でnullが入ってしまって、null参照してしまったケース。例えば、File.Readのような関数を使った場合、ファイルが見当たらなければnullを返す、というのは自然でしょう。もちろん例外をスローする場合もあるでしょうが。

1. 正常系でもnullが想定されてたけど、それを考慮せずにnull参照してしまったケース

これはnull安全にすべきです。そりゃnullでもちゃんとチェックすれば動くでしょうが、nullを特別扱いしなければならない編成画面や戦闘中処理がかわいそうです。パーティメンバーをPartyMemberクラスで表現していたとすれば、空欄を意味するPartyMemberインスタンスを用意し、空欄の可能性を様々なAPIで伝える努力をするべきです。

2. 異常系でnullが入ってしまって、null参照してしまったケース

一方で、こちらはどうでしょうか。こちらのnull参照回避として

var (result, err) = file.Read("path");

こういう、go言語でよくみるような、ファイルが読めなかったらエラー変数に何か入れるというルールもあるでしょうし、関数型の言語ならResultやOptionといった、値を持つかどうか、処理が成功したかどうかを内包する構造体を使うこともあるでしょう。

ですが、これって実行時の処理という観点で言うと、nullチェックと変わらないんですよね。もちろんエラーの内容について型で判定できるとか、コンパイル時に判断できるとかの利点はありますけど、結局のところ、異常系の場合はこうする〜という処理は続けなくてはならない。nullチェックと実質同じです。nullの場合、というのに文脈上の意味を持たせているにすぎません。

null参照そのものが危険なのではなく、nullが意味を持ってアプリケーションレイヤーに表出するのが危険

2のケースは、例外が起こるパターンを考慮できていればそれで十分ですし、考慮できていなければダメなので、null参照がどうとかは正直関係がありません。

しかし、1のケースは、nullに「パーティメンバーが空欄である」という意味があると、チームで開発していたならチームメンバーもそれを知っている必要があります。ここに、不要な知識の要求が生まれるわけです。しかもコード上でコメントとか書かないと残らないような知識です。

問題の本質はここで、本来、アプリケーションの仕様上、nullが適切な場面というのは存在しません。「空欄」とか「何も持っていない」というのは、それをユーザーに見せなきゃいけない時点で、「『何もない』という情報を表現する実体」が必要になります。

DBの論理削除が批判されるのと同じだと思っています。論理削除の何が問題かというと、人が論理削除を選択するとき、本当にやりたいのは削除ではないんですよね。なんらかの処理から無視したいという要求です。ただ状態が変わっただけと言えます。であれば、その状態を素直にデータとして表現してやればいいわけです。

ドメインを正しく理解するべきでしょう。

コードエディタのサポートを受けやすいAPIを作ろう

Unity RenderGraphの惜しい点

RenderGraphでは以下のようなAPIで、パイプラインで使用されている各種データにアクセスすることができます。(厳密には、アクセスを宣言する)

var cameraData = contextContainer.Get<UniversalCameraData>();
var resourceData = contextContainer.Get<UniversalResourceData>();

有向非巡回グラフの特性を使って、依存を明示することで並列性の最適化をする、みたいなのはプログラムのコンパイラがやっていたり、DBでもそういうことやってますよね。なので思想自体は本当に正しいのですが……

Genericsの罪

なぜ基本的なリソース取得をGenericsにしてしまったのか。C#を書いているとき最も嬉しいのが、.を打つとその先の候補が並ぶことです。Genericsを使うと、その恩恵が失われるのですよね。

こういうの書いといた方がいい

public static class RenderGraphExtensions
{
    /// <summary>カメラデータを取得</summary>
    public static UniversalCameraData GetUniversalCameraData(this ContextContainer contextContainer)
    {
        return contextContainer.Get<UniversalCameraData>();
    }
}

あのAPI名前なんだったっけ……となるなら、エディタのサポートを受けられるように用意しておきましょう。 最近はAIのコードサポートがあるので、適切な変数名を付けておけばいい感じに補完してくれるかもしれませんが。