C# abstractの完全ガイド:使い方と実践パターン7選

はじめに

C#における抽象クラスは、オブジェクト指向設計の要となる重要な機能です。適切に活用することで、コードの再利用性を高め、保守性の優れたアプリケーションを構築することができます。
本記事では、抽象クラスの基本から実践的な活用パターンまでを、具体的なコード例を交えて解説します。

本記事で学べること

抽象クラスの基本的な概念と使用方法

インターフェースとの違いと適切な使い分け

7つの実践的な設計パターンでの活用方法

テストしやすく保守性の高い抽象クラスの設計手法

よくあるアンチパターンとその改善方法

実際のプロジェクトにおける具体的な実装例

C#の抽象クラスとは:基礎から理解する

抽象クラスが解決する3つの設計上の課題

抽象クラス(Abstract Class)は、オブジェクト指向プログラミングにおける重要な設計要素です。以下の3つの主要な設計上の課題を解決します。

1. コードの重複防止

継承を通じて共通の実装を提供することで、サブクラス間でのコードの重複を防ぎます。

2. 設計の一貫性確保

抽象メソッドを定義することで、サブクラスに特定の機能の実装を強制し、設計の一貫性を保証します。

3. 実装の柔軟性向上

共通の基底クラスとして機能しながら、具象クラスごとに特殊な振る舞いを定義できます。

abstractキーワードの基本的な使い方

クラスでの使用

// 抽象クラスの基本的な定義
public abstract class Animal
{
    // 通常のプロパティ
    public string Name { get; set; }

    // 通常のメソッド(具象メソッド)
    public void Sleep()
    {
        Console.WriteLine($"{Name} is sleeping...");
    }

    // 抽象メソッド(実装は派生クラスで必須)
    public abstract void MakeSound();

    // 仮想メソッド(オーバーライド可能だが任意)
    public virtual void Move()
    {
        Console.WriteLine($"{Name} is moving...");
    }
}

// 抽象クラスを継承した具象クラス
public class Dog : Animal
{
    // 抽象メソッドの実装(必須)
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }

    // 仮想メソッドのオーバーライド(任意)
    public override void Move()
    {
        Console.WriteLine($"{Name} is running on four legs!");
    }
}

1. インスタンス化の制限

// これはコンパイルエラー
Animal animal = new Animal(); // 抽象クラスは直接インスタンス化できない

// これは正しい使用法
Animal dog = new Dog(); // 具象クラスを通じて利用

2. メンバーの可視性

public abstract class Example
{
    // 抽象クラスは通常のメンバーも持てる
    private int privateField;
    protected string protectedProperty { get; set; }
    public void PublicMethod() { }

    // 抽象メンバーは暗黙的にvirtual
    public abstract void AbstractMethod();
}

3. 抽象メソッドの制約

public abstract class BaseClass
{
    // 抽象メソッドは実装を持てない
    public abstract void Method();

    // これは許可されない
    // public abstract void InvalidMethod() { }
}

データアクセス層での実践的な使用例

public abstract class RepositoryBase<T> where T : class
{
    protected readonly DbContext _context;

    protected RepositoryBase(DbContext context)
    {
        _context = context;
    }

    // 共通の実装を提供
    public virtual async Task<T> GetByIdAsync(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }

    // 具象クラスで実装すべき特殊な操作を定義
    public abstract Task<IEnumerable<T>> GetByConditionAsync(
        Expression<Func<T, bool>> condition);
}

// 具体的な実装例
public class UserRepository : RepositoryBase<User>
{
    public UserRepository(DbContext context) : base(context) { }

    public override async Task<IEnumerable<User>> GetByConditionAsync(
        Expression<Func<User, bool>> condition)
    {
        return await _context.Set<User>()
            .Where(condition)
            .ToListAsync();
    }
}

これらの例は、抽象クラスの基本的な使用方法から実践的なパターンまでをカバーしています。
適切に使用することで、コードの再利用性、保守性、拡張性を大きく向上させることができます。

抽象クラスとインターフェースの違いを徹底解説

