12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#Advent Calendar 2023

Day 11

【C#】抽象クラスとインターフェースを併用する理由を考えた

Last updated at Posted at 2023-12-10

業務で抽象クラスを継承し、インターフェースを実装しているクラスがあり、併用している意図が分からなかったので調べてみました。

結論

Template MethodとDIの組み合わせでした。

Template Method

Template Methodは親クラスで大まかな処理を定義し、詳細は親クラスを継承した子クラスで実装するデザインパターンです。

親クラスで大まかな処理を定義しておくことで、子クラスごとに同じコードを書かずに共通化することができます。

DI

オブジェクトの依存関係を外部から注入することで、クラス間を疎結合にするデザインパターンです。

DIを利用することでクラスの付け替えがしやすくなり、モックによるテストがしやすくなります。

クラス図

TemplateMethod.png

PRGでキャラクターを操作するCommnadクラスとキャラクターを共通化した抽象クラスのCharactorクラスがあります。

子クラスのWarriorWizardは攻撃コマンドのインターフェースであるIAttackCommandを実装し、抽象クラスのCharacterを継承しています。

コード

  • C# 8.0
  • NUnit 3.12.0
  • Moq 4.17.2
  • Visual Studio 2022 17.4.1
Command.cs
public class Command
{
    private IAttackCommand attackCommand;

    public Command(IAttackCommand attackCommand)
    {
        this.attackCommand = attackCommand;
    }

    public void Attack()
    {
        attackCommand.Attack();
    }
}
IAttackCommand.cs
public interface IAttackCommand
{
    public void Attack();
}
Character.cs
public abstract class Character
{
    public abstract string Name { get; }
    public abstract int AttackPoint { get; }

    public void Attack()
    {
        Console.WriteLine($"{Name}のこうげき!");
        Console.WriteLine($"あいてモンスターに{AttackPoint}のダメージ");
    }
}

攻撃用のAttack()の中で抽象プロパティを使用し、具体的な値は子クラスで設定しています。

Attack()を呼び出すとキャラクター名とダメージのメッセージが表示されるという振る舞いを共通化して、詳細な値は子クラスごとに決めることができます。

Warrior.cs
public class Warrior : Character, IAttackCommand
{
    public override string Name => "戦士";
    public override int AttackPoint => 100;
}
Wizard.cs
public class Wizard : Character, IAttackCommand
{
    public override string Name => "魔法使い";
    public override int AttackPoint => 50;
}

使用例

Program.cs
class Program
{
    static void Main(string[] args)
    {
        IAttackCommand warrior = new Warrior();
        IAttackCommand wizard = new Wizard();

        var warriorCommand = new Command(warrior);
        var wizardCommand = new Command(wizard);

        warriorCommand.Attack();
        wizardCommand.Attack();
    }
}
戦士のこうげき!
あいてモンスターに100のダメージ
魔法使いのこうげき!
あいてモンスターに50のダメージ

テストコード

IAttackCommandインターフェースをDIしているため、テストコード側でテスト用のモックを使いテストすることができます。

下記の場合、CommnadクラスのAttack()を実行し、IAttackCommandインターフェースのAttack()が1回呼び出されたかテストしています。

CommandTest.cs
using NUnit.Framework;
using Moq;

public class CommandTest
{
    
    [Test]
    public void AttackTest()
    {
        var mock = new Mock<IAttackCommand>();
        mock.Setup(x => x.Attack()).Callback(() => Console.WriteLine("テストのこうげき!"));
    
        var command = new Command(mock.Object);
        command.Attack();
    
        mock.Verify(x => x.Attack(), Times.Once);
    }
}

まとめ

最初は意図が分からなかったですが、以下の目的があることが分かりました。

  • 抽象クラスはTemplate Methodによって親クラスで処理を共通化し、子クラスで詳細を決めるため
  • インターフェースはDIによってモックを使ったテストをできるようにするため

参考

12
9
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
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?