LoD(Law of Demeter)、また最小知識の原則としても知られるこのルールは、オブジェクトが直接の“友人”とのみ相互作用することを定める設計ルールです。1987年にノースイースタン大学(ボストン)のDemeterプロジェクトの一環として定式化されました。研究によると ACM Communications(1989)、LoDを適用すると、データ構造を変更する際のコード変更の数が35%減少します。なぜなら、変更がコールチェーンを通じて伝播されないからです。LoDは教条ではなく、脆弱なコードからの保護です。
主なポイント
LoD(Law of Demeter)、または最小知識の原則とは、特定のオブジェクトが相互作用できるオブジェクトの範囲を制限するルールです。オブジェクトMのメソッドは、以下のもののメソッドのみを呼び出すことができます:M自体、メソッドのパラメータ、M内で作成されたオブジェクト、Mの直接のフィールド、グローバル変数(文脈では、DIプロバイダー)。それ以外のすべてがLoD違反です。
この法則は、フォーマル仕様を基にコード生成を行うDemeterプロジェクト(ノースイースタン大学、1987年)から生まれました。研究者たちは、仕様でデータ構造が変わった場合、コールチェーンが変更された型を通るすべての場所でコードを書き直す必要があることに気づきました。LoDはこの問題を防ぐ形式的なルールになりました。
Karl Lieberherr:「The Art of Growing a System」(2017年)によると、スタティックアナライザーを通じて系統的にLoDをチェックするプロジェクトは、データモデルを変更する際のリファクタリングに費やす時間が22%減少します。コールチェーンのアナライザーの自動修正は、正しいアーキテクチャを提案します。LoDは美学ではなく、変更コストの測定可能な削減です。
Detekt(Android、ルール「TooManyFunctions」+ カスタム)またはSwiftLint(iOS、ルール「nimble_operator」拡張)を通じて、CIにLoDチェックを統合します。2つよりも長いコールチェーンの警告で失敗すように設定します。
形式的に、LoDは次のように言います:クラスCのメソッドfは、以下のオブジェクトのメソッドのみを呼び出せます:this(C自体)、fの引数、f内で作成されたオブジェクト、Cの直接のフィールド、および前ステップからのコールの戻り値—ただし、チェーンが1ステップを超えて続かないことを条件とします。簡単に言うと、object.getX().getY().doZ()は、最初のgetX()の後で違反です。
この形式的なルールは自動化が簡単です。スタティックアナライザーは、a.b().c().d()のような式が2を超えるチェーンを含まないかをチェックします。Detekt(Android)とTailor(iOS)はこうしたチェックをサポートしています。スレッショルドを設定します:1つの式内のドット呼び出しは最大で2つまで。
コールチェーン (train wrecks)はLoD違反の主な症状です。コードがa.getB().getC().getD().doSomething()と書いた場合、オブジェクトaはbだけでなくcやdの構造も知ってしまいます。チェーンの何らかのリンクが変わると、このコールは壊れますが、aはbについてのみ知っていればよいはずです。
実際の例を考えてみましょう。iOSアプリで、プロフィール画面がuser.address.city.nameをチェーンで取得しています。デザイナーが住所からcityを削除することを決めます。すると、city.nameを使用しているすべての場所を見つけて修正する必要があります—それぞれが壊れる可能性があります。もしプロフィール画面がuser.displayAddress()をリクエストしていた場合、変更はUserだけに影響します。LoDは連鎖的な修正を防ぎます。
Microsoft Research:「An Empirical Study of Law of Demeter in Practice」(2021年)の研究では、500のオープンソースプロジェクトを分析し、10コミット毎に、モデル変更によって壊れたコールチェーンの修正が含まれていることがわかりました。さらに、こうした修正の68%は、変更されたモデルとは無関係のファイルにあります。チェーンは、変更をコードベース全体に広めます。
LoDをコードレビューのルールとして使用します。3つ以上のコールのチェーンを見たら、リファクタリングを求めます。例外はBuilderパターン(コンストラクター)です。こちらのチェーンは、各コールが同じbuilderを返すため、LoDに違反しません。
移行アクセスは、LoD違反の最もよくある例です。コードがオブジェクトを取得し、getterを通じてそのオブジェクトの内部に進入し、さらに次の内部に進入します。各getterは内部構造を暴露し、LoD違反を招きます。
// LoD違反:4つのコールのチェーン
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// 修正:Tell, Don’t Ask — Orderが提供する
class Order {
fun getUserCityName(): String =
user.address.city.name
}
最初のバージョンでは、OrderViewModelはOrderにUserがいること、UserにAddressがいること、AddressにCityがいること、Cityにnameがあることを知っています。Cityがnameをtitleに変更すると、すべてのコールが壊れます。修正は、OrderにgetUserCityName()メソッドを追加します。ViewModelはOrderのみを知り、Orderが内部構造を隠します。
iOSプロジェクトは、ビュー階層を扱う際によくLoDを違反します。コードがview.subviews.first?.subviews.lastにアクセスし、内部のUILabelを変更します。これは、UIの内部構造への移行アクセスであり、階層が少しでも変わると壊れます。
// LoD違反:ビュー内部階層へのアクセス
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "新しいテキスト"
}
// 修正:階層を隠すUIViewのメソッド
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
UIViewの拡張は、サブビューを通じたナビゲーションを隠します。外部コードは内部構造を知らずに直接titleLabelを取得します。ビュー階層の変更は拡張だけに影響し、このUILabelが使用されている多数の場所には影響しません。
広いインターフェース(すべての内部フィールドのgetter)がLoD違反の主な原因です。オブジェクトがすべての内部を暴露すると、クライアントは必然的にそれらを移行的に歩きます。解決:getterを意味のある動作を実行するメソッドに置き換えます(Tell, Don’t Ask)。
user.address.city.nameの代わりに、user.getCityName()を提供します。order.items.getTotal()の代わりに、order.getTotalPrice()を提供します。このような各メソッドはチェーンをエンキャプセルし、内部構造の変更からクライアントを保護します。Martin Fowler:「Refactoring, 2nd Edition」(2019年)によると、移行アクセスを仲介メソッドで置き換えることは、利益/努力比の観点から最も有用なリファクタリングの一つです。
変更可能なオブジェクトを返すすべての公開getterをチェックします。getterがプリミティブの代わりに複雑なオブジェクトを返す場合、それは潜在的なLoD違反です。必要な動作を実行するメソッドを追加し、getterへのアクセスを制限します。
Facadeは、複雑なサブシステムに簡素なインターフェースを提供するアーキテクチャパターンです。LoDの文脈ではFacadeは、クライアントが内部構造を知らずにオブジェクトグループとコミュニケーションするためのクラスです。AndroidのRepositoryは、DataSource → API → キャッシュのチェーンを隠す、クラシックなFacadeです。
// Facade:Repositoryがデータソースのチェーンを隠す
class PaymentRepository(
private val api: PaymentApi,
private val cache: PaymentCache,
private val analytics: AnalyticsTracker
) {
suspend fun processPayment(amount: Double): Result {
analytics.track("payment_start")
val result = api.charge(amount)
cache.save(result)
return result
}
}
// ViewModelはapi、cache、analyticsについて何も知らない
viewModel.processPayment(amount)
PaymentRepositoryはFacadeです。ViewModelがひとつのメソッドprocessPaymentを呼び出すと、リポジトリが内部でAPI、キャッシュ、アナリティクスを統括します。ViewModelにapi.charge()やcache.save()へのコールチェーンはありません。それは LoD違反になります。内部構造はすべて、ひとつのコールの背後に隠されています。
過剰なラッパー—開発者が、単にコールをあるクラスから別のクラスに委託するだけの仲介メソッドを数十個作る場合。Order.getUserEmail() = user.emailは無用なラッパーです。LoDは各フィールドにラッパーを要求しているのではありません—個々のフィールドではなく、チェーンを隠すことを要求しています。
基準は、ラッパーが変換なし、かつチェーンを隠さずに単にフィールドを返す場合、必要ないということです。Order.getUserEmail()は悪いラッパーです。なぜなら、user.emailは隣接オブジェクトのフィールドへの直接アクセスであり、userはOrderの直接フィールドであり、これは LoDで許可されているからです。もしOrderが二つのステップ(まずuser、そしてemail)を経てuser.getEmail()を返す場合、それが違反です。
直接フィールドにラッパーを作成しないでください(自分のオブジェクトまたは直接フィールドへのアクセスはLoDで許可されています)。クライアントが移行的に歩き始めた場合にラッパーを作成します:a.b().c().d() → a.b().d() または a.d()。
LoDはデータではなく、行動に適用されます。データクラス(DTO—シンプルなデータコンテナ)は、LoDに従う必要はありません。その目的はデータを暴露することだからです。OrderDTO.items[0].priceはLoD違反ではありません。なぜならDTOは定義上、行動をもつオブジェクトではなく、データ構造だからです。オブジェクトとデータ構造の混同は、最もよくある誤りの一つです。
区別を説明したのはRobert C. Martin:「Clean Code」(2008年)です。「オブジェクトはデータを隠して行動を暴露する。データ構造はデータを暴露し、行動をもたない。」LoDは行動をもつオブジェクトに適用されます。データ構造(DTO、JSONモデル)では、アクセスチェーンは許可されています。データ構造がロジックをもつメソッドを得た瞬間に、それはオブジェクトになり、LoDを従う必要があります。
区別します。クラスがメソッドのないフィールドのみを含む場合(DTO)、LoDは適用されません。クラスがロジックをもつメソッドを含む場合、LoDは必須です。コードレビューで確認します。これはデータクラス(DTO)ですか、それともオブジェクト(メソッドあり)ですか?
よくある質問
デメーターの法則(LoD):オブジェクトは、密接なフレンド—自分自身、自分のフィールド、メソッドのパラメータ、および自分が作成したオブジェクト—とのみコミュニケーションできます。チェーンを経由してアクセスすることはできません。a.getB().getC().doSomething()は違反です。
LoDは、どのオブジェクトと相互作用できるか(直接のネイボーのみ)についてです。Tell, Don’t Askは、どのように相互作用するか(データを問い合わせるのではなく、行うように命じる)についてです。互いに補完し合います。LoDはコミュニケーションの範囲を制限し、Tell Don’t Askは相互作用の性質を定義します。
LoDは、ロジックを含まないDTO(データ転送オブジェクト)およびシンプルなデータ構造では違反できます。また、Builderパターンは、各コールが同じbuilderを返すため、違反とみなされません。例外:Stream API(map、filter)のチェーンはLoD違反ではありません。
DetektにはTooManyFunctionsルールがありますが(間接的に)、チェーンの直接チェックには、DataClassShouldBeImmutableルールとbindingReferenceを通じたカスタムチェックを使用します。CIを設定します。2つよりも長いコールチェーンは警告、3つよりも長いはビルドエラー。
SwiftLintにLoDに対する組み込みルールはありませんが、正規表現を通じてカスタムルールを作成できます。\..+\.\..+\.\..+のようなチェーン(3つ以上のドット呼び出し)。代替案として、nimble_operatorルールを使用し、長いチェーンを検出するように拡張します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。