C#: 演習:オブジェクト指向の総合演習

1. 銀行口座システム

目標: クラス、継承、多態性を利用して、さまざまな種類の口座を管理する銀行口座システムを構築する。

要件:

▶ サンプル

CSHARP 📖 参照専用
using System;
using System.Collections.Generic;

class Account
{
    public string Owner { get; set; }
    public decimal Balance { get; protected set; }

    public Account(string owner, decimal balance)
    {
        Owner = owner;
        Balance = balance;
    }

    public virtual void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("Deposit amount must be greater than 0");
            return;
        }
        Balance += amount;
        Console.WriteLine($"{Owner} deposited {amount:C}, balance {Balance:C}");
    }

    public virtual void Withdraw(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("Withdrawal amount must be greater than 0");
            return;
        }
        if (amount > Balance)
        {
            Console.WriteLine($"{Owner} insufficient balance, cannot withdraw {amount:C}");
            return;
        }
        Balance -= amount;
        Console.WriteLine($"{Owner} withdrew {amount:C}, balance {Balance:C}");
    }

    public virtual void DisplayInfo()
    {
        Console.WriteLine($"Account holder: {Owner}, balance: {Balance:C}");
    }
}

class SavingsAccount : Account
{
    public decimal InterestRate { get; set; }

    public SavingsAccount(string owner, decimal balance, decimal interestRate)
        : base(owner, balance)
    {
        InterestRate = interestRate;
    }

    public void ApplyInterest()
    {
        decimal interest = Balance * InterestRate;
        Balance += interest;
        Console.WriteLine($"{Owner} earned interest {interest:C}, balance {Balance:C}");
    }

    public override void Withdraw(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("Withdrawal amount must be greater than 0");
            return;
        }
        if (amount > Balance)
        {
            Console.WriteLine($"Savings account cannot overdraft, balance {Balance:C}, cannot withdraw {amount:C}");
            return;
        }
        Balance -= amount;
        Console.WriteLine($"{Owner}(savings) withdrew {amount:C}, balance {Balance:C}");
    }

    public override void DisplayInfo()
    {
        Console.WriteLine($"[Savings Account] holder: {Owner}, balance: {Balance:C}, interest rate: {InterestRate:P}");
    }
}

class CheckingAccount : Account
{
    public decimal OverdraftLimit { get; set; }

    public CheckingAccount(string owner, decimal balance, decimal overdraftLimit)
        : base(owner, balance)
    {
        OverdraftLimit = overdraftLimit;
    }

    public override void Withdraw(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("Withdrawal amount must be greater than 0");
            return;
        }
        if (amount > Balance + OverdraftLimit)
        {
            Console.WriteLine($"Exceeds overdraft limit, available {Balance + OverdraftLimit:C}, cannot withdraw {amount:C}");
            return;
        }
        Balance -= amount;
        Console.WriteLine($"{Owner}(checking) withdrew {amount:C}, balance {Balance:C}, overdraft limit {OverdraftLimit:C}");
    }

    public override void DisplayInfo()
    {
        Console.WriteLine($"[Checking Account] holder: {Owner}, balance: {Balance:C}, overdraft limit: {OverdraftLimit:C}");
    }
}

class Program
{
    static void Main()
    {
        List<Account> accounts = new List<Account>
        {
            new SavingsAccount("Zhang San", 10000m, 0.03m),
            new CheckingAccount("Li Si", 5000m, 2000m)
        };

        Console.WriteLine("=== Polymorphic Withdrawal Demo ===");
        foreach (Account acc in accounts)
        {
            acc.Withdraw(6000m);
        }

        Console.WriteLine();
        Console.WriteLine("=== Deposit & Interest ===");
        accounts[0].Deposit(2000m);
        ((SavingsAccount)accounts[0]).ApplyInterest();

        Console.WriteLine();
        Console.WriteLine("=== Account Info ===");
        foreach (Account acc in accounts)
        {
            acc.DisplayInfo();
        }
    }
}
論理コード 129 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
=== Polymorphic Withdrawal Demo ===
Savings account cannot overdraft, balance ¥10,000.00, cannot withdraw ¥6,000.00
Li Si(checking) withdrew ¥6,000.00, balance ¥-1,000.00, overdraft limit ¥2,000.00

=== Deposit & Interest ===
Zhang San deposited ¥2,000.00, balance ¥12,000.00
Zhang San earned interest ¥360.00, balance ¥12,360.00

