業務で抽象クラスを継承し、インターフェースを実装しているクラスがあり、併用している意図が分からなかったので調べてみました。
結論
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によってモックを使ったテストをできるようにするため