デバッグの容易性
あなたのライブラリのユーザーは、その機能の上に構築を行い、彼らが構築する機能には特定して解決する必要があるエラーが含まれるでしょう。このエラー解決プロセスは、開発中にデバッガー内で行われることもあれば、本番環境でロギングおよび可観測性ツールを使用することもあります。あなたのライブラリは、デバッグを容易にするために以下のベストプラクティスに従うことができます。
状態を持つ型にtoStringメソッドを提供する
状態を持つすべての型について、意味のある toString
実装を提供してください。この実装は、たとえ内部的な型であっても、インスタンスの現在の内容を分かりやすく表現するものを返すようにすべきです。
型の toString
表現はしばしばログに書き込まれるため、このメソッドを実装する際にはセキュリティを考慮し、機密性の高いユーザーデータを返さないようにしてください。
状態を記述するために使用される形式は、ライブラリ内の異なる型間で可能な限り一貫性があるようにしてください。この形式は、APIによって実装されるコントラクトの一部である場合、明示的に記述され、十分に文書化されるべきです。toString
メソッドからの出力は、例えば自動テストスイートで、パースをサポートする可能性があります。
例えば、サービス購読をサポートするライブラリの以下の型を考えてみましょう。
enum class SubscriptionResultReason {
Success, InsufficientFunds, IncompatibleAccount
}
class SubscriptionResult(
val result: Boolean,
val reason: SubscriptionResultReason,
val description: String
)
toString
メソッドがない場合、SubscriptionResult
インスタンスをそのまま表示しても、あまり有用ではありません。
fun main() {
val result = SubscriptionResult(
false,
IncompatibleAccount,
"Users account does not support this type of subscription"
)
//prints 'org.example.SubscriptionResult@13221655'
println(result)
}
デバッガーでも、情報がすぐには表示されません。
シンプルな toString
実装を追加すると、どちらの場合でも出力が大幅に改善されます。
//prints 'Subscription failed (reason=IncompatibleAccount, description="Users
// account does not support this type of subscription")'
override fun toString(): String {
val resultText = if(result) "succeeded" else "failed"
return "Subscription $resultText (reason=$reason, description=\"$description\")"
}
toString
メソッドを自動的に得るためにデータクラスを使用することは魅力的かもしれませんが、後方互換性の理由から推奨されません。データクラスについては、APIでのデータクラスの使用を避けるセクションで詳しく説明されています。
toString
メソッドで記述される状態は、問題領域からの情報である必要はないことに注意してください。それは、進行中のリクエストのステータス(上記の例のように)、外部サービスへの接続の健全性、または進行中の操作における中間状態に関連する可能性があります。
例えば、以下のビルダー型を考えてみましょう。
class Person(
val name: String?,
val age: Int?,
val children: List<Person>
) {
override fun toString(): String =
"Person(name=$name, age=$age, children=$children)"
}
class PersonBuilder {
var name: String? = null
var age: Int? = null
val children = arrayListOf<Person>()
fun child(personBuilder: PersonBuilder.() -> Unit = {}) {
children.add(person(personBuilder))
}
fun build(): Person = Person(name, age, children)
}
fun person(personBuilder: PersonBuilder.() -> Unit = {}): Person =
PersonBuilder().apply(personBuilder).build()
この型の使用例は次のとおりです。
上記の画像に表示されているブレークポイントでコードを停止させると、表示される情報はあまり有用ではありません。
シンプルな toString
実装を追加すると、はるかに有用な出力が得られます。
override fun toString(): String =
"PersonBuilder(name=$name, age=$age, children=$children)"
この追加により、デバッガーは次のように表示します。
これにより、どのフィールドが設定されており、どれが設定されていないかを即座に確認できます。
例外処理のポリシーを採用し、文書化する
適切なエラー処理メカニズムの選択セクションで議論されているように、ライブラリがエラーを通知するために例外をスローすることが適切な場合があります。この目的のために、独自の例外型を作成することもできます。
低レベルAPIを抽象化および簡素化するライブラリは、その依存関係によってスローされる例外も処理する必要があります。ライブラリは、例外を抑制するか、そのまま渡すか、異なる型の例外に変換するか、または異なる方法でユーザーにエラーを通知するかを選択できます。
これらのオプションのいずれも、コンテキストによっては有効です。例えば、
- ユーザーがライブラリBを簡素化する利便性のためだけにライブラリAを採用する場合、ライブラリAがライブラリBによって生成された例外を修正せずに再スローすることが適切である場合があります。
- ライブラリAがライブラリBを純粋に内部的な実装の詳細として採用する場合、ライブラリBによってスローされるライブラリ固有の例外は、ライブラリAのユーザーに決して公開されるべきではありません。
ユーザーがあなたのライブラリを効果的に使用できるように、例外処理に対する一貫したアプローチを採用し、文書化する必要があります。これはデバッグにとって特に重要です。ライブラリのユーザーは、デバッガーやログにおいて、例外があなたのライブラリから発生したものであることを認識できるべきです。
例外の型はエラーの種類を示すべきであり、例外に含まれるデータはユーザーが問題の根本原因を特定するのに役立つべきです。一般的なパターンは、低レベルの例外をライブラリ固有の例外でラップし、元の例外が cause
としてアクセスできるようにすることです。
次のステップ
ガイドの次のパートでは、テスト容易性について学びます。