1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

担当:インターン生🐻🐰

この記事は

  • ASP.NET Core(Razor PagesやMVC)を学び始めた方
  • C#を独学していて、これから実務に入る方
  • 実務で初めてRazorPages(PageModel)・Service・Repository構成に触れた方

向けに書いています。

私たちは授業の一環でアクティブフュージョンズにインターンで来ている実習生です。
実習中の課題としてC#・ASP.NET Core Razor Pagesを使用して書籍管理システムを開発しました。

提示されたサンプルコードを見たとき、PageModel・Service・Repositoryと複数のクラスに処理が分かれていました。
そのため、「なぜここまで細かく分ける必要があるのだろう?」と疑問に感じていました。

しかし、開発を始めたばかりの頃は、お手本となるプロジェクトを参考に実装するだけで精一杯で、それぞれの役割を意識する余裕はありませんでした。

この記事では、私たちが実際に開発した書籍管理システムを例に、PageModel・Service・Repositoryの役割と、なぜ役割を分けるのかについて紹介します。

開発環境

書籍管理システムでは、以下の環境を使用しました。

  • C#
  • ASP.NET Core Razor Pages
  • Entity Framework Core
  • MariaDB
  • AdminLTE

また、本システムはASP.NET Core Razor Pagesを採用しているため、ControllerではなくPageModel(.cshtml.cs)が画面処理を担当しています。

インターネット上にある記事では「Controller / Service / Repository」の構成が紹介されることが多いです。

本記事ではASP.NET Core Razor Pagesで使用するPageModelを例に説明します。

MVC Razor Pages
Controller PageModel (.cshtml.cs)
View .cshtml
Service Service
Repository Repository

ControllerとPageModelは仕組みこそ異なりますが、「画面からのリクエストを受け取り、Serviceへ処理を依頼する」という役割は共通しています。

PageModel・Service・Repositoryの役割

実際のプロジェクトでは、機能ごとにService・Repositoryを分けています。

例えば、今回開発した書籍管理システムでは、次のように機能ごとにクラスを分割しています。

  • BookService / BookRepository(書籍管理)
  • BorrowService / BorrowRepository(貸出・返却)
  • EmployeeService / EmployeeRepository(社員管理)

ここからは、BookService / BookRepositoryの中でも書籍登録機能を例に、それぞれの具体的な役割と処理の流れを紹介します。

書籍登録機能における全体の処理は、次のような流れで実行されます。

PageModel・Service・Repositoryの処理の流れ

PageModel

PageModelは、利用者からのリクエストを受け取り、入力内容を確認したうえでServiceへ処理を依頼する役割を担当します。

PageModelの主な役割

  • 利用者からのリクエストを受け取る
  • 入力内容をチェック(検証)する
  • 画面用のデータを、システム内部用のデータに変換する
  • Serviceへ処理を依頼する
  • 処理の完了後に、適切な画面へ遷移させる
public async Task<IActionResult> OnPostAsync()
{
    // ①入力チェック
    // 画面の入力内容(ISBNやタイトルなど)に漏れや不整合がないか検証
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ②ViewModelからEntityへ変換
    // 画面から受け取ったViewModelを、保存に適したEntity(Bookクラス)へ変換
    var book = _mapper.Map<Book>(Book);

    // ③Serviceへ登録処理を依頼
    // データの準備ができたらBookServiceへバトンを渡し、登録処理を依頼
    await _bookService.AddBookAsync(book);

    // ④登録後は一覧画面へ戻る
    // すべての処理が無事に完了したら、一覧画面(Index)へと画面遷移
    return RedirectToPage("Index");
}

このように、PageModelは画面処理(入力チェックや画面遷移など)に専念し、具体的な登録処理はBookServiceへ依頼しています。

BookService

BookServiceは、書籍に関する業務ロジック(システムが満たすべきルールや処理)を担当します。

BookServiceの主な役割

  • 業務ロジックを実行する
  • 必要に応じて他のServiceや外部機能と連携する
  • Repositoryへ処理を依頼する
