🐧

【Flutter】超ざっくり/非エンジニアDDDを理解してみよう入門?

に公開

こんにちは〜道上です!
https://zenn.dev/yochi/articles/4deca630e30094
Flutterでアプリ開発をしていると、「UIは作れるけど、設計どうしたらいいの?」という壁にぶつかります。そんな時に知っておきたいのが、DDD(ドメイン駆動設計)です。

今回は、DDDの基本的な考え方を取り入れたシンプルなカウンターアプリをFlutterで実装しながら、「責務の分離」と「設計の見通しの良さ」を体験してみましょう!

DDD(ドメイン駆動設計)とは?

DDDは、Eric Evansが提唱した「ソフトウェアの設計思想」です。ビジネスのルール(ドメイン)を中心にアプリ全体を設計することで、保守性・拡張性を高めることを目的としています。

みんな大好き飲食店経営に例えてみよう

-DDDでいう「ドメイン」は、飲食店にとっての「日々の営業で大切にしているルールや判断基準」。
例えば「食材ロスを減らすために、前日の売れ筋を元に仕込みを調整する」など、ビジネスの知恵と知識の塊がドメインです。
DDDではこのドメインに沿ってソフトウェアを設計していくので、「厨房での動き」「注文の流れ」「在庫管理」などが自然にシステムに反映されていきます。

DDDの概念 飲食店にたとえると 解説
ドメイン(Domain) お店の“営業ルール”や“ビジネスの本質” 例:ランチタイムは定食のみ、仕入れに応じてメニューが変わる…など
エンティティ(Entity) 顧客、注文、料理、スタッフなど 識別子(ID)を持ち、時間が経っても同一性を保つもの
バリューオブジェクト(Value Object) メニューの「価格」「料理名」「カロリー」など 意味ある値の集合で、IDは不要。再利用・比較されるもの
集約(Aggregate) 「注文」+「注文された料理の詳細」 一つのまとまりとして操作される単位(=業務的に意味がある塊)
ドメインサービス 「配膳順を決める」「在庫の自動発注」など 複数のエンティティにまたがるルールや処理(でもエンティティに属さない)
リポジトリ(Repository) 注文履歴を記録・取り出すためのノートやPOS データの永続化と取得を担当する仕組み
ユビキタス言語(Ubiquitous Language) 「ホール」「仕込み」「まかない」などの共通語 スタッフ全員が使う、意味のブレない用語(コードでも使う)

~ちょっと難しく考えよう~

↓なぜ使うか
ドメインを積み重ねると組織システムが強くなっていくと思われる

  1. 共通言語(ユビキタス言語)が根付く
    ドメインを中心に考えると、現場の言葉や価値観をコードや設計に反映するようになります。
    開発者・営業・マーケターが同じ言葉で会話できるようになると、認識のズレが減り、組織のコミュニケーションコストが劇的に下がります。
  2. 業務理解が深まり、ノウハウが形式知として残る
    ドメインはその組織ならではの知識・判断・戦略です。それをモデルとしてコード化・設計に反映していくことで、属人性の低い、再現性のある業務ロジックが蓄積されていきます。
  3. 変更に強くなる(=拡張性・柔軟性)
    ビジネスが成長すると、ドメインも変化します。DDDではドメインを中心に設計しているため、「新しい戦略」「新たな顧客層」にも柔軟に対応しやすい設計になります。これが組織の適応力につながります。
  4. 事業に対する「理解の深さ」が武器になる
    ドメインを正しく積み重ねていると、他社では真似できないレベルで事業や業務の本質を理解している状態になります。これは競合優位性になります。

現場との対話(=業務理解)
明確な言語(ユビキタス言語)
モデルの継続的なリファクタリング
ビジネスと技術の協調

DDDにおける主なレイヤー構成:

├── Presentation(UI層)
├── Application(アプリケーション層:ユースケース)
├── Domain(ドメイン層:ビジネスルール)
└── Infrastructure(インフラ層:DBやAPI)

実装コード:DDDなカウンターアプリ

git→[https://github.com/yoichi4141/DDD_sample]

domain/counter.dart
class Counter {
  int _value;

  Counter(this._value);

  int get value => _value;

  void increment() {
    _value++;
  }

  void decrement() {
    _value--;
  }
}
repository/counter_repository.dart
import '../domain/counter.dart';

abstract class CounterRepository {
  Counter fetch();
  void save(Counter counter);
}
infrastructure/counter_repository_impl.dart
import '../domain/counter.dart';
import '../repository/counter_repository.dart';

class InMemoryCounterRepository implements CounterRepository {
  Counter _counter = Counter(0);

  
  Counter fetch() => _counter;

  
  void save(Counter counter) {
    _counter = counter;
  }
}
application/counter_service.dart
import '../domain/counter.dart';
import '../repository/counter_repository.dart';

class CounterService {
  final CounterRepository _repository;

  CounterService(this._repository);

  Counter getCounter() {
    return _repository.fetch();
  }

  void increment() {
    final counter = _repository.fetch();
    counter.increment();
    _repository.save(counter);
  }

  void decrement() {
    final counter = _repository.fetch();
    counter.decrement();
    _repository.save(counter);
  }
}
presentation/counter_page.dart
import 'package:flutter/material.dart';
import '../application/counter_service.dart';

class CounterPage extends StatefulWidget {
  final CounterService service;

  const CounterPage({super.key, required this.service});

  
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  late int _value;

  
  void initState() {
    super.initState();
    _value = widget.service.getCounter().value;
  }

  void _updateValue() {
    setState(() {
      _value = widget.service.getCounter().value;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('DDD Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('$_value', style: TextStyle(fontSize: 40)),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    widget.service.increment();
                    _updateValue();
                  },
                  child: Text('+'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    widget.service.decrement();
                    _updateValue();
                  },
                  child: Text('−'),
                ),
              ],
            )
          ],
        ),
      ),
    );
  }
}
main.dart
import 'package:flutter/material.dart';
import 'presentation/counter_page.dart';
import 'application/counter_service.dart';
import 'infrastructure/counter_repository_impl.dart';

void main() {
  final repository = InMemoryCounterRepository();
  final service = CounterService(repository);

  runApp(MyApp(service: service));
}

class MyApp extends StatelessWidget {
  final CounterService service;

  const MyApp({super.key, required this.service});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DDD Counter',
      home: CounterPage(service: service),
    );
  }
}

おわりに

-この構成は一見「分けすぎ?」と感じるかもしれませんが、
-責務が明確になる
-テストがしやすくなる
-UIとロジックが分離される

さらに深掘りが必要

//DDD深掘り後からリンク

Discussion