BEMAロゴ

エンジニアの
成長を支援する
技術メディア

貨物船は、積荷を知らない。コンテナ物語とListViewに学ぶStrategy Patternの本質【Flutter】

はじめに | なぜ、あなたのコードは「神クラス」になるのか?

こんにちは、株式会社メンバーズ Cross ApplicationOpen in new tabカンパニーの田原です。

皆さんは、機能追加のたびにコードが複雑になり、身動きが取れなくなった経験はありませんか?

本体に引数やフラグ、大量の条件分岐(switch / if-else)を詰め込むことで、意図せず「ファットな神クラス」「スパゲティコード」を生み出してしまう。その結果、保守コストに追われている開発者は少なくありません。優れたフレームワークが柔軟性を保てる秘密は、「具体的な処理を、あえて自分では持たない」という潔い自制にあります。

本記事では、この「詳細を外部に委ねる設計」の代表格であるStrategy Pattern(ストラテジーパターン) の本質を、現代物流の「コンテナ物語」をヒントに深掘りします。Strategy Patternは様々なライブラリやフレームワークで頻出のデザインパターンですが、ここでは、Flutterの ListView と ScrollPhysics を例に、拡張性を担保するための設計思想を、具体例とともに解説します。

良い設計は「詳細」を知らない

ソフトウェア設計における一つの理想は、「器(コンテキスト)」が「中身(アルゴリズム)」の具体的な手順を知らないことです。

この設計思想の重要性を理解するために、少しだけ「現代の物流史」を覗いてみましょう。

物流を変えた「コンテナ」規格化の設計思想

現代の物流を劇的に変えたのは、一隻の巨大な貨物船でも、強力なエンジンでもありません。「コンテナ」という、ただの鉄の箱の規格化でした。

20世紀半ばまでの港の荷役は、混沌としていました。積荷(樽、袋、木箱)に合わせて職人が積み方を変える、属人的で非効率な作業の連続です。これでは、船を大型化したところで、積み込みに時間がかかりすぎて停泊コストに押し潰されてしまいます。

しかし、アメリカの陸運業者マルコム・マクリーンが提唱した「コンテナ」という共通の規格が登場したことで世界は一変します。

コンテナの本質は、中身を隠すことではなく、中身が何であれ、外側(インターフェース)を同一に揃えたことにあります。コンテナの四隅にある固定金具の位置や強度が世界標準として厳格に規定されたのです。

これにより、船も、クレーンも、トラックも、「中身が何か(精密機械なのか、穀物なのか)」を一切気にせず、ただ「コンテナをどう扱うか」という共通の手順だけに集中すればよくなったのです。

Strategy Patternの核心 | 「契約による自由」をプログラミングに適用

コンテナにおける「外側を揃える」設計は、プログラミングにおける 「抽象化」 そのものです。そして、この知恵をさらに進め、「振る舞い(アルゴリズム)そのものを交換可能にする」という形で結実させたのがStrategy Patternです。

ここには、効率化のための共通した「契約」の形が存在します。

  • 器(Context / 貨物船)は、具体的な中身に立ち入らず、「中身をどう扱うか」という操作の規格(インターフェース)だけを規定します。

  • 中身(Strategy / 荷物)は、その規格を遵守して実装されます。

この「器がインターフェースを規定し、中身がそれを遵守する」という主従関係を構築することこそが、このパターンの核心です。

船側がコンテナの規格という「契約」だけを頼りに作業を進めるように、プログラムの「器」もまた、インターフェースという「契約」だけを信じて処理を実行します。

詳細をあえて知らない。その「潔い無知」を契約によって担保することで、私たちはシステムの堅牢性を保ったまま、中身を自由に入れ替えられる「拡張性」を手に入れることができるのです。

【基本構造】Strategy Patternの3つの登場人物(Context, Strategy, ConcreteStrategy)

コンテナ物語で概念を理解したところで、Strategy Patternがプログラム上でどのように表現されるのか、その基本的な構造を確認します。このパターンには、以下の3つの主要な登場人物がいます。

  1. Context (コンテキスト / 貨物船):
    処理を実行する「器」です。具体的なアルゴリズムを知らず、Strategyインターフェースに処理を委譲します。

  2. Strategy (ストラテジー / コンテナの規格)
    アルゴリズムの共通インターフェース(抽象クラスやインターフェース)を規定します。

  3. ConcreteStrategy (具体的なストラテジー / 各種の積み方):
    Strategyを実装し、具体的なアルゴリズム(振る舞い)を提供します。

