業務で抽象クラスを継承し、インターフェースを実装しているクラスがあり、併用している意図が分からなかったので調べてみました。
結論
Template MethodとDIの組み合わせでした。
Template Method
Template Methodは親クラスで大まかな処理を定義し、詳細は親クラスを継承した子クラスで実装するデザインパターンです。
親クラスで大まかな処理を定義しておくことで、子クラスごとに同じコードを書かずに共通化することができます。
DI
オブジェクトの依存関係を外部から注入することで、クラス間を疎結合にするデザインパターンです。
DIを利用することでクラスの付け替えがしやすくなり、モックによるテストがしやすくなります。
例
クラス図
PRGでキャラクターを操作するCommnadクラスとキャラクターを共通化した抽象クラスのCharactorクラスがあります。
子クラスのWarriorとWizardは攻撃コマンドのインターフェースであるIAttackCommandを実装し、抽象クラスのCharacterを継承しています。
コード
- C# 8.0
- NUnit 3.12.0
- Moq 4.17.2
- Visual Studio 2022 17.4.1
public class Command
{
private IAttackCommand attackCommand;
public Command(IAttackCommand attackCommand)
{
this.attackCommand = attackCommand;
}
public void Attack()
{
attackCommand.Attack();
}
}
public interface IAttackCommand
{
public void Attack();
}
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()を呼び出すとキャラクター名とダメージのメッセージが表示されるという振る舞いを共通化して、詳細な値は子クラスごとに決めることができます。
public class Warrior : Character, IAttackCommand
{
public override string Name => "戦士";
public override int AttackPoint => 100;
}
public class Wizard : Character, IAttackCommand
{
public override string Name => "魔法使い";
public override int AttackPoint => 50;
}
使用例
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回呼び出されたかテストしています。
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によってモックを使ったテストをできるようにするため
