導入
「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();
ITaskRepositoryはTaskItemをそのまま出し入れするだけで、中身の状態(完了かどうか)には関心がないTaskServiceはITaskRepositoryを使いながら、「未完了だけ」「重複禁止」のような判断を追加する- 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を組み合わせ、IsCompletedがfalseのものだけ残します
まとめ
- Repositoryは「そのまま出し入れ」、Serviceは「業務ルール・判断」を担当する
CompleteTaskはFindByIdで取り出したTaskItem(参照型)を直接Complete()するだけでよい(レッスン13の参照の仕組み)- ViewModelはServiceだけを呼び、Repositoryの存在すら知らなくてよい
次回: TaskServiceにもっと業務ルールを足し、TDDで積み上げます。