この構造を頭に入れた上で、いよいよFlutterの世界に飛び込みます。

【Flutter実例】ListView と ScrollPhysics に見るStrategy Pattern

この世界規格のコンテナによる物流革命を、私たちの身近なコードに落とし込んでみましょう。Flutter において、もっとも巨大で、もっとも多種多様な「荷物」を運ぶ貨物船。それが ListViewOpen in new tab です。

ListView(厳密にはその内部の ScrollableOpen in new tab)は、驚くべきことに、スクロールの具体的な計算式を一行も持っていません。持っているのは、ScrollPhysicsOpen in new tabという規格に準拠したコンテナなら誰でも受け入れる」 という窓口だけです。

FlutterにおけるStrategy Patternの構造 | 船とコンテナの設計図

この設計をコードに落とし込むと、オブジェクト指向の持つ「役割の分離」という美しさが際立ちます。

  • ScrollPhysics(Strategy / 共通規格)

    • コンテナの四隅にある「固定金具」の仕様書に相当します。 「端に到達したとき、どう振る舞うべきか?」「指を離した後の慣性は?」といった、物理演算に必要な「問い(メソッド)」だけが定義された抽象クラスです。

  • BouncingScrollPhysics(Concrete Strategy / 具体的実装)

    • 共通規格である ScrollPhysics を遵守しつつ、その内部に「iOSのように端でバウンスさせる」という具体的な答え(実装)を詰め込んだクラスです。 規格通りの金具(メソッド)を備え、中身に独自の知能を持った「コンテナそのもの」と言えます。

  • ListView(Context / 貨物船)

    • 積荷の正体には依存せず、ただコンテナを載せ、規格化された手順で運搬(表示)を行う「器」です。

拡張性を担保する設計 | コードで見る「窓口」の設計

ListView の設計思想を理解するために、その構造を Strategy Pattern の視点でシンプルにモデル化してみましょう。この「器」が中身をいかに知らないかがよく分かります。

// ※設計の本質を理解するために、ListViewの構造を簡略化・モデル化しています。
class ListView extends BoxScrollView {
  final ScrollPhysics? physics; 

  const ListView({
    super.key,
    this.physics,
    // ...
  });
}

※実際の Flutter フレームワークにおいて、physics フィールドは親クラスの ScrollView で定義されていますが、ここでは設計の構造(窓口の役割)を分かりやすく示すために ListView 内で表現しています。

ここで重要なのは、ListView が BouncingScrollPhysicsOpen in new tabClampingScrollPhysicsOpen in new tab といった個別のクラスを一切参照していない点です。

待っているのは、特定の誰かではなく、「ScrollPhysics という規格を満たした誰か」 です。この主従関係の逆転こそが、中身を自由に入れ替えられる拡張性の正体です。

Strategy Patternがもたらす「3つのメリット」

この「船とコンテナ」の構造によって、私たちは以下の利益を無意識に享受しています。

  1. 「開発の分離」:疎結合 
    Flutterチームが物理演算のバグを修正するために BouncingScrollPhysics を書き換えても、それを利用している私たちの ListView 側のコードを修正する必要は一切ありません。

  2. 「実行時の振る舞い変更」:交換可能性 
    プラットフォームごとに挙動を変える実装も、コンテナを積み替えるだけで完了します。

physics: Platform.isIOS 
    ? const BouncingScrollPhysics() 
    : const ClampingScrollPhysics(),
  1. 「未知の挙動」への拡張性:開放閉鎖の原則 
    これが最大の利益です。Flutterが誕生したときに存在しなかった「新しいスクロール挙動」が必要になっても、Framework本体を改造する必要はありません。私たちが ScrollPhysics 規格に沿った自作コンテナを作るだけで、全く新しい挙動を ListView に流し込めるのです。

もしこの「契約」がなかったら?

もしStrategy Patternが失われたら、システムはどう変貌するでしょうか。現在の洗練された設計と、設計が破綻した「負債のシナリオ」を比較してみます。

シナリオ A:【理想】現在のインターフェースによる抽象化