=== Account Info ===
[Savings Account] holder: Zhang San, balance: ¥12,360.00, interest rate: 3.00%
[Checking Account] holder: Li Si, balance: ¥-1,000.00, overdraft limit: ¥2,000.00

2. 図形の面積計算ツール

目標: 抽象クラスとインターフェースを用いて、複数の図形の面積計算を実装し、面積順の並べ替えに対応する。

要件:

▶ サンプル

CSHARP 📖 参照専用
using System;
using System.Collections.Generic;

abstract class Shape : IComparable<Shape>
{
    public abstract double Area();

    public int CompareTo(Shape other)
    {
        if (other == null) return 1;
        return Area().CompareTo(other.Area());
    }

    public override string ToString()
    {
        return $"{GetType().Name} - area: {Area():F2}";
    }
}

class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area()
    {
        return Math.PI * Radius * Radius;
    }

    public override string ToString()
    {
        return $"Circle(radius={Radius}) - area: {Area():F2}";
    }
}

class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double Area()
    {
        return Width * Height;
    }

    public override string ToString()
    {
        return $"Rectangle({Width}x{Height}) - area: {Area():F2}";
    }
}

class Program
{
    static void Main()
    {
        List<Shape> shapes = new List<Shape>
        {
            new Circle(5),
            new Rectangle(4, 6),
            new Circle(2),
            new Rectangle(10, 3),
            new Circle(3.5)
        };

        Console.WriteLine("=== Before Sorting ===");
        foreach (Shape s in shapes)
        {
            Console.WriteLine(s);
        }

        shapes.Sort();

        Console.WriteLine();
        Console.WriteLine("=== Sorted by Area ===");
        foreach (Shape s in shapes)
        {
            Console.WriteLine(s);
        }

        Console.WriteLine();
        Console.WriteLine($"Total area: {TotalArea(shapes):F2}");
    }

    static double TotalArea(List<Shape> shapes)
    {
        double total = 0;
        foreach (Shape s in shapes)
        {
            total += s.Area();
        }
        return total;
    }
}
論理コード 86 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
=== Before Sorting ===
Circle(radius=5) - area: 78.54
Rectangle(4x6) - area: 24.00
Circle(radius=2) - area: 12.57
Rectangle(10x3) - area: 30.00
Circle(radius=3.5) - area: 38.48

=== Sorted by Area ===
Circle(radius=2) - area: 12.57
Rectangle(4x6) - area: 24.00
Rectangle(10x3) - area: 30.00
Circle(radius=3.5) - area: 38.48
Circle(radius=5) - area: 78.54

Total area: 183.59

3. シンプルな在庫管理システム

目標: 汎用コレクションとイベントを使用して、在庫が閾値を下回った際に「在庫不足」イベントをトリガーする在庫管理システムを構築する。

要件:

▶ サンプル

CSHARP 📖 参照専用
using System;
using System.Collections.Generic;

class Product
{
    public string Name { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public int LowStockThreshold { get; set; }

    public Product(string name, int quantity, decimal price, int lowStockThreshold)
    {
        Name = name;
        Quantity = quantity;
        Price = price;
        LowStockThreshold = lowStockThreshold;
    }

    public bool IsLowStock()
    {
        return Quantity < LowStockThreshold;
    }

    public override string ToString()
    {
        return $"{Name} - quantity: {Quantity}, unit price: {Price:C}, threshold: {LowStockThreshold}";
    }
}

class LowStockEventArgs : EventArgs
{
    public Product Product { get; set; }
    public int CurrentQuantity { get; set; }

    public LowStockEventArgs(Product product, int currentQuantity)
    {
        Product = product;
        CurrentQuantity = currentQuantity;
    }
}

class Inventory
{
    private List```<Product>``` products = new List```<Product>```();

    public event EventHandler<LowStockEventArgs> LowStockAlert;

    public void Add(Product product)
    {
        Product existing = FindByName(product.Name);
        if (existing != null)
        {
            existing.Quantity += product.Quantity;
            Console.WriteLine($"Product {product.Name} already exists, quantity increased to {existing.Quantity}");
        }
        else
        {
            products.Add(product);
            Console.WriteLine($"Added product: {product.Name}, quantity: {product.Quantity}");
        }
        CheckLowStock(product);
    }

    public void Remove(string name, int quantity)
    {
        Product product = FindByName(name);
        if (product == null)
        {
            Console.WriteLine($"Product {name} does not exist");
            return;
        }
        if (quantity > product.Quantity)
        {
            Console.WriteLine($"Insufficient stock for {name}, current quantity: {product.Quantity}, requested removal: {quantity}");
            return;
        }
        product.Quantity -= quantity;
        Console.WriteLine($"Removed {quantity} of {name}, remaining {product.Quantity}");
        CheckLowStock(product);
    }

