本文へスキップ
BecomeCoder

C# MVVMコース · 第5章 Service ― 業務ロジックの層 · レッスン16

Serviceとは何か ― Repositoryとの違い

ブラウザで完結

導入

「Repositoryがあるのに、なぜもう1つServiceが要るの?」という疑問はよくあります。ITaskRepositoryは「保存する・取り出す」しかできません。「未完了だけを見せる」「同じタイトルは禁止する」といった業務のルール・判断は、Repositoryの仕事ではありません。それを担うのがServiceです。

図解

flowchart LR
    VM["ViewModel(画面の状態)"] --> SVC["TaskService<br/>業務ルール・判断"]
    SVC --> REPO["ITaskRepository<br/>データの出し入れだけ"]
    REPO -.-> MEM["InMemory / Fake"]
    style SVC fill:#e1f5fe

サンプル

役割の違いをコードで見ます。

// Repository: 「そのまま出し入れ」しかしない(判断なし)
IReadOnlyList<TaskItem> all = repository.All();

// Service: 「未完了だけ」という判断(業務ルール)を持つ
IReadOnlyList<TaskItem> active = service.GetActiveTasks();
  • ITaskRepositoryTaskItemをそのまま出し入れするだけで、中身の状態(完了かどうか)には関心がない
  • TaskServiceITaskRepositoryを使いながら、「未完了だけ」「重複禁止」のような判断を追加する
  • ViewModelはServiceだけを呼べばよく、細かい業務ルールを知らなくてよい(次章で作ります)

やってみよう

TaskItem自身が持つ検証(TaskTitleの空文字・50文字制限)と、これからTaskServiceに足す検証(重複タイトル禁止)の違いを意識しながら読み進めましょう。前者はEntity(レッスン6)の責任、後者はServiceの責任です。

演習

var repo = new InMemoryTaskRepository();
var service = new TaskService(repo);
service.AddTask("洗濯", Priority.Low);
var t2 = service.AddTask("掃除", Priority.Medium);
t2.Complete();

Assert(service.GetActiveTasks().Count == 1, "未完了は1件");
Console.WriteLine("全テスト成功");

void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }

// --- 型のコピー ---
public readonly struct TaskTitle : IEquatable<TaskTitle>
{
    public string Value { get; }
    public TaskTitle(string value)
    {
        if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("タイトルは空にできません");
        if (value.Length > 50) throw new ArgumentException("タイトルは50文字以内です");
        Value = value.Trim();
    }
    public bool Equals(TaskTitle other) => Value == other.Value;
    public override string ToString() => Value;
}
public enum Priority { Low, Medium, High }
public class TaskItem
{
    public Guid Id { get; }
    public TaskTitle Title { get; private set; }
    public Priority Priority { get; private set; }
    public bool IsCompleted { get; private set; }
    public TaskItem(Guid id, TaskTitle title, Priority priority)
    { Id = id; Title = title; Priority = priority; }
    public void Complete() => IsCompleted = true;
    public void Rename(TaskTitle newTitle) => Title = newTitle;
}
public static class TaskFactory
{
    public static TaskItem Create(string title, Priority priority = Priority.Medium)
        => new TaskItem(Guid.NewGuid(), new TaskTitle(title), priority);
}
public interface ITaskRepository
{
    void Add(TaskItem task);
    IReadOnlyList<TaskItem> All();
    TaskItem? FindById(Guid id);
}
public class InMemoryTaskRepository : ITaskRepository
{
    private readonly List<TaskItem> _tasks = new();
    public void Add(TaskItem task) => _tasks.Add(task);
    public IReadOnlyList<TaskItem> All() => _tasks;
    public TaskItem? FindById(Guid id) => _tasks.FirstOrDefault(t => t.Id == id);
}
public interface ITaskService
{
    TaskItem AddTask(string title, Priority priority);
    void CompleteTask(Guid id);
    IReadOnlyList<TaskItem> GetActiveTasks();
}
public class TaskService : ITaskService
{
    private readonly ITaskRepository _repository;
    public TaskService(ITaskRepository repository) => _repository = repository;
    public TaskItem AddTask(string title, Priority priority)
    {
        var task = TaskFactory.Create(title, priority);
        _repository.Add(task);
        return task;
    }
    public void CompleteTask(Guid id) => _repository.FindById(id)?.Complete();
    // TODO: 完了していないタスクだけを返す GetActiveTasks を実装してください
    ___
}
  • 期待される出力: 全テスト成功
ヒント1を見る

public IReadOnlyList<TaskItem> GetActiveTasks() => _repository.All().Where(t => !t.IsCompleted).ToList();

ヒント2を見る

_repository.All()にLINQのWhereを組み合わせ、IsCompletedfalseのものだけ残します

まとめ

  • Repositoryは「そのまま出し入れ」、Serviceは「業務ルール・判断」を担当する
  • CompleteTaskFindByIdで取り出したTaskItem(参照型)を直接Complete()するだけでよい(レッスン13の参照の仕組み)
  • ViewModelはServiceだけを呼び、Repositoryの存在すら知らなくてよい

次回: TaskServiceにもっと業務ルールを足し、TDDで積み上げます。

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。