使い分けの3つの黄金ルール

1. IS-A 関係 vs CAN-DO 関係

抽象クラスとインターフェースの最も基本的な使い分けは、以下の関係性に基づきます。

// IS-A関係の例(抽象クラス)
public abstract class Vehicle
{
    public string Model { get; set; }
    public abstract void Start();
}

public class Car : Vehicle  // 車は乗り物である(IS-A)
{
    public override void Start()
    {
        Console.WriteLine("エンジン始動");
    }
}

// CAN-DO関係の例(インターフェース)
public interface IPayable
{
    decimal CalculatePayment();
}

public class Employee : IPayable  // 従業員は支払い可能である(CAN-DO)
{
    public decimal CalculatePayment()
    {
        return BaseSalary + Bonus;
    }
}

2. 実装の共有 vs 契約の定義

// 実装の共有(抽象クラス)
public abstract class DataProcessorBase
{
    // 共通の実装を提供
    protected void ValidateData(string data)
    {
        if (string.IsNullOrEmpty(data))
            throw new ArgumentException("データが空です");
    }

    // 抽象メソッド
    public abstract void ProcessData(string data);
}

// 契約の定義(インターフェース)
public interface IDataProcessor
{
    void ProcessData(string data);
    // 実装は提供せず、必要なメソッドのみを定義
}

3. 単一継承 vs 多重実装

// 抽象クラスは単一継承のみ
public abstract class Animal { }
public abstract class Mammal : Animal { }
// public class Dog : Animal, Mammal { } // エラー:多重継承不可

// インターフェースは多重実装可能
public interface ISwimmable { void Swim(); }
public interface IFlyable { void Fly(); }
public class Duck : Animal, ISwimmable, IFlyable
{
    public void Swim() { /* 実装 */ }
    public void Fly() { /* 実装 */ }
}

パフォーマンスの観点から見た特徴

