Utilityクラスは捨てろ:Dart拡張機能で実現する「読むだけでわかる」コード設計術

「このコード、主語はいったい誰なんだ?」
コードレビューをしていて、StringHelper.process(text)DateUtils.format(date) といった記述にうんざりしたことはありませんか?これらは機能的には正しいですが、オブジェクト指向の「オブジェクトにメッセージを送る」という直感的な流れを断ち切ってしまいます。

Dart 2.7で導入された拡張機能(Extension Methods)は、単なるシンタックスシュガーではありません。これは、既存のライブラリやSDKのクラスに対して、あたかも最初からその機能が備わっていたかのように振る舞いを追加できる強力な武器です。本記事では、単なる構文解説にとどまらず、実務で即使えるFlutterのパターンや、シニアエンジニアが意識しているパフォーマンスと設計の勘所まで深掘りします。

1. なぜ「Helperクラス」ではダメなのか?

従来のアプローチと拡張機能を比較すると、その「可読性」の違いは一目瞭然です。文字列を整数に変換するシンプルな処理を見てみましょう。


// ❌ 従来のHelperクラス方式
// 主語(numberStr)と動詞(tryParseInt)が離れており、直感的ではない
var parsed = StringParser.tryParseInt(numberStr);

// ✅ Dart拡張機能方式
// "numberStrをintegerとして扱う" という自然な英文のように読める
var parsed = numberStr.asInteger;
メリット: IDEのオートコンプリート(ドット.を入力した瞬間)に候補として表示されるため、開発体験(DX)が圧倒的に向上します。開発者は「どのHelperクラスを使うべきか」を覚える必要がなくなります。

2. 今すぐ使える実践パターン3選

教科書的な説明は公式ドキュメントに任せ、ここでは現場で頻出する「効果が高い」パターンに絞って紹介します。

① Flutterのネスト地獄を解消する

Flutterを書いていると、PaddingCenterでウィジェットをラップするたびにインデントが深くなり、可読性が落ちがちです。拡張機能を使えば、これをメソッドチェーンのように記述できます。


// extension_ui.dart
import 'package:flutter/material.dart';

extension WidgetModifiers on Widget {
  Widget padding(EdgeInsetsGeometry padding) {
    return Padding(padding: padding, child: this);
  }

  Widget get center => Center(child: this);
}

// 使用例:宣言的で読みやすい!
Widget build(BuildContext context) {
  return Text('Hello World')
      .padding(EdgeInsets.all(16.0))
      .center;
}

② Null許容型(Nullable)への「安全な」拡張

DartのNull Safetyにおいて、String?のようなNullableな型に対しても拡張を定義できます。これにより、わざわざif (text != null)を書く手間を省けます。


extension NullableStringExt on String? {
  /// null または 空文字の場合に true を返す
  bool get isNullOrEmpty {
    return this == null || this!.isEmpty;
  }
}

void main() {
  String? input;
  if (input.isNullOrEmpty) {
    print('値がありません'); // 安全に実行される
  }
}
注意点: thisnull になり得ることを常に意識してください。拡張内部で this! を使う際は、確実に null でないことが保証されている箇所に限定しましょう。

③ ジェネリクス制約で「特定の型だけ」強化する

すべてのリストではなく、「数値のリスト」だけに合計値を計算する機能を追加したい場合、where(制約)を使います。


// Tがnum(intやdoubleの親)を継承している場合のみ有効
extension NumList<T extends num> on List<T> {
  T get sum {
    if (isEmpty) return (T == int ? 0 : 0.0) as T;
    return reduce((a, b) => (a + b) as T);
  }
}

void main() {
  [1, 2, 3].sum;       // OK: 6
  [1.5, 2.5].sum;     // OK: 4.0
  // ['a', 'b'].sum;  // コンパイルエラー!Stringはnumではない
}

3. シニアエンジニアが教える「内部動作」とパフォーマンス

「便利だけど、パフォーマンスは大丈夫?」と不安になるかもしれませんが、心配無用です。

静的解決(Static Dispatch)の仕組み
Dartの拡張機能は、コンパイル時に解決されます。つまり、コンパイラは obj.method() を見つけると、裏側で自動的に静的なヘルパー関数呼び出し Extension.method(obj) に変換します。

このため、実行時のオーバーヘッド(仮想テーブルのルックアップなど)はゼロです。通常の関数呼び出しと全く同じ速度で動作します。

ただし、ここだけは気をつけて!

拡張機能は「静的な型」に基づいて解決されます。dynamic型変数に対しては機能しません。


extension Ex on String {
  void sayHello() => print('Hello');
}

void main() {
  dynamic d = 'text';
  // d.sayHello(); // ランタイムエラー:NoSuchMethodError
  
  if (d is String) {
    d.sayHello(); // OK:型昇格(Type Promotion)により解決可能
  }
}

4. 名前の衝突(Conflict)をどう回避するか

大規模なプロジェクトでは、異なるライブラリが偶然同じ名前の拡張メソッド(例:toJson)を提供してしまうことがあります。

解決策 コード例 推奨度
Hide/Show import 'lib_a.dart' hide MyExt; ★★★★★ (最も一般的)
ラッパー呼び出し MyExt(obj).method() ★★★☆☆ (一時的な解決に便利)
エイリアス import 'lib_a.dart' as libA; ★★★★☆ (明確だが記述が長くなる)

まとめ:コードの「表現力」を武器にしよう

Dartの拡張機能は、既存のクラスを変更することなく、あなたのドメイン(解決したい問題領域)に特化した語彙をコードに追加できる素晴らしい機能です。

  • Utilityクラスをやめ、主語(オブジェクト)中心のAPIにする。
  • FlutterのUI構築をメソッドチェーンでスッキリさせる。
  • 静的解決なのでパフォーマンスの心配は無用。

ただし、「なんでも拡張すればいい」わけではありません。あくまで「その型にとって自然な振る舞いか?」を常に自問しながら、用法・用量を守って使いこなしてください。

Post a Comment