ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

抽象化は何も継承やインターフェースの実装だけを言うのではない

抽象化

プログラミングの話は一旦横に置いといて。
「抽象化」とは「物事のある側面のみを切り分けて考えること」と言えます。
何当たり前のこと言ってんだ……と思う人は多分大丈夫、理解している、というか、普段の思考に抽象化が染み付いているはずです。プログラミング特有のテクニックはまた別に知る必要がある可能性はありますが、そんなものはよくオススメされてる本を読めば解決できます。

問題は、一般的な「抽象化」が理解できてないのに、プログラミングの「抽象化」を行おうとしているプログラマです。

プログラムにおけるいわゆる抽象化手法

プログラムにおいて抽象化とは、例えばインターフェースや継承可能なクラスを使ったテクニックを指すことが多いと思います。

public class BaseClass { }

public class DerivedClass : BaseClass { }

BaseClass instance = new DerivedClass();

こんな感じですね。

ですが、これが実際のプロダクトのコードだとするなら、これでは抽象化できていません。
変数は基底クラスで宣言されていますが、代入するインスタンスDerivedClassであることをしっかり書いているので。
中途半端に設計を齧った人にたまにいるのですが、変数を基底クラスやインターフェースにするだけでは何も抽象化できていることにはならないです。
なんなら、具象クラスとして宣言しても変わらないところをわざわざ抽象変数で宣言されると、IDEで追いづらくなるし、(言語、環境によるけど)実行時に仮想呼び出しになって遅くなるし、デメリットすら有ります。

抽象化の本質

抽象化とは、インターフェースや基底クラスなどといったテクニックのことだけを言うのではなく、特に、モジュール化が意識されたプログラムにおいては「使用者から見て必要な情報だけが見える状態になっている」こと全般を指します。テクニックはただの手段に過ぎません。テクニックもそりゃ大事ですが、なぜそうすると良いのか、誰が嬉しいのか、といった本質を見失わないようにしましょう。

継承もインターフェースも使わない抽象化

継承やインターフェースが「抽象化」の強力な手段であることは間違い無いのですが、とはいえそれが全てではありません。(ちなみに筆者は基本的に継承アンチです)
例えば、ある条件で処理を分岐させたいと思ったらメソッドの中でif分岐しますよね。 if分岐を隠蔽したいからその部分を別メソッドに切り分けますよね。 これも立派な抽象化です。使用者からは内部の分岐を見えなくして、必要な情報だけ見えるようにしてるんですから。

public void UserMethod()
{
    ...
    // 中で分岐があるが、ここからは見えない。これも立派な抽象化。
    BranchMethod();
    ...
}

private void BranchMethod()
{
    if(this.isHogehoge)
    {
        // 何か複雑な処理
    }
    else
    {
        // とても複雑な処理
    }
}

クラスの依存関係に関しても例を挙げましょう。
あるインターフェース(I)を実装したクラス(A),(B)があり、条件によってどちらを使うか切り分けたいとき、使用者側(U)のクラスで変数(I)を持って、代入する具象クラス(A),(B)を切り替える、という書き方ができます。でも、それでは結局(U)は(I),(A),(B)の3つの型に依存することになります。

public class U
{
    private I fuga;
    
    public U(bool isHogehoge)
    {
        // フィールド変数はIだけど、ここでAとBを知ってしまっている。
        fuga = isHogehoge ? new A() : new B();
    }
    
    // Iを使用した諸々のメソッド
    ...
}

public class A : I { }
public class B : I { }
public interface I { }

(U)の責務は恐らく(I),(A),(B)を道具として使い、また別の何かをこなすことです。どの道具を使うかの切り替えは別の責務と言えます。
ですので、最も良いのは、(A),(B)どちらかを使うかの使い分け用クラス(X)を用意して、(U)はその使い分けクラス(X)にだけ依存することです。
(I),(A),(B)の存在は、プロダクト全体を見ても(X)だけが知っているのがベストでしょう。何なら(I),(A),(B),(X)でアセンブリを分けてしまうくらいしてもいいです。

(I),(A),(B)はinternalで宣言し、(X)だけをpublicで宣言することで、内部で実は適切にクラスが切り分けられているのが、外部からは一切気にしなくていいモジュールが出来上がります。(Xの内部クラスとしてprivateで宣言してもいいと思います。) (X)はインターフェースでも何かの基底クラスでもありませんが、ちゃんと抽象化しています。「使用者から見て必要な情報だけが見える状態になっている」ので。

さらに言えば、(X)があるなら(I)すら要らない可能性もあります。内部で愚直に(A),(B)を使い分ければいいんですから。

public class X
{
    private A instanceA = new();
    private B instanceB = new();

    public void HogeMethod()
    {
        if(this.isHogehode)
        {
            instanceA.MethodA();
        }
        else
        {
            instanceB.MethodB();
        }
    }
}

こうしたところで、(X)の使用者である(U)にとってはどうでもいいことです。つまり、インターフェースすら出てきてないし継承も使ってないのに抽象化ができたことになります。
格好悪い書き方だと思うでしょうか? でも、これならインターフェースの適切な分離とかも考えなくていいし何の制約も無いから、どんな仕様変更が来ても素直に対応できます。テクニカルな解決なんてプログラマの自己満足です。

大事な考え方

大事にすべきなのは「使用者の目線」です。設計においては「モジュール」という概念が非常に大事なのですが、あらゆるモジュールには使用者がいます。メソッドもクラスもそうですし、dllにも使用者がいます。 プロダクトにだってユーザーがいるでしょう。 プログラマが作る全てのものは誰かが使うために作られると言えます。ですから、その使用者の立場に立って考えることが大事なのは当たり前のことですね。