physics という一つの窓口(インターフェース)に、物理演算という「知能(オブジェクト)」を注入するだけで完結します。

// 理想的な設計:規格化されたコンテナを積み込む世界
ListView.builder(
  // 「物理演算」という知能をカプセル化したオブジェクトを注入
  physics: const BouncingScrollPhysics(), 
  itemCount: 100,
  itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
)

シナリオ B:【破綻】詳細が漏れ出した硬直的な設計

一方で、もしStrategy Patternという「知恵」がなかったらどうなるでしょうか。

「戦略」をオブジェクトとして渡せなくなった世界では、貨物船(ListView)自体が、あらゆる荷物の扱い方を「引数」として抱え込むことになります。

ListView.builder(
  physicsType: PhysicsType.bouncing,
  friction: 0.015,
  bouncingDecelerationRate: 0.998,
  allowOverScroll: true,
  itemCount: 100,
  itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
)

このコードの最大の問題は、本来「コンテナの中身」に隠蔽されているべき計算パラメータが、ListView の引数として漏れ出している点にあります。

  1. カプセル化の破壊:
    friction(摩擦)や decelerationRate(減速率)といった値は、特定の物理演算クラスだけが知っていればよい、極めてプライベートな詳細です。それらが ListView のインターフェースに露出することで、船の設計図(API)が特定の荷物の都合によって「汚染」されています。

  2. 無限に増殖する引数:
    もし新しいスクロール挙動を追加したければ、その度に ListView に新しい引数を追加し続けなければなりません。船を造り直さなければ、新しい荷物が運べない状態です。

  3. 利用者の混乱:
    「バウンスさせたいだけ」のユーザーであっても、関係のない膨大なパラメータの山と向き合うことになります。

これはまさに、コンテナが普及する前の港で、船長が「この樽は壊れやすいか?」「この袋は湿気に強いか?」と、一つひとつの荷物の詳細を把握しなければ出航できなかった、混沌とした光景そのものです。

Framework 内部で起きる「責務の汚染」

この比較が突きつける真のリスクは、Framework内部のコードを想像するとより鮮明になります。

理想: 疎結合な設計(Strategy Pattern適用時)

ListView は「表示」という責務に集中し、計算は専門家(ScrollPhysics)に委ねられます。

void handleScroll(double offset) {
  // 契約に基づき、外部の専門家に計算を依頼するだけ
  final newOffset = physics.applyPhysicsToUserOffset(offset);
}

破綻: 密結合な設計 | switch文による巨大な計算ロジックへの集約

ListView 自体が「巨大な計算機」になってしまい、あらゆる要件を一つのファイルで抱え込むことになります。

// 【保守性の崩壊】ListViewの中に、あらゆるスタイルの計算式が混在する
void handleScroll(double offset) {
  switch (physicsType) {
    case PhysicsType.bouncing:
      /* --- iOS風:端で弾む複雑な計算 --- */
      break;

    case PhysicsType.clamping:
      /* --- Android風:端で止まる計算 --- */
      break;

    /* --- 以下、終わりなき条件分岐の羅列 --- 
     * case PhysicsType.page:
     * case PhysicsType.fixedExtent:
     * case PhysicsType.carousel: 
     * case PhysicsType.neverScroll:
     * case PhysicsType.alwaysScroll:
     * case PhysicsType.customA:
     * ...
     * case PhysicsType.platformUnknown:
     * ...
     */
     
    default:
      throw UnimplementedError('新機能を追加するには、ListView本体の修正が必要です');
  }
}

負債の連鎖 | 複数メソッドへのswitch文の蔓延

さらに深刻なのは、このswitch分岐が、ScrollPhysics が担っていたすべてのメソッド(振る舞い)の内部に増殖するという点です。

class ListView extends ... {
  // ドラッグ量の計算
  double applyPhysicsToUserOffset(...) {
    switch (physicsType) { /* 膨大な分岐 */ }
  }

  // 慣性シミュレーションの生成
  Simulation? createBallisticSimulation(...) {
    switch (physicsType) { /* 同様の分岐が繰り返される */ }
  }
  
  // ... applyBoundaryConditions 等、全メソッドに波及する
}

この構造が引き起こす「ビジネス上の代償」

この設計が示す真の恐ろしさは、単なる「行数の多さ」ではありません。「器(貨物船)」が「中身(荷物)」の詳細に依存しすぎており、保守が困難になることにあります。

