Java で利用していた カプセル化 を中心とした OOPの手法が Goでは役に立ちづらい理由を解説していきたい。
言語特性
可視性の制御
カプセル化における大前提は 可視性の制御であり、またその境界である。Javaの場合その境界はクラスである。都合の良いことに、Javaではフィールド・メソッドの可視性を public
protected
package private
private
で修飾しクラス単位の コントロールが可能である。
一方の Goでは クラスに類似した構造体 こそ用意されているものの、可視性の制御は 公開
非公開
の2種であり単位は パッケージ である。シンプルで素敵な仕様だが、致命的なのはカプセル化に必要な境界がパッケージ単位と緩いことだ。同一パッケージにおける構造体同士では、お隣様の内臓が丸見えな上に操作可能である。これではカプセル化にならない。
無理して構造体を全て別パッケージに定義すればJava相当の環境を実現できないこともないが、Goのパッケージはコードのディレクトリ階層と一致させるのがベストプラクティスである。こんな設計を採用するとコードがディレクトリ地獄になる。
main.go
+--model/
+-- user/
| +-- user.go
+-- store/
| +-- store.go
エラーハンドリング
Java では エラーを try
cache
を始めとした例外処理で扱うが、Go では エラーを error
型の値として扱う。Go にも一応 panic
と recover
いう例外処理に似た仕組みは用意されているが、これは主に回復不可能なエラーをハンドリングするための機構である。
ある時期から Javaでは非チェック例外を、カプセル化されたオブジェクトの Setterにおける値チェック違反というようなカジュアルな局面で使い始め、この用法は非常に効果的だったが、この仕組みを Goの panic
で再現させるのは適さない。 アプリの終了判定という思い判定を本来の目的とする panic
を軽いチェックであるバリデーションで流用するのは大袈裟過ぎるし、そもそもGoの設計コンセプトと矛盾する。従来の error
の形を適用するのが正解である。
しかし error
を使えば済む話ではない。error
を活用する場合、Javaでは以下で済ませた内容が…
user.setName("Gopher");
Goでは以下のように膨張する。
err := user.SetName("Gopher")
if err != nil {
return fmt.Errorf("fail in setting user name: %v", err)
}
無理に Setterを活用しなければ下記で済んでいたものがである。
user.Name = "Gopher"
つまり、Javaと Goではエラーハンドリングの仕組みが決定的に違うため、Goではデメリットの方が目立つ。更に Javaでは関連する処理(ここでは適切な入力チェック)をカプセル化したオブジェクト内に集約させることでオブジェクト側の処理が増える一方で利用側の処理は不要になりコードの一覧性が上がる。一方の Goは 常に error
処理がつきまとうため、Setter内部のバリデーションを隠蔽しきれず、コードの一覧性は微妙になる。例えば具体的なコードに落としてみる。
Javaだと以下のようにスッキリした印象だが
var user = new User(name, age)
store.sellingAlcholEnabled(user.isAdult())
Goでは以下の様に雑多になる
user, err := NewUser(name, age)
if err != nil {
return fmt.Errorf("fail in creating user: %v", err)
}
if err := store.SellingAlcholEnabled(user.IsAdult()); err != nil {
return fmt.Errorf("fail in setting alcholeEnabled: %v", err)
}
Java では一度投資すれば(透過的なバリデーション処理が増える)メリットしか残らないが、Goでは投資してメリットが発生するのと同時に(エラーハンドリング増加による可読性の低下という)デメリットが発生するのだ。これが何故発生するのかと言えば、言語的な考え方の差である。
言語的背景
言語 | 開発環境 | 平均的な開発者のスキル | 防御的コードの必要性 | 防御的コードと言語の親和性 |
---|---|---|---|---|
Java | 大規模な企業システム全般 | ジュニア・ミドル | 高 | 高 |
Go | Webサービスのバックエンド | ミドル・シニア | 低 | 低 |
大規模な人員を投入する業務用システムの開発で利用される局面の多い Javaではジュニアな開発者の占める割合も多い。特に経験の少ないジュニアな開発者は勘違いや手違いを起こしやすい。これを防ぐ投資的行動として型やバリデーションの縛りを入れる訳だ。つまり Javaの防御的なコードは優しい拘束衣である。また Javaはこの拘束衣を作りやすい言語であり、大変効果的である。
一方で、高速なWebサービスのバックエンドで利用される局面の多い Goでは経験豊富なエンジニアが多い。そこでの主たる課題はケアレスミスではなく、多様すぎるバックグラウンドにおける宗教論争や、Go言語自体は初めてという言語限定での学習時間であり、これらを乗り越えていかに素早く生産性を発揮できるかだ。
Goはこの様な局面を意識してシンプルさや効率性をより重視した言語である。つまり拘束衣の要らない人員が多い上に拘束衣を作りにくい言語特性まで持っている。この辺りを考慮するとデメリットの方が上回りつつある。
結論
今回は OOPのカプセル化の観点を Goに埋め込むことを検討したが、結論としては Goでのカプセル化はメリットが低下する上にデメリットが露呈するため、お勧めできない。無理に適用して「Go言語でのカプセル化が成功した」と主張するのは簡単だが、それは多くの他の価値を犠牲にしたうえで成り立ったことである。例えるなら「ノートPCの性能を上げてくれ」と頼んだら確かに性能は上がったものの重量が 2.5kgプラスされて外部に追加パーツと配線がはみ出ている様なものだ。嬉しいのか悲しいのかは複雑なところである。
ノートPCには性能や価格を犠牲にしたポータビリティという個性があるように、Go言語にも防御的コードをOOPとして入れ込む適性を犠牲にしたシンプルさや生産性という個性がある。言語には言語の個性があるので、その個性が生きるような拡張を盛り込むべきだろう。
以上、Javaを経験した上で Goを活用している人であれば誰でも肌で感じたことを敢えて言語化してみた。何かの参考にしてみて頂けると幸いである。