public async Task AddBookAsync(Book book)
{
    // ①ISBNの整形
    // 入力揺れを防ぐため、ハイフン(`-`)を取り除き、前後の不要な空白を削除
    book.ISBN = book.ISBN.Replace("-", "").Trim();

    // ②ISBNチェック
    // 整形したISBNが、正しい形式のルールになっているか検証
    if (!IsValidIsbn(book.ISBN))
    {
        throw new ArgumentException("ISBNが正しくありません。");
    }

    // ③Google Books APIからISBNをキーにして情報を取得
    // 外部APIと連携し、インターネット上から不足している書籍詳細データを自動で取得
    if (!string.IsNullOrWhiteSpace(book.ISBN))
    {
        var info = await _googleBooksService.GetBookInfoAsync(book.ISBN);

        if (info != null)
        {
            // ④取得した情報を設定
            // APIからデータが取得できた場合、説明文やページ数、カテゴリを自動でセット
            book.Description = info.Description;
            book.PageCount = info.PageCount;
            book.Category = info.Category;
        }
    }

    // ⑤Repositoryへ保存を依頼
    // すべての業務ルールをクリアした安全なデータを、Repositoryへ渡してデータベース保存を依頼
    await _bookRepository.AddAsync(book);
}

このように、BookServiceには「登録前に必要な処理」が集約されています。

そのため、画面処理はPageModel、書籍登録の手順はBookServiceというように役割を分けています。

また、Serviceは画面処理とデータベース処理の間に入り、
アプリケーション固有の業務ロジックをまとめる役割を担っています。

BookRepository

BookRepositoryは、データベースとの直接的なやり取り(データアクセス)のみを担当します。

BookRepositoryの主な役割

  • 書籍データの登録・取得・更新・削除を行う
// 書籍登録の処理
public async Task AddAsync(Book book)
{
    // ① データベースへの追加
    // 渡された書籍データを、データベースへの追加対象として登録
    await _dbContext.Books.AddAsync(book);

    // ② 変更内容を確定して保存        
    // `SaveChangesAsync()` を実行し、変更内容をデータベースへ保存
    await _dbContext.SaveChangesAsync();
}

※ 実際のシステムではジェネリックリポジトリを使用していますが、役割を分かりやすくするため一部簡略化しています。

データベースへの登録や取得などのデータアクセス処理をRepositoryへまとめることで、
Serviceはデータベースの実装を意識せず利用できます。

それぞれの役割をまとめると、次のような構成になります。

クラス 主な役割
PageModel 画面からのリクエストを受け取り、Serviceへ処理を依頼する
BookService 業務ロジックを実行し、Repositoryへ処理を依頼する
BookRepository Entity Framework Coreを利用してデータベースへアクセスする

まとめ

画面表示や業務ルール、データベース操作をすべて1つのファイルに書いてしまうと、コードが長くなって見通しが悪くなってしまいます。
それを防ぐため、今回は「関心の分離」を意識し、以下の3つに役割を分けて実装しました。

  • PageModel:リクエスト受付・入力チェック・画面遷移
  • Service:業務ロジック・外部API連携
  • Repository:データベースアクセス(データの保存・取得)

開発当初は、「ここまで細かく分ける必要があるの?」と疑問でした。
しかし、貸出機能やAPI連携を実装していく中で、その価値を強く実感しました。

役割ごとにファイルが分かれているため、不具合の原因特定や修正がスムーズに行えます。
また、クラスごとに影響範囲が限定されるおかげで、既存の処理を壊すことなく、安心して新しい機能を追加できました。

「とりあえず1つのファイルに書く」のではなく、「役割を意識してコードを分ける」設計にする。
この考え方を意識することで、コードの読みやすさも開発の進めやすさも大きく変わります。

この記事が、これからASP.NET CoreやRazor Pagesで開発を始める方の参考になれば幸いです。

参考リンク集

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?