メモリ使用量

  1. 抽象クラス
    • インスタンスフィールドを持てる
    • 状態を保持できるため、メモリ使用量が大きくなる可能性がある
  2. インターフェース
    • フィールドは持てない(C# 8.0からのデフォルト実装を除く)
    • メモリ効率が良い
public abstract class CacheBase
{
    protected Dictionary<string, object> _cache = new();  // メモリを消費
    public abstract void Add(string key, object value);
}

public interface ICache
{
    void Add(string key, object value);  // メモリオーバーヘッドなし
}

メソッド呼び出し時のオーバーヘッドとパフォーマンス比較

// 抽象クラスの仮想メソッド呼び出し
public abstract class Logger
{
    public virtual void Log(string message)  // 仮想テーブルを通じた呼び出し
    {
        Console.WriteLine(message);
    }
}

// インターフェースのメソッド呼び出し
public interface ILogger
{
    void Log(string message);  // インターフェーステーブルを通じた呼び出し
}

// パフォーマンステスト用コード
public class PerformanceTest
{
    private const int Iterations = 1000000;

    public void TestAbstractClass()
    {
        Logger logger = new ConsoleLogger();
        for (int i = 0; i < Iterations; i++)
        {
            logger.Log("test");  // 仮想メソッド呼び出し
        }
    }

    public void TestInterface()
    {
        ILogger logger = new ConsoleLogger();
        for (int i = 0; i < Iterations; i++)
        {
            logger.Log("test");  // インターフェースメソッド呼び出し
        }
    }
}

実際のパフォーマンスの違いは以下の通りです。

  • 抽象クラスの仮想メソッド呼び出し:わずかに高速
  • インターフェースメソッド呼び出し:若干のオーバーヘッド
  • ただし、現代のハードウェアではその差はほとんど無視できるレベル

これらの特徴を理解した上で、アプリケーションの要件に応じて適切な選択を行うことが重要です。
パフォーマンスよりも設計の柔軟性や保守性を重視する場合が多いことにも注意が必要です。

実践的な抽象クラスの設計パターン7選

テンプレートメソッドパターンによる処理の共通化

テンプレートメソッドパターンは、アルゴリズムの骨格を抽象クラスで定義し、具体的な実装を子クラスに委ねるパターンです。

public abstract class DataExporter
{
    // テンプレートメソッド
    public void Export()
    {
        ValidateData();
        PreProcess();
        ProcessData();
        PostProcess();
        Notify();
    }

    // 共通の実装
    private void ValidateData()
    {
        Console.WriteLine("データの検証中...");
    }

    // フックメソッド(オプショナルな実装)
    protected virtual void PreProcess() { }
    protected virtual void PostProcess() { }

    // 抽象メソッド(必須の実装)
    protected abstract void ProcessData();
    protected abstract void Notify();
}

public class CsvExporter : DataExporter
{
    protected override void ProcessData()
    {
        Console.WriteLine("CSVファイルにデータを出力中...");
    }

    protected override void Notify()
    {
        Console.WriteLine("CSV出力完了を通知");
    }
}

ファクトリーメソッドパターンでの活用法

ファクトリーメソッドパターンは、オブジェクト生成のインターフェースを抽象クラスで定義し、具体的な生成プロセスをサブクラスで実装します。

public interface IDocument
{
    void Open();
    void Process();
    void Save();
}

public abstract class DocumentFactory
{
    // ファクトリーメソッド
    public abstract IDocument CreateDocument();

    // 共通の処理
    public void ProcessDocument()
    {
        var document = CreateDocument();
        document.Open();
        document.Process();
        document.Save();
    }
}

public class PdfFactory : DocumentFactory
{
    public override IDocument CreateDocument()
    {
        return new PdfDocument();
    }
}

ストラテジーパターンとの組み合わせ

ストラテジーパターンと抽象クラスを組み合わせることで、アルゴリズムの共通部分を抽象クラスで定義しつつ、可変部分を柔軟に切り替えることができます。

public interface ICompressionStrategy
{
    byte[] Compress(byte[] data);
}

public abstract class CompressionHandler
{
    protected readonly ICompressionStrategy _strategy;

    protected CompressionHandler(ICompressionStrategy strategy)
    {
        _strategy = strategy;
    }

    // 共通処理
    public virtual void HandleCompression(byte[] data)
    {
        ValidateData(data);
        var compressed = _strategy.Compress(data);
        SaveCompressedData(compressed);
    }

    protected abstract void SaveCompressedData(byte[] compressedData);
    protected abstract void ValidateData(byte[] data);
}

デコレーターパターンでの実装例

デコレーターパターンを使用して、基本的な機能を持つコンポーネントに対して、動的に新しい機能を追加できます。

public abstract class LoggerDecorator : ILogger
{
    protected readonly ILogger _logger;

    public LoggerDecorator(ILogger logger)
    {
        _logger = logger;
    }

    public abstract void Log(string message);
}

public class TimestampDecorator : LoggerDecorator
{
    public TimestampDecorator(ILogger logger) : base(logger) { }

    public override void Log(string message)
    {
        var timestampedMessage = $"[{DateTime.Now}] {message}";
        _logger.Log(timestampedMessage);
    }
}

コンポジットパターンでの応用

コンポジットパターンを使用して、個々のオブジェクトと複合オブジェクトを同じように扱える構造を作成できます。

public abstract class FileSystemItem
{
    public string Name { get; protected set; }
    public abstract long GetSize();
    public abstract void Print(string indent = "");
}

public class File : FileSystemItem
{
    private long _size;

    public File(string name, long size)
    {
        Name = name;
        _size = size;
    }

    public override long GetSize() => _size;

    public override void Print(string indent = "")
    {
        Console.WriteLine($"{indent}File: {Name} ({GetSize()} bytes)");
    }
}

public class Directory : FileSystemItem
{
    private List<FileSystemItem> _items = new();

    public Directory(string name)
    {
        Name = name;
    }

    public void Add(FileSystemItem item)
    {
        _items.Add(item);
    }

    public override long GetSize()
        => _items.Sum(item => item.GetSize());

    public override void Print(string indent = "")
    {
        Console.WriteLine($"{indent}Directory: {Name}");
        foreach (var item in _items)
        {
            item.Print(indent + "  ");
        }
    }
}

ブリッジパターンでの実装方法

ブリッジパターンを使用して、抽象化と実装を分離し、それぞれを独立して変更できるようにします。

// 実装の抽象化
public interface IMessageSender
{
    void SendMessage(string message);
}

// 抽象化
public abstract class Message
{
    protected IMessageSender _sender;

    protected Message(IMessageSender sender)
    {
        _sender = sender;
    }

    public abstract void Send();
}

// 具体的な抽象化
public class UserMessage : Message
{
    private string _userName;
    private string _content;

    public UserMessage(
        string userName, 
        string content, 
        IMessageSender sender) : base(sender)
    {
        _userName = userName;
        _content = content;
    }

    public override void Send()
    {
        string formattedMessage = 
            $"User {_userName} says: {_content}";
        _sender.SendMessage(formattedMessage);
    }
}

オブザーバーパターンでの活用術

オブザーバーパターンを使用して、オブジェクト間の1対多の依存関係を定義し、あるオブジェクトの状態が変化した時に依存するオブジェクトに自動的に通知します。

public interface IObserver
{
    void Update(string message);
}

public abstract class Observable
{
    private List<IObserver> _observers = new();

    public void Attach(IObserver observer)
    {
        _observers.Add(observer);
    }

    public void Detach(IObserver observer)
    {
        _observers.Remove(observer);
    }

    protected void NotifyObservers(string message)
    {
        foreach (var observer in _observers)
        {
            observer.Update(message);
        }
    }
}

public class WeatherStation : Observable
{
    private float _temperature;

    public float Temperature
    {
        get => _temperature;
        set
        {
            if (_temperature != value)
            {
                _temperature = value;
                NotifyObservers($"温度が{value}度に変更されました");
            }
        }
    }
}

これらのデザインパターンは、抽象クラスの特性を活かして、コードの再利用性、保守性、拡張性を高めることができます。
実際のプロジェクトでは、要件に応じて適切なパターンを選択するか、複数のパターンを組み合わせて使用することが重要です。

C# abstractクラスのベストプラクティス

命名規則とコーディング規約

抽象クラスの命名と実装には、以下のベストプラクティスを適用します。

1. クラス名の規則

// 推奨される命名パターン
public abstract class ServiceBase { }      // "Base" サフィックス
public abstract class AbstractService { }   // "Abstract" プレフィックス
public abstract class ServiceTemplate { }   // "Template" サフィックス

// 避けるべき命名パターン
public abstract class BaseService { }      // "Base" をプレフィックスにすることは非推奨
public abstract class AbsService { }       // 略語の使用は非推奨

2. メンバーの命名規則

public abstract class DataAccessBase
{
    // 抽象メソッドは動詞または動詞句で始める
    public abstract Task<T> RetrieveDataAsync<T>();

    // 保護されたフィールドには '_' プレフィックスを使用
    protected readonly ILogger _logger;

    // プロパティは名詞または名詞句
    protected abstract IConfiguration Configuration { get; }
}

テストしやすい抽象クラスの設計方法

テスタビリティを考慮した抽象クラスの設計には、以下の原則を適用します。

// テスト容易性を考慮した設計例
public abstract class OrderProcessorBase
{
    // 依存性を明示的に注入
    protected readonly ILogger _logger;
    protected readonly IOrderValidator _validator;

    protected OrderProcessorBase(
        ILogger logger,
        IOrderValidator validator)
    {
        _logger = logger;
        _validator = validator;
    }

    // 保護された仮想メソッドでテスト可能な単位を提供
    protected virtual bool ValidateOrder(Order order)
    {
        return _validator.Validate(order);
    }

    // 抽象メソッドは小さな単位に分割
    protected abstract Task<decimal> CalculateTotalAsync(Order order);
    protected abstract Task<bool> ReserveInventoryAsync(Order order);

    // パブリックメソッドで処理フローを制御
    public async Task<bool> ProcessOrderAsync(Order order)
    {
        try
        {
            if (!ValidateOrder(order))
            {
                _logger.LogError("Order validation failed");
                return false;
            }

            var total = await CalculateTotalAsync(order);
            var reserved = await ReserveInventoryAsync(order);

            return reserved;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Order processing failed");
            return false;
        }
    }
}

// テストクラスの例
public class OrderProcessorTests
{
    private class TestOrderProcessor : OrderProcessorBase
    {
        public TestOrderProcessor(
            ILogger logger,
            IOrderValidator validator)
            : base(logger, validator) { }

        protected override Task<decimal> CalculateTotalAsync(Order order)
            => Task.FromResult(100m);

        protected override Task<bool> ReserveInventoryAsync(Order order)
            => Task.FromResult(true);
    }

    [Fact]
    public async Task ProcessOrder_WithValidOrder_ReturnsTrue()
    {
        // Arrange
        var logger = Mock.Of<ILogger>();
        var validator = Mock.Of<IOrderValidator>(v =>
            v.Validate(It.IsAny<Order>()) == true);

        var processor = new TestOrderProcessor(logger, validator);

        // Act
        var result = await processor.ProcessOrderAsync(new Order());

        // Assert
        Assert.True(result);
    }
}

依存性注入を考慮した実装のコツ

1. コンストラクタインジェクション

public abstract class RepositoryBase<T> where T : class
{
    protected readonly DbContext _context;
    protected readonly ILogger<RepositoryBase<T>> _logger;
    protected readonly IMapper _mapper;

    protected RepositoryBase(
        DbContext context,
        ILogger<RepositoryBase<T>> logger,
        IMapper mapper)
    {
        _context = context;
        _logger = logger;
        _mapper = mapper;
    }
}

// 実装クラス
public class UserRepository : RepositoryBase<User>
{
    public UserRepository(
        DbContext context,
        ILogger<RepositoryBase<User>> logger,
        IMapper mapper)
        : base(context, logger, mapper)
    {
    }
}

2. オプショナルな依存性の処理

public abstract class CacheableServiceBase
{
    protected readonly ICache _cache;
    protected readonly bool _isCachingEnabled;

    protected CacheableServiceBase(ICache cache = null)
    {
        _cache = cache;
        _isCachingEnabled = cache != null;
    }

    protected async Task<T> GetWithCachingAsync<T>(
        string key,
        Func<Task<T>> dataProvider,
        TimeSpan? cacheExpiration = null)
    {
        if (!_isCachingEnabled)
            return await dataProvider();

        if (await _cache.TryGetAsync<T>(key, out var cachedData))
            return cachedData;

        var data = await dataProvider();
        await _cache.SetAsync(key, data, cacheExpiration);
        return data;
    }
}

3. サービスロケーターの適切な使用

public abstract class BackgroundServiceBase : IHostedService
{
    private readonly IServiceProvider _serviceProvider;

    protected BackgroundServiceBase(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected async Task ExecuteWithScopedServicesAsync(
        Func<IServiceProvider, Task> action)
    {
        using var scope = _serviceProvider.CreateScope();
        await action(scope.ServiceProvider);
    }

    public abstract Task StartAsync(CancellationToken cancellationToken);
    public abstract Task StopAsync(CancellationToken cancellationToken);
}

これらのベストプラクティスを適用することで、保守性が高く、テスト可能で、依存性が適切に管理された抽象クラスを設計することができます。

よくあるアンチパターンと解決策

過度な抽象化を避けるための3つの指針

1. 具体的なニーズに基づいた抽象化

// アンチパターン
// 過度に抽象化された例
public abstract class EntityBase
{
    public abstract int Id { get; set; }
    public abstract string Name { get; set; }
    public abstract DateTime CreatedAt { get; set; }
    public abstract DateTime? UpdatedAt { get; set; }
    public abstract bool IsActive { get; set; }
    // ... 多数の抽象プロパティ
}

// 改善案
// 必要な抽象化のみを提供
public abstract class EntityBase
{
    public int Id { get; set; }  // 共通の実装
    public DateTime CreatedAt { get; set; }

    // 本当に異なる実装が必要なもののみを抽象化
    public abstract string GetDisplayName();
}

2. 「将来のため」の過剰な抽象化を避ける

// アンチパターン
// 「いつか必要になるかも」という理由での抽象化
public abstract class ServiceBase
{
    public abstract Task<T> ExecuteAsync<T>();
    public abstract Task<T> ExecuteWithRetryAsync<T>();
    public abstract Task<T> ExecuteWithTimeoutAsync<T>();
    public abstract Task<T> ExecuteWithCachingAsync<T>();
    // ... 使われないかもしれない多数のメソッド
}

// 改善案
// 実際のニーズに基づいた設計
public abstract class ServiceBase
{
    public abstract Task<T> ExecuteAsync<T>();

    // 共通機能は拡張メソッドとして提供
    public static class ServiceExtensions
    {
        public static async Task<T> WithRetry<T>(
            this ServiceBase service,
            Func<Task<T>> operation)
        {
            // 必要になった時点で実装
        }
    }
}

3. インターフェースで十分な場合は抽象クラスを避ける

// アンチパターン
// 不必要な抽象クラス
public abstract class IValidator<T>
{
    public abstract bool Validate(T entity);
    public abstract IEnumerable<string> GetErrors();
}

// 改善案
// シンプルなインターフェース
public interface IValidator<T>
{
    bool Validate(T entity);
    IEnumerable<string> GetErrors();
}

継承の深さに関する設計判断

継承の深さを制限する方法

// アンチパターン
// 深い継承階層
public abstract class Entity { }
public abstract class ValidatableEntity : Entity { }
public abstract class AuditableEntity : ValidatableEntity { }
public abstract class SoftDeletableEntity : AuditableEntity { }
public class Customer : SoftDeletableEntity { }

// 改善案
// コンポジションを使用した設計
public class Entity
{
    private readonly IValidator _validator;
    private readonly IAuditTracker _auditTracker;
    private readonly ISoftDelete _softDelete;

    protected Entity(
        IValidator validator,
        IAuditTracker auditTracker,
        ISoftDelete softDelete)
    {
        _validator = validator;
        _auditTracker = auditTracker;
        _softDelete = softDelete;
    }
}

public class Customer : Entity
{
    public Customer() : base(
        new CustomerValidator(),
        new AuditTracker(),
        new SoftDeleteBehavior())
    {
    }
}

コードの重複を防ぐリファクタリング手法

1. 共通コードの抽出

// アンチパターン
public class UserService
{
    private void LogError(Exception ex)
    {
        Console.WriteLine($"[ERROR] {DateTime.Now}: {ex.Message}");
        // エラー処理の重複
    }
}

public class OrderService
{
    private void LogError(Exception ex)
    {
        Console.WriteLine($"[ERROR] {DateTime.Now}: {ex.Message}");
        // 同じエラー処理の重複
    }
}

// 改善案
public abstract class ServiceBase
{
    protected readonly ILogger _logger;

    protected ServiceBase(ILogger logger)
    {
        _logger = logger;
    }

    protected virtual void HandleException(Exception ex)
    {
        _logger.LogError(ex, ex.Message);
    }
}

public class UserService : ServiceBase
{
    public UserService(ILogger<UserService> logger) : base(logger) { }
}

public class OrderService : ServiceBase
{
    public OrderService(ILogger<OrderService> logger) : base(logger) { }
}

2. テンプレートメソッドの活用

// アンチパターン
public class PdfReport
{
    public void Generate()
    {
        ValidateData();
        PrepareData();
        CreatePdfDocument();
        SaveDocument();
    }
}

public class ExcelReport
{
    public void Generate()
    {
        ValidateData();
        PrepareData();
        CreateExcelDocument();
        SaveDocument();
    }
}

// 改善案
public abstract class ReportBase
{
    public void Generate()
    {
        ValidateData();
        PrepareData();
        CreateDocument();
        SaveDocument();
    }

    protected abstract void CreateDocument();

    private void ValidateData() { /* 共通の検証ロジック */ }
    private void PrepareData() { /* 共通のデータ準備 */ }
    private void SaveDocument() { /* 共通の保存ロジック */ }
}

public class PdfReport : ReportBase
{
    protected override void CreateDocument()
    {
        // PDF固有の生成ロジック
    }
}

これらのアンチパターンを認識し、適切な解決策を適用することで、より保守性の高い、理解しやすいコードを作成することができます。

実際のプロジェクトでの活用例

Webアプリケーションでのサービス層の実装

サービス層の抽象化

public abstract class ServiceBase<TEntity, TDto>
    where TEntity : class
    where TDto : class
{
    protected readonly IRepository<TEntity> _repository;
    protected readonly IMapper _mapper;
    protected readonly ILogger _logger;
    protected readonly IValidator<TDto> _validator;

    protected ServiceBase(
        IRepository<TEntity> repository,
        IMapper mapper,
        ILogger logger,
        IValidator<TDto> validator)
    {
        _repository = repository;
        _mapper = mapper;
        _logger = logger;
        _validator = validator;
    }

    public virtual async Task<TDto> GetByIdAsync(int id)
    {
        try
        {
            var entity = await _repository.GetByIdAsync(id);
            return _mapper.Map<TDto>(entity);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error retrieving entity with id {id}");
            throw;
        }
    }

    public virtual async Task<TDto> CreateAsync(TDto dto)
    {
        if (!_validator.Validate(dto))
            throw new ValidationException(_validator.GetErrors());

        try
        {
            var entity = _mapper.Map<TEntity>(dto);
            var created = await _repository.CreateAsync(entity);
            return _mapper.Map<TDto>(created);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating entity");
            throw;
        }
    }
}

// 具体的な実装例
public class UserService : ServiceBase<User, UserDto>
{
    public UserService(
        IRepository<User> repository,
        IMapper mapper,
        ILogger<UserService> logger,
        IValidator<UserDto> validator)
        : base(repository, mapper, logger, validator)
    {
    }

    // ユーザー固有の追加機能
    public async Task<bool> ChangePasswordAsync(
        int userId, 
        string newPassword)
    {
        var user = await _repository.GetByIdAsync(userId);
        user.PasswordHash = HashPassword(newPassword);
        await _repository.UpdateAsync(user);
        return true;
    }
}

データアクセス層での抽象化パターン

リポジトリパターンの実装

public abstract class RepositoryBase<T> where T : class
{
    protected readonly ApplicationDbContext _context;
    protected readonly DbSet<T> _dbSet;
    protected readonly ILogger _logger;

    protected RepositoryBase(
        ApplicationDbContext context,
        ILogger logger)
    {
        _context = context;
        _dbSet = context.Set<T>();
        _logger = logger;
    }

    public virtual async Task<IEnumerable<T>> GetAllAsync(
        Expression<Func<T, bool>> filter = null,
        Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
        string includeProperties = "")
    {
        try
        {
            IQueryable<T> query = _dbSet;

            if (filter != null)
                query = query.Where(filter);

            foreach (var includeProperty in includeProperties
                .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
                return await orderBy(query).ToListAsync();

            return await query.ToListAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in GetAllAsync");
            throw;
        }
    }

    public virtual async Task<T> GetByIdAsync(int id)
    {
        try
        {
            return await _dbSet.FindAsync(id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error getting entity with id {id}");
            throw;
        }
    }
}

// 具体的な実装例
public class ProductRepository : RepositoryBase<Product>
{
    public ProductRepository(
        ApplicationDbContext context,
        ILogger<ProductRepository> logger)
        : base(context, logger)
    {
    }

    public async Task<IEnumerable<Product>> GetActiveProductsAsync()
    {
        return await GetAllAsync(
            filter: p => p.IsActive,
            orderBy: q => q.OrderByDescending(p => p.CreatedAt),
            includeProperties: "Category,Supplier"
        );
    }
}

ビジネスロジック層での実践的な使用例

ドメインサービスの実装

public abstract class OrderProcessingServiceBase
{
    protected readonly IOrderRepository _orderRepository;
    protected readonly IInventoryService _inventoryService;
    protected readonly IPaymentService _paymentService;
    protected readonly INotificationService _notificationService;
    protected readonly ILogger _logger;

    protected OrderProcessingServiceBase(
        IOrderRepository orderRepository,
        IInventoryService inventoryService,
        IPaymentService paymentService,
        INotificationService notificationService,
        ILogger logger)
    {
        _orderRepository = orderRepository;
        _inventoryService = inventoryService;
        _paymentService = paymentService;
        _notificationService = notificationService;
        _logger = logger;
    }

    public async Task<OrderResult> ProcessOrderAsync(Order order)
    {
        try
        {
            // トランザクション開始
            using var transaction = 
                await _orderRepository.BeginTransactionAsync();

            try
            {
                // 在庫確認
                if (!await CheckInventoryAsync(order))
                    return OrderResult.Failed("在庫不足");

                // 支払い処理
                if (!await ProcessPaymentAsync(order))
                    return OrderResult.Failed("支払い処理失敗");

                // 在庫引き当て
                await ReserveInventoryAsync(order);

                // 注文確定
                await FinalizeOrderAsync(order);

                // トランザクションコミット
                await transaction.CommitAsync();

                // 通知送信
                await SendNotificationsAsync(order);

                return OrderResult.Success();
            }
            catch (Exception ex)
            {
                await transaction.RollbackAsync();
                throw;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "注文処理中にエラーが発生しました");
            return OrderResult.Failed(ex.Message);
        }
    }

    protected abstract Task<bool> CheckInventoryAsync(Order order);
    protected abstract Task<bool> ProcessPaymentAsync(Order order);
    protected abstract Task ReserveInventoryAsync(Order order);
    protected abstract Task FinalizeOrderAsync(Order order);
    protected abstract Task SendNotificationsAsync(Order order);
}

// 具体的な実装例
public class StandardOrderProcessingService : OrderProcessingServiceBase
{
    public StandardOrderProcessingService(
        IOrderRepository orderRepository,
        IInventoryService inventoryService,
        IPaymentService paymentService,
        INotificationService notificationService,
        ILogger<StandardOrderProcessingService> logger)
        : base(orderRepository, inventoryService, paymentService, 
               notificationService, logger)
    {
    }

    protected override async Task<bool> CheckInventoryAsync(Order order)
    {
        foreach (var item in order.Items)
        {
            if (!await _inventoryService
                .HasSufficientStock(item.ProductId, item.Quantity))
                return false;
        }
        return true;
    }

    protected override async Task<bool> ProcessPaymentAsync(Order order)
    {
        var paymentResult = await _paymentService
            .ProcessPayment(order.PaymentDetails);
        return paymentResult.IsSuccessful;
    }

    // 他のメソッドの実装...
}

これらの実装例は、実際のプロジェクトで使用される抽象クラスの一般的なパターンを示しています。
これらのパターンを適切に活用することで、保守性が高く、拡張性のあるアプリケーションを構築することができます。

抽象クラスの使い方と実践パターンのまとめ

C#の抽象クラスは、適切に活用することでコードの品質を大きく向上させることができます。
設計の初期段階から抽象化の必要性を見極め、適切なパターンを選択し、アンチパターンを避けることで、保守性と拡張性に優れたアプリケーションを実現できます。
実際のプロジェクトでは、ここで紹介した実装例を基に、各プロジェクトの要件に合わせた抽象クラスの設計を行っていくことをお勧めします。

この記事の主なポイント

抽象クラスはインターフェースと異なり、共通実装を提供できる

過度な抽象化は避け、具体的なニーズに基づいた設計を行う

デザインパターンを活用することで、より効果的な抽象化が実現できる

テストしやすさと依存性注入を考慮した設計が重要

実務では特にサービス層やデータアクセス層での活用が有効