おーみんブログ

C#, ASP.NET Core, Unityが大好きです。

C#でOpen-Closed Principle(OCP)を説明してみる。

はじめに

自学でデザインパターンや設計を勉強してもなかなか定着しなかったのですが、お客様先(常駐している現場)の上司に教えてもらって一気に理解が進んだので備忘録として残しておこうと思います。

Open-Closed Principle(OCP)とは?

OCPとはSOLID原則の1つであり、「クラスは拡張に対して開かれており、修正(変更)に対しては閉じていなければならない」という考え方です。

つまり「新しい機能を追加する(拡張)場合は既存の機能のことを考える必要なく(それぞれの機能が独立している)、既存機能を修正・変更する際は他に依存しないような作りにする」ということです。

と言ってもサンプルコードがないと!

そこでサンプルコードを書いてみました。
以下はASP.NET Core コンソールアプリで渡されたアプリケーション引数によって犬か猫の鳴き声を表示させるプログラムです。

OCPに反したコード

Animal.cs
 
namespace ConsoleApp2
{
    public class Animal
    {
        public static string Dog() => "bowwow";

        public static string Cat() => "meow";
    }
}
Program.cs
 
using System;
using System.Linq;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            if (!args.Any())
            {
                Console.WriteLine("アプリケーション引数を入れてください。");
                return;
            }

            string nakigoe;
            switch (args[0])
            {
                case "dog":
                    nakigoe = Animal.Dog();
                    Console.WriteLine(nakigoe);
                    break;
                case "cat":
                    nakigoe = Animal.Cat();
                    Console.WriteLine(nakigoe);
                    break;
            }
            Console.ReadLine();
        }
    }
}

僕もよくこんな感じで書いちゃっていましたが、これだと例えば鳥の鳴き声を表示する機能を作る際にAnimalクラスに鳥の鳴き声の関数を、ProgramクラスのSwitchの中に鳥の場合を足していかなければいけません。
このサンプルだと「それくらい余裕でしょ!」といけますが、実際に業務で扱うプログラムはこんなに単純ではないのでミスが増える可能性が高まります。

インターフェースを用いて以下のように作成します。

OCP原則に基づいたコード

AnimalService.cs
 
namespace ConsoleApp1
{
    public interface IAnimalService
    {
        public string Pattern { get; }
        public string Nakigoe();
    }

    public class DogService : IAnimalService
    {
        public string Pattern => "dog";

        public string Nakigoe() => "bowwow";
    }

    public class CatService : IAnimalService
    {
        public string Pattern => "cat";

        public string Nakigoe() => "meow";
    }
}

Program.cs
 
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            if (!args.Any())
            {
                Console.WriteLine("アプリケーション引数を入れてください。");
                return;
            }

            var services = new ServiceCollection();
            services.AddTransient<IAnimalService, DogService>();
            services.AddTransient<IAnimalService, CatService>();

            using var provider = services.BuildServiceProvider();

            //DIされた内容が配列として渡ってきます
            var animalServices = provider.GetServices<IAnimalService>();
            var animalService = animalServices
                .FirstOrDefault(o => o.Pattern == args[0]) ?? new DogService();

            var nakigoe = animalService.Nakigoe();
            Console.WriteLine(nakigoe);
            Console.ReadLine();
        }
    }
}

(DIやFirstOrDefaultのデフォルト時にDogServiceとなっているのは一旦置いておきましょう)

こちらではDIで配列として渡ってきたanimalServicesのPatternとアプリケーション引数を比較してサービスを求めています。
"Pattern"はDogServiceクラス、CatServiceクラスでそれぞれ定義しています。

IAnimalServiceインターフェースを継承して鳥のサービスクラスを作るだけでOKです。
(DIコンテナに鳴き声クラスを登録する必要はありますが...)

おわりに

日々の仕様変更に耐えるためにも保守性を意識したプログラムは大切だなと思うのでしっかり理解を深めていきたいところです。