4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[C#]拡張メソッドを使って、I/Fの変更無しに扱えるフィールドやサービスを増やす

Last updated at Posted at 2022-02-24

はじめに

拡張メソッドを使うことがありましたので、そのとき得た知見をまとめることを目的としています。
マイクロソフト公式の拡張メソッドのガイドラインはこちら

拡張メソッドを使うことになった背景を簡単に説明します。
I/Fを変更するとそのI/Fを実装するすべてのクラスに変更の影響があり、実装を修正しなければなりません。
特に、プロパティやメソッドが頻度高く増え続ける可能性があるI/Fかつ、複数の外部のプロジェクトでI/Fが実装される場合、毎回、その実装クラスを修正しなければならず、変更がし辛いです。
この問題を解決するために拡張メソッドを利用しました。以下にこの時取った解決策を記載します。

解決方法

前提として、I/Fとその具象プロジェクトは別プロジェクトになっており、外部からはI/Fがあるプロジェクトのみが参照可能です。

I/Fは、固有のプロパティやメソッドは一切持たないようにします。
I/Fは、フィールドやサービスを設定・取得するための汎用的なメソッドのみにします。

public interface ISampleContext
{
    /// <summary>
    /// サービスを取得します
    /// </summary>
    /// <typeparam name="T">サービスのインターフェース</typeparam>
    /// <returns>サービス</returns>
    T GetService<T>(T defaultValue = default(T)!) where T : class;

    /// <summary>
    /// サービスを登録します
    /// </summary>
    /// <param name="instance">サービスの実クラス</param>
    /// <typeparam name="T">サービスのインターフェース</typeparam>
    void RegisterService<T>(T instance);

    /// <summary>
    /// フィールドを取得します
    /// </summary>
    /// <param name="fieldName">フィールドの名前</param>
    /// <param name="defaultValue">デフォルト値</param>
    /// <typeparam name="T">フィールドの型</typeparam>
    /// <returns>フィールド</returns>
    T GetField<T>(string fieldName, T defaultValue = default(T)!);

    /// <summary>
    /// フィールドを設定します
    /// </summary>
    /// <param name="fieldName">フィールドの名前</param>
    /// <param name="value">設定したい値</param>
    void SetField(string fieldName, object value);
}

I/Fの実装クラスでは、フィールドやサービスを保持するためのディクショナリを持ち、メソッドの実装ではそのディクショナリへの値の設定と、ディクショナリから値を返すだけの実装にします。

internal class SampleContext : ISampleContext
{
    /// <summary>
    /// サービスを保持するディクショナリ
    /// </summary>
    private readonly IDictionary<Type, object> m_Services = new Dictionary<Type, object>();

    /// <summary>
    /// フィールドを保持するディクショナリ
    /// </summary>
    private readonly IDictionary<string, object> m_Fields = new Dictionary<string, object>();

    /// <summary>
    /// サービスを取得します
    /// </summary>
    /// <typeparam name="T">サービスのインターフェース</typeparam>
    /// <returns>サービス</returns>
    public T GetService<T>() where T : class
    {
        if (!m_Services.ContainsKey(typeof(T))) return defaultValue;
        if (!(m_Services[typeof(T)] is T service)) return defaultValue;
        return service;
    }

    /// <summary>
    /// サービスを登録します
    /// </summary>
    /// <param name="instance">サービスの実クラス</param>
    /// <typeparam name="T">サービスのインターフェース</typeparam>
    public void RegisterService<T>(T instance)
    {
        if (m_Services.ContainsKey(typeof(T))) return;
        if(instance == null) throw new Exception("instance != null");
        m_Services.Add(typeof(T), instance);
    }

    /// <summary>
    /// フィールドを取得します
    /// </summary>
    /// <param name="fieldName">フィールドの名前</param>
    /// <param name="defaultValue">デフォルト値</param>
    /// <typeparam name="T">フィールドの型</typeparam>
    /// <returns>フィールド</returns>
    public T GetField<T>(string fieldName, T defaultValue = default(T)!)
    {
        if (string.IsNullOrEmpty(fieldName)) return defaultValue;
        if (!m_Fields.ContainsKey(fieldName)) return defaultValue;
        if (!(m_Fields[fieldName] is T field)) return defaultValue;
        return field;
    }

    /// <summary>
    /// フィールドを設定します
    /// </summary>
    /// <param name="fieldName">フィールドの名前</param>
    /// <param name="value">設定したい値</param>
    public void SetField(string fieldName, object value)
    {
        if(string.IsNullOrEmpty(fieldName)) throw new Exception("fieldName != null");
        if (!m_Fields.ContainsKey(fieldName!))
        {
            m_Fields.Add(fieldName!, value);
        }
        else
        {
            m_Fields[fieldName!] = value;
        }
    }
}

I/Fで定義したメソッドを直接使うこともできます。
ただし、このままだとこのI/Fの利用者が、サービスを取得したうえで、サービスに委譲する処理を実装したり、文字列でフィールド名を指定したりする必要があるため使い勝手が悪いです。
拡張メソッドを使うことで、解消します。以下のようなイメージです。


public static class SampleContextExtensions
{
    /// <summary>
    /// XXXの処理をします
    /// </summary>
    /// <param name="self"></param>
    /// <returns></returns>
    public static void DoSomething(this IControlContext self)
    {
        // I/Fのメソッドを使用してサービスを取得
        var xxxService = self?.GetService<IXXXService>();
        
        // サービスに処理を委譲
        xxxService?.Something();
    }

    /// <summary>
    /// YYYを取得します
    /// </summary>
    /// <param name="self"></param>
    /// <returns>YYY</returns>
    public static IYYY? GetYYYField(this IControlContext self)
    {
        // I/Fのメソッドを使用してフィールドの値を取得
        return self?.GetField<IYYY>(nameof(IYYY));
    }

    /// <summary>
    /// YYYを設定します
    /// </summary>
    /// <param name="self"></param>
    /// <param name="value"></param>
    public static void SetYYYField(this IControlContext self, IYYY value)
    {
        // I/Fのメソッドを使用してフィールドの値を設定
        self.SetField(nameof(IYYY), value);
    }
}

このように、I/Fでは固有のプロパティやメソッドを定義せず汎用的なメソッドのみを用意し、そのバリエーションや便利メソッドは拡張メソッドで定義することがポイントです。
こうすることで、I/Fを変更する必要が無いため、I/Fを実装する他ユーザーなどへの影響がありません。
また、拡張メソッドはI/Fと同じプロジェクトに定義します。こうすることで、I/Fを参照するプロジェクトでも拡張メソッドが使用できるようになり便利です。

さいごに

拡張メソッドの存在自体は知っていましたが、どういう場合に使えるのか知らなかったので今回記事にしてみました。
マイクロソフト公式では、拡張メソッド使用時の注意事項がたくさん書かれており、むやみな拡張メソッドの使用は避けることが推奨されていますので、ご注意ください。特に、他の人が作ったソースコードに対して拡張メソッドを作るのは、推奨していないようでした。
自分がライブラリを公開したりする場合には、I/Fの変更をせずに済み、ライブラリの利用者への影響が無くなるため、今回のような拡張メソッドを使った方法は有用だと思いました。

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?