クラスが循環参照しそうなときの回避策
はじめに
オブジェクト指向プログラミングでは、実装を進めて機能拡張していくほどにクラス間の関係が複雑になっていきます。
そのクラス間の関係において、特にクラスの循環参照はかなり危険です。
クラス間の関係のアンチパターン(?)と言うほどのものでもありませんが、循環参照は一方の変更によってもう一方が追従して変更し、その変更によって…といったように無限ループを引き起こしてしまったりと実行時エラーを引き起こしうるため、やはりリスクが高いものだと考えます。
ツリー構造とかだとあった方が便利なので、親が子を参照して子が親を参照する、といったことはありますので、一概に全部だめとは言い切れません。ただ、避けられるのであればやはり避けたいものです。
そこで、クラスが循環参照しそうなときの考える順番および回避策を自分なりに言語化・整理してみました。他の人にも役立つと幸いです。
回避策
①クラスの責務を見直して単一方向の参照だけにできないか考え直す
まず考えるのは、単一方向の参照に減らせないか?だと思います。
アプローチとしては、C#の場合は実行してほしいActionを渡したり、あるいはイベントにハンドラを登録するなどすれば、参照を切れるかと思います。
また、製品開発などきっちり進めている場合はほぼない状況かと思いますが、PoCの試作中など柔らかい状況だと仮実装だったりして使ってないクラスが残っていたりするかもしれません。コードの整理もかねて改めて本当に使っているのか、参照が必要なのかを見直すのも必要かもしれません。
②循環参照しそうなクラスの間にクラスを挟んでみる
次に考えるのは、循環参照しそうなクラスの間にクラスを挟んでみる、です。
例えば、いわゆるファサードパターンのように、循環参照しそうなクラスを所有、あるいは参照するクラスを一つ用意すれば循環参照を取り除くことができます。
または、イベントアグリゲータパターン(イベントを集約するサービスを用意して、そのサービスを介してイベントをやり取りする考え方)を用いて、あくまで依存するのはイベントを表すクラスに依存し、イベントの発行者・受信者を直接結び付けないようにするのもよいかと思います。
循環参照を切るために他のクラスを用意することが、割りと多いパターンな気がします。
補足
ChatGPTに聞いたところ、インターフェイスを導入する
といった方法もあるようです。確かに依存する対象はインターフェイスになりますが、結局プログラムを動かすときはそのインターフェイスを実現しているクラスをインスタンス化して動かします。そのインスタンス化したオブジェクトが実体として参照しあっているのであれば、実質循環参照になってしまうのでは?と思いました。
例えばParentクラスがIParentインターフェイスを実現し、ChildクラスがIChildインターフェイスを実現していて、ParentはIChildを参照、ChildクラスがIParentを参照している場合などです。
また、元々のあるべき姿を考えると、インターフェイスはあくまで振る舞いを表すものですので、参照を切るために無理に使うものではないとも考えます(書きながら、「②循環参照しそうなクラスの間にクラスを挟んでみる」と相反しているようにも思いましたが、そこの言語化はまだできていないです。。。)。
おわりに
今回は、オブジェクト指向プログラミングでクラスが循環参照しそうなときの回避策について考えてみました。
クラス間の関連をうまく整理するというのは中々難しい気がしますが、設計力につながるポイントのようにも思います。
日々鍛錬していきたいですね。