本来、貨物船である ListView は、荷台をどう移動させるかだけに集中すべきでした。しかし Strategy Pattern が失われた世界では、船のブリッジに「積荷ごとの特殊な操作パネル」が剥き出しで並んでいるような状態です。

これでは、特定の挙動に修正を入れるたびに、全く関係のない ListView 本体のコードを改修しなければなりません。一つの修正が、予期せぬ場所で「デグレード(品質退行)」を引き起こすリスクを常に抱え続けることになるのです。

Flutterのあらゆる場所に潜む「コンテナの知恵」

実は、Flutterというフレームワーク自体が、この「規格の連鎖」の集大成です。私たちが意識せず使っている多くの機能が、実は Strategy Pattern によって支えられています。

  • Sliver
    CustomScrollView は個別のパーツの中身を知りません。Sliver という契約(RenderSliver)さえ守られていれば、リストもグリッドもヘッダーも、自由な組み合わせで積み込めます。

  • Decoration
    Container 自体は角を丸くする方法も、影をつける方法も知りません。Decoration という規格に従った「塗装指示書」を受け取り、その指示通りに見た目を作るだけです。

  • CustomPainter
    複雑な図形を描くとき、Flutterは描画の手順を抱え込みません。CustomPainter という規格を私たちに渡し、「このキャンバス(インターフェース)に好きなように描いてくれ」と詳細を外部に委ねます。

「器」が「個別の詳細」をあえて知らないからこそ、私たちはフレームワーク本体を改造することなく、自由なレイアウトや演出を際限なく組み上げることができます。FlutterにおいてStrategy Patternは、単なるデザインパターンの一つではなく、このエコシステムを支える「背骨」そのものなのです。

まとめ | 貨物船は「知らないフリ」を貫く

もし世界からStrategy Patternが消えてしまったら。 ListView の引数は無限に増殖し、ソースコードは数万行の if-else 分岐に埋め尽くされるでしょう。新しいスクロール挙動を追加したければ、Google の Flutter チームにプルリクエストを送り、承認を待つしかありません。

私たちが physics 一つで挙動を自由に変えられるのは、ListView が計算の詳細を『知らないフリ』をしてくれているからなのです。

ソースコードという「港」の景色

この話は、何もFlutter本体の話に限ったことではありません。私たちのプロジェクトの中にも、日々「規格なき荷物」が運び込まれ、整理のつかない山積みの負債となってはいませんか?

  • 決済手段が増えるたびに、引数が増えていく注文処理

  • ユーザー権限ごとに、複雑なswitch文を抱え込んだWidget

  • 出力先が増えるたびに、内部が汚染されていくログ出力ロジック

それらはすべて、かつての港で職人が樽や袋を抱えて途方に暮れていた光景と同じです。

船を止めるな、荷物を隠せ

マクリーンが成し遂げたのは、「船の改良」ではなく「荷物の隠蔽」でした。 船(器)をいじるのをやめて、コンテナ(インターフェース)を定義しましょう。

中身が精密機械だろうと、真っ赤なリンゴだろうと、四隅に決まった「固定金具(メソッド)」さえついていれば、船はそれを同じように積み込み、目的地まで運ぶことができます。

「すべてを知っている万能なコード」を書く必要はありません。 むしろ、詳細をあえて知ろうとせず、「ここから先はコンテナの中身にお任せします」と外部に委ねる「潔い無知」こそが、あなたのシステムを停滞から救い、巨大な貨物船へと進化させます。

次にあなたが、膨れ上がる引数やswitch文という「混沌」に直面したときは、少しだけ手を止めて、港に立つマクリーンの気分で問いかけてみてください。

「それはコンテナに詰め込んで、船に『知らないフリ』をさせられないか?」

その一工夫が、あなたのコードを複雑さの泥沼から救い出し、拡張という名の広い海へ連れ出してくれるはずです。

次にあなたが if-else を書こうとした時、それはコンテナの規格化が必要な合図かもしれません。

最後までお読みいただき、ありがとうございました!

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

田原 葉
田原 葉
2024年にメンバーズに中途で入社。前職はiOSエンジニア。現在はCross ApplicationカンパニーでFlutter技術をメインにモバイルアプリ開発支援を担当。
詳しく見る
ページトップへ戻る