    public Product FindByName(string name)
    {
        foreach (Product p in products)
        {
            if (p.Name == name)
            {
                return p;
            }
        }
        return null;
    }

    public void DisplayAll()
    {
        Console.WriteLine("=== Inventory List ===");
        foreach (Product p in products)
        {
            string warning = p.IsLowStock() ? " ⚠️Low stock" : "";
            Console.WriteLine($"{p}{warning}");
        }
        Console.WriteLine($"Product types: {products.Count}");
    }

    protected virtual void OnLowStock(LowStockEventArgs e)
    {
        LowStockAlert?.Invoke(this, e);
    }

    private void CheckLowStock(Product product)
    {
        if (product.IsLowStock())
        {
            OnLowStock(new LowStockEventArgs(product, product.Quantity));
        }
    }
}

class Program
{
    static void Main()
    {
        Inventory inventory = new Inventory();
        inventory.LowStockAlert += OnLowStock;

        inventory.Add(new Product("Keyboard", 50, 299m, 10));
        inventory.Add(new Product("Mouse", 30, 99m, 10));
        inventory.Add(new Product("Monitor", 5, 1999m, 10));

        Console.WriteLine();
        inventory.Remove("Monitor", 2);
        inventory.Remove("Keyboard", 45);

        Console.WriteLine();
        inventory.Add(new Product("Keyboard", 3, 299m, 10));

        Console.WriteLine();
        Product found = inventory.FindByName("Mouse");
        if (found != null)
        {
            Console.WriteLine($"Search result: {found}");
        }

        Console.WriteLine();
        inventory.DisplayAll();
    }

    static void OnLowStock(object sender, LowStockEventArgs e)
    {
        Console.WriteLine($"💡 [Low Stock Alert] {e.Product.Name} current quantity {e.CurrentQuantity}, below threshold {e.Product.LowStockThreshold}");
    }
}
論理コード 131 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
Added product: Keyboard, quantity: 50
Added product: Mouse, quantity: 30
Added product: Monitor, quantity: 5
💡 [Low Stock Alert] Monitor current quantity 5, below threshold 10

Removed 2 of Monitor, remaining 3
💡 [Low Stock Alert] Monitor current quantity 3, below threshold 10
Removed 45 of Keyboard, remaining 5
💡 [Low Stock Alert] Keyboard current quantity 5, below threshold 10

Product Keyboard already exists, quantity increased to 8
💡 [Low Stock Alert] Keyboard current quantity 8, below threshold 10

Search result: Mouse - quantity: 30, unit price: ¥99.00, threshold: 10

=== Inventory List ===
Keyboard - quantity: 8, unit price: ¥299.00, threshold: 10 ⚠️Low stock
Mouse - quantity: 30, unit price: ¥99.00, threshold: 10
Monitor - quantity: 3, unit price: ¥1,999.00, threshold: 10 ⚠️Low stock
Product types: 3

❓ よくある質問

Q なぜ普通預金口座では引き出し時に残高不足になることはないのですか?
A 普通預金口座の設計理念は、保守的な資金管理にあります。オーバーライドされた「Withdraw」メソッドには、残高を超える引き出し要求を拒否する残高チェックが追加されています。
Q 抽象クラスとインターフェース、どちらを選ぶべきですか?
A 共通のフィールドやデフォルトの実装がある場合は、抽象クラス(ShapeクラスのCompareToなど)を使用してください。動作の契約を定義するだけでよい場合は、インターフェース(IComparable<T>など)を使用してください。 両方を併用することも可能です。
Q イベントとデリゲートの違いは何ですか?
A イベントとは、デリゲートをカプセル化したものです。外部コードからは += や -= を通じてのみ登録・解除が可能であり、直接呼び出すことはできないため、より安全なパブリッシュ・サブスクライブ・パターンを実現しています。

📖 まとめ

📝 練習問題

  1. 銀行口座システムに、FixedDepositAccountクラスを追加し、定期預金の期間を設定するとともに、早期解約時には違約金を課すようにする
  2. 図形面積計算機にTriangleクラスを追加し、面積計算を実装してソートに参加させる
  3. 在庫管理システムに、商品が追加された際にトリガーされ、購入ログを記録するPurchaseイベントを追加する
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%