Day 39:複数クラスの連携
Day 39では、複数のクラスを役割ごとに分け、協力させる方法を学びます。今回の基本形は、次の流れです。
Program(利用者とのやり取り)
↓
Service(アプリの処理や手順)
↓
Model(データと、そのデータを守るルール)
クラスを分ける目的は、コードの量を減らすことだけではありません。それぞれのクラスが何を担当するかを明確にし、機能の追加や修正をしやすくすることです。
3つの役割を理解する
Program:画面とのやり取りを担当する
Program は、コンソールへの表示や入力の受け取りなど、利用者とのやり取りを担当します。入力された値を使ってServiceのメソッドを呼び、結果を画面に表示します。
Service:アプリの処理を担当する
Service は、アプリケーションの機能や処理の流れを担当します。たとえば、商品を登録する、指定した商品を探す、販売処理を依頼する、といった操作をまとめます。
Model:データとデータのルールを担当する
Model は、アプリで扱うデータを表します。商品を扱うなら、商品名や価格、在庫数などを持ち、在庫数がマイナスにならないようなルールを管理します。
この3つの役割は、学習用プログラムを理解しやすくするための基本形です。実際の大きなアプリでは、画面、データベース、外部サービスなどを扱うため、さらにクラスを分けることがあります。
今回作るもの
商品を登録し、商品一覧を表示し、商品を販売する簡単なプログラムを作ります。商品データを Product、商品の登録や販売を ProductService、メニュー表示や入力を Program が担当します。
ステップ1:ModelとなるProductを作る
Product クラスは商品一つ分のデータを持ちます。商品の在庫を直接外から変更できないようにし、販売数を確認してから在庫を減らすメソッドを用意します。
// 商品1つ分のデータと、そのデータを守るルールを表します
class Product
{
// 商品IDは作成後に変更できません
public int Id { get; }
// 商品名は外から読み取れますが、変更はクラス内部に限定します
public string Name { get; }
// 価格は外から読み取れますが、変更はクラス内部に限定します
public decimal Price { get; }
// 在庫数は外から読み取れますが、変更はクラス内部に限定します
public int Stock { get; private set; }
// 商品情報を受け取り、インスタンスを初期化します
public Product(int id, string name, decimal price, int stock)
{
// IDは1以上にします
if (id <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(id),
"商品IDは1以上にしてください。"
);
}
// 商品名が空でないことを確認します
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException(
"商品名を入力してください。",
nameof(name)
);
}
// 価格が0円以上であることを確認します
if (price < 0)
{
throw new ArgumentOutOfRangeException(
nameof(price),
"価格は0円以上にしてください。"
);
}
// 在庫数が0以上であることを確認します
if (stock < 0)
{
throw new ArgumentOutOfRangeException(
nameof(stock),
"在庫数は0以上にしてください。"
);
}
// 確認済みの値を保存します
Id = id;
Name = name.Trim();
Price = price;
Stock = stock;
}
// 指定した個数を販売できた場合はtrueを返します
public bool Sell(int quantity)
{
// 販売数は1以上でなければなりません
if (quantity <= 0)
{
return false;
}
// 在庫が足りない場合は販売できません
if (quantity > Stock)
{
return false;
}
// 販売できる場合だけ在庫を減らします
Stock -= quantity;
return true;
}
// 商品情報を画面表示用の文字列にします
public string GetInfo()
{
return $"ID:{Id} / 商品名:{Name} / 価格:{Price}円 / 在庫:{Stock}個";
}
}C#Product は、商品の情報と在庫ルールを担当します。画面への直接表示は担当せず、商品情報を文字列として返す GetInfo() を用意しています。
ステップ2:ServiceとなるProductServiceを作る
ProductService は複数の商品を管理し、登録や検索、販売処理を担当します。商品一覧は List<Product> に保存します。
using System.Collections.Generic;
// 商品の登録・検索・販売を担当するクラスです
class ProductService
{
// 商品一覧を保存します
private readonly List<Product> _products = new List<Product>();
// 商品を一覧に追加します
public void AddProduct(Product product)
{
// 商品をリストに追加します
_products.Add(product);
}
// 登録されている商品一覧を返します
public IReadOnlyList<Product> GetAllProducts()
{
// 呼び出し側からリストの内容を変更されない形で返します
return _products.AsReadOnly();
}
// IDに一致する商品を探します。見つからなければnullを返します
public Product? FindProduct(int id)
{
// 商品を順番に調べます
foreach (Product product in _products)
{
// IDが一致した商品を返します
if (product.Id == id)
{
return product;
}
}
// 一致する商品がなければnullを返します
return null;
}
// 指定した商品を販売できた場合はtrueを返します
public bool SellProduct(int id, int quantity)
{
// まず商品を探します
Product? product = FindProduct(id);
// 商品が見つからなければ販売できません
if (product == null)
{
return false;
}
// 在庫の確認と更新はProductに任せます
return product.Sell(quantity);
}
}C#ProductService は商品一覧を持ち、IDで商品を探します。実際の在庫の増減ルールは Product の Sell() に任せています。Serviceがすべての詳細を自分で処理するのではなく、適切な役割のクラスに仕事を依頼するのがポイントです。
ステップ3:ProgramからServiceを使う
Program は、商品を作って登録し、一覧を表示し、販売を試します。商品の保管や販売のルールは、それぞれ ProductService と Product に任せます。
using System;
// コンソールアプリの開始地点です
class Program
{
static void Main()
{
// 商品を管理するServiceを作ります
ProductService productService = new ProductService();
// 商品を作成します
Product notebook = new Product(
id: 101,
name: "ノート",
price: 180m,
stock: 10
);
Product pen = new Product(
id: 102,
name: "ペン",
price: 120m,
stock: 5
);
// Serviceに商品を登録します
productService.AddProduct(notebook);
productService.AddProduct(pen);
// 商品一覧を画面に表示します
Console.WriteLine("商品一覧");
foreach (Product product in productService.GetAllProducts())
{
// Programが画面への表示を担当します
Console.WriteLine(product.GetInfo());
}
// ID 101の商品を3個販売します
bool sold = productService.SellProduct(id: 101, quantity: 3);
// 処理結果に応じてメッセージを表示します
if (sold)
{
Console.WriteLine("商品を販売しました。");
}
else
{
Console.WriteLine("販売できません。商品IDや在庫数を確認してください。");
}
// 販売後の商品一覧を表示します
Console.WriteLine();
Console.WriteLine("販売後の商品一覧");
foreach (Product product in productService.GetAllProducts())
{
Console.WriteLine(product.GetInfo());
}
}
}C#コードを実行すると、商品一覧のあとに販売結果が表示され、販売した商品の在庫数が減ります。Programは入力や表示、Serviceは商品管理の流れ、Productは個々の商品データと在庫のルールを担当しています。
処理の流れを追う
ID 101 の商品を3個販売する場合、処理は次の順に進みます。
- `Program` が `SellProduct(101, 3)` を呼び出します
- `ProductService` がID `101` の商品を探します
- 商品が見つかったら、その商品の `Sell(3)` を呼び出します
- `Product` が販売数と在庫数を確認します
- 在庫が足りれば、`Product` が在庫数を減らします
- 成功または失敗の結果が `ProductService` と `Program` に返ります
- `Program` が結果を画面に表示します
この流れを追うと、メソッドを呼び出す側と、実際の処理を担当する側が分かれていることが分かります。
なぜクラスを分けるのか
すべての処理を Main に書くと、最初は簡単に見えても、機能が増えるにつれて見通しが悪くなります。クラスを分けると、変更する場所を特定しやすくなります。
- 表示方法を変えるなら、主に `Program` を確認します
- 商品の探し方や登録方法を変えるなら、`ProductService` を確認します
- 在庫ルールを変えるなら、`Product` を確認します
役割が分かれていれば、たとえば画面表示を変えても、在庫がマイナスにならないルールを維持しやすくなります。
クラス間の依存関係を意識する
この例では、Program が ProductService を使い、ProductService が Product を使っています。つまり、上の役割から下の役割へ処理を依頼しています。
依存関係を考えるときは、次の点を意識しましょう。
- `Program` に商品の在庫を直接変更させないようにします
- `ProductService` に画面入力やコンソール表示を持ち込まないようにします
- `Product` に商品一覧全体の管理を持たせないようにします
- 一つのクラスが多くの役割を抱えないようにします
小さな学習用プログラムでは、この分担を厳密にしすぎる必要はありません。まずは「この処理はどのクラスの役割か」を考える習慣をつけましょう。
よくある間違いと注意点
- 商品一覧の管理、商品ルール、画面表示をすべて `Program` に書く
- `Product` の `Stock` を外部から直接変更できるようにする
- `ProductService` が商品名や価格、在庫のすべてのルールまで抱え込む
- 商品が見つからない場合を考えず、検索結果を使う
- 商品登録時に、同じIDが重複しないか確認しない
今回の例では、同じ商品IDを複数登録できる点は簡略化しています。実際には AddProduct() で既存IDを確認し、重複登録を防ぐ処理を追加できます。また、商品が見つからない場合と在庫が足りない場合を別々の結果として扱いたいときは、単純な bool より詳しい結果を返す設計に発展させられます。
ファイルを分ける場合
学習を進めたら、クラスごとにファイルを分けると管理しやすくなります。
- `Product.cs`:`Product` クラスを定義します
- `ProductService.cs`:`ProductService` クラスを定義します
- `Program.cs`:アプリの開始地点や画面処理を書きます
ファイルを分けても、役割やクラス同士の呼び出し方は同じです。最初は1ファイルで動きを確かめ、その後にクラスごとに分割する方法でもかまいません。
練習課題
完成例を動かしたら、次の機能追加に挑戦してみましょう。
- 商品IDの重複を防ぐ処理を追加します
- 商品を追加する `Restock()` メソッドを `Product` に作ります
- 商品が見つからない場合と、在庫不足の場合を区別して表示します
- 商品一覧をカテゴリ別に表示します
- コンソールから商品IDと販売数を入力できるメニューを作ります
機能を追加するときは、まず「どのクラスの責務か」を決めてからコードを書きます。たとえば、販売数を入力する処理は Program、対象商品の検索や販売の流れは ProductService、在庫数を減らす条件は Product が担当します。
Day 39のゴール
- `Program`、`Service`、`Model` の基本的な役割を説明できます
- モデルにデータと、そのデータを守るルールをまとめられます
- Serviceに複数のモデルを扱う処理をまとめられます
- ProgramからServiceのメソッドを呼び出せます
- 処理の成功や失敗をクラス間で受け渡せます
- 機能追加の際に、どのクラスを変更するか考えられます
まとめ
Day 39では、複数クラスの連携を通して、役割分担の基本を学びました。
- `Program` は利用者との入力・表示を担当します
- `Service` はアプリの処理の流れやモデルの管理を担当します
- `Model` はデータと、そのデータを守るルールを担当します
- クラス同士はメソッドを通じて連携します
- 一つのクラスに役割を詰め込みすぎないようにします
まずは完成例を実行し、販売処理が Program から ProductService を経由して Product に届く流れを追ってみてください。クラスごとの責務を意識できるようになると、プログラムが大きくなっても整理しやすくなります。
