💊

カプセル化2024

2024/12/03に公開

こんにちは!株式会社PREVENT開発部のこまたつです。
この記事はPREVENTアドベントカレンダーの3日目の記事です。

昨今では生成AIの登場によって検索結果の質が低下していると嘆かれているようですが、実は生成AIの登場以前からインターネットには誤解を元にした記事が溢れていました。
というのも、プロジェクトやメンバーが変わればそこで通用するジャーゴンとして特定の語(たとえばカプセル化)が当てはめられることは多く、特定条件下でのみ正しい形で悪気なくそういった記事が作られていったからです。

今回は、意外と誤解している人が多いカプセル化についてAndroidチームの定例で使った資料を元に改めて共通認識を作って行きたいと思います。
執筆にあたって過去の情報や書籍をあたり正しいと思われる理解を元に執筆しておりますが、誤解があれば優しくご指摘ください。
コードや説明はAndroid開発で使われているものを基準としております。

カプセル化とは?

カプセル化とは関連するデータとロジックをひとつのモジュールにまとめることをいいます。
オブジェクト指向プログラミングにおけるパラダイムと思われがちですが、これらを提唱したのは別の人間で直接の関連性があったわけではありません。
情報隠蔽の重要性とともにモジュールの独立性を高める設計方針として紹介[1]されたものが、オブジェクト指向プログラミングの普及とともに「カプセル化」という名前で知られるようになったようです。

では早速Androidでよくある感じのコードと一緒に見ていきましょう。

よくある例

まずカプセル化のよくある誤解のひとつとして、POJO=カプセル化だと思ってしまうパターンがあります。
POJO自体にもさまざまな呼び名が付与されていて理解を難しくしている要因かと思いますが、コードにするとスッと理解できるかと思います。

data class Lifelog(var step: Long, var sleep: Long)

このようなデータをひとまとめに扱うための構造体をJava系の言語ではPOJOと呼びます。
Kotlinをはじめとする最近の言語ではプロパティアクセスにわざわざgetter/setterを書かなくて良くなっていますが、昔はもっと長ったらしく書いていた時代もありました。

これはデータをひとまとめにしていますがロジックがありません。
モジュール化はされていますが、これだけではカプセル化とは言えないでしょう。

より実際にありそうなカプセル化できていない例を次にあげます。

data class Lifelog(var step: Long, var sleep: Long)

object LifelogUtil {
  fun getStepWithUnit(step: Long): String {
    return NumberFormat.format(step)
  }
}

// どこかのFragmentなどで
binding.steps.text = LifelogUtil.getStepWithUnit(lifelog.step)

このコードではLifelogUtilに定義された処理を使って歩数に単位「歩」をつけたテキストとしてTextViewに挿入しています。
NumberFormat部分については本筋からそれるので適当に読み替えてください。
こういったコードはまあまあよくあると思います。
これを正しくカプセル化すると次のようになります。
先に行っておきますが、正しくカプセル化することが常に正解ではありません。

data class Lifelog(var step: Long, var sleep: Long) {
  fun getStepText(): String {
    return NumberFormat.format(step)
  }
}

// どこかのFragmentなどで
binding.steps.text = lifelog.getStepText()

このコードではデータとロジックがLifelogにまとまっていることがわかるかと思います。
プロパティのstepがデータで、getStepText()がロジックです。

で、それってなにが嬉しいの?

これらのコードでどんな問題が解決されるかというと、
カプセル化していない方のコードではLifelogUtil.getStepWithUnit()の可視性が過度に広く歩数ではない任意のLongを「歩」をつけるためだけに利用できてしまいます。
この例だとあまり危機感を感じてもらえないかもしれませんが、たとえばこれが文字列をLocalDateTime型にするような処理だと前提としている文字列のフォーマットが違うため例外がでるなどのリスクを想像できるかと思います。
これはインターネットでよく言われているUtilクラスに対するバッシングでも同様な主張があるかと思います。

またこのようにLifelogクラスにロジックが一体化することでコードのまとまりがよくなり、このコードベースに慣れていないプログラマーが参加した際にLifelogUtilの存在に気付かず同じ処理を書いてしまうなどの無駄がはぶけるようになります。

ここで「歩」をつけたい値が他のPOJOにもあった場合にDRY原則に反しませんか?という人がいるかもしれません。
DRY原則の誤解については2〜3年前から盛んに語られているかと思いますので詳細は省きますが、反さないというのが自分の考えです。
ですが、正しいカプセル化をするのが常に正解というわけではないのでお近くの信頼できる技術者にご相談ください。

最後に

現実世界にはいろんな意味でカプセル化という言葉を使う人がいると思います。
コミュニケーションで大事なのは言葉の正しさではなく相互理解ではありますが、これを気に正しいカプセル化が使えるようになると良いですね。
一病息災を支えるPREVENTをよろしくおねがいします!

脚注
  1. PARNAS, David Lorge. On the criteria to be used in decomposing systems into modules. Communications of the ACM, 1972, 15.12: 1053-1058. ↩︎

Discussion