20200709のC#に関する記事は11件です。

unityで音を鳴らそう!

始めに

この記事の対象者
- 製作中のゲームに音を入れたい人
- unityに興味がある人

下準備

  1. unityのプロジェクトを開く スクリーンショット 2020-02-17 20.04.08.png
  2. Create -> Create Emptyを選択してCreate Emptyを生成する スクリーンショット 2020-02-17 20.05.17.png
  3. Create(Project) -> C# Scriptを洗濯して新しいスクリプトを生成する スクリーンショット 2020-02-17 20.06.01.png
  4. スクリプトを右クリックしてRemameを選択'sound_script'と入力してダブルクリック スクリーンショット 2020-02-17 20.12.04.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

unityでとりあえずプレイヤーを動かすスクリプト

始めに

フライトゲームやFPSでプレイヤーを動かすスクリプトを書きました。
プレイヤーの動かし方が分からない人は必見です!
(これはmacで開発することが前提となっています)

下準備

  1. unityのnewを選択 スクリーンショット 2020-02-16 20.28.41.png
  2. Project nameを決めてCreate projectを押すスクリーンショット 2020-02-16 20.31.50.png
  3. Create -> 3D Object -> Planeをクリック、地面を作ります。 スクリーンショット 2020-02-16 20.45.42.png
  4. Create -> 3D Object -> Cube、今回はこれを動かします。 スクリーンショット 2020-02-16 20.46.41.png
  5. Cube -> Add Componentをクリック'Rigidbody'と検索Rigidbodyをクリックして追加する スクリーンショット 2020-02-16 20.54.18.png
  6. Main CameraをドラクアンドドロップでCubeに入れる
    スクリーンショット 2020-02-16 20.59.28.png
  7. ProjectのCreateからC#Scriptを選択してクリック
    スクリーンショット 2020-02-16 21.03.09.png
  8. デリートキーを押し、'Player_controller'と入力
    スクリーンショット 2020-02-16 21.05.14.png
  9. 作成したスクリプトをダブルクリックで開く スクリーンショット 2020-02-16 21.13.27.png これで下準備はOKです。

スクリプト

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player_controller : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.position += new Vector3(0,0,0.1f);
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            transform.position += new Vector3(0,0,-0.1f);
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            transform.position += new Vector3(0.1f,0,0);
        }
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            transform.position += new Vector3(-0.1f,0,0);
        }
    }

}

開いたプログラムの内容を全て削除して上のスクリプトをコピペしてコマンドSで保存。unityに戻りこのスクリプトをCubeにドラクアンドドロップをする。
これで上の三角のボタンを押せば終了です。
もし、Cubeにスクリプトを入れられなかったらスクリプトを右クリック -> Remameを押して'Player_controller'と入れてみてください!

最後に

最後まで見てくださってありがとうございます。
Qiitaは始めたばかりなので間違っていたら遠慮なく指摘してください!よろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# で前前前世

Delphi で前前前世 - Qiita を C# でやってみました。拡張メソッドがあるので簡単ですね。

using System;

namespace ConsoleApp1
{
    static class Program
    {
        static void Main() => then().then().then().();

        public static string then() => "".then();

        public static string then(this string prev) => prev + "前";

        public static void (this string prev) => Console.WriteLine(prev + "世");
    }
}

出力

前前前世

踊り字を使ってみる。

using System;
using System.Linq;

namespace ConsoleApp1
{
    static class Program
    {
        static void Main() => then().then().then().();

        public static string then() => "".then();

        public static string then(this string prev) => prev + (!string.IsNullOrEmpty(prev) && "前々".Contains(prev.Last()) ? "々" : "前");

        public static void (this string prev) => Console.WriteLine(prev + "世");
    }
}

出力

前々々世

まあ Console.WriteLine をこんなところに入れてる時点で気持ち悪いんですが。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】LINQ

★LINQ(統合言語クエリ)とは

--Wikipedia引用--
統合言語クエリ(Language INtegrated Query)とは.NET Framework 3.5において、様々な種類のデータ集合に対して標準化された方法でデータを問い合わせること(クエリ)を可能にするために、言語に統合された機能のことである。
統合言語問合せとも表記される

★LINQの使い方
・LINQには標準でIEnumerable、IEnumeratorが実装されている


LINQの記述方法
◇クエリ構文 
・from句が必要で、from句から始める
・終わりがselect句かgroup句で終了
・ループ処理は発生しない

QueryLinq.cs
var test1 =
    from a in test
    where a * 2 == 0
    select a;


◆メソッド構文
・from句が不要
・メソッドベースの構文

MethodLinq.cs
var test1 = test.where(a => a * 2 == 0)
                .select(a => a);

★遅延評価
・LINQの特徴に遅延評価というのが存在する。
・基本的にはクエリ式は即時評価されない。1
・クエリ変数がforeachなどの反復処理によって評価されるまでクエリの実行は遅延する


  1. 遅延評価の逆は正格評価(先行評価)といい、即時評価される 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】try-catch

★try-catchの記述方法
・tryブロックには例外1の可能性がある処理を記述
・catchブロックには例外1が発生した場合の処理を記述
・finallyブロックには例外1の有無関わらず実行する処理を記述

try
{
    //例外発生の可能性のある処理を記述
}
catch
{
    //例外が発生した場合の処理を記述
}
finally
{
    //例外の発生有無に関わらず実行する処理を記述
}


(例)

Test.txt
ささみの味噌煮
try-catchの記述方法
using System
class SampleTryCatch
{
    static void Main()
    {
        StreamReader sr = null;
        try
        {
            sr = new StreamReader(@"D:Test.txt",Encoding.GetEncoding("Shift_JIS"));
            string str = sr.ReadToEnd();
            Console.WriteLine(str);
        }
        catch (IOException e)
        {
            Console.WriteLine("例外が発生しました。");
            Console.WriteLine(e);
            return 0;
            //またはスルーさせる
            //throw;
        }
        finally
        {
            if(sr != null)
            {
                sr.Close();
                Console.WriteLine("ファイルを閉じました");
    }

実行結果

ささみの味噌煮
ファイルを閉じました

★例外フィルター
・catch句にwhen句を記述することによって例外条件を記述できる
・when句がある場合、catch句に同名型を複数使用できるが、上位から順に処理される

例外条件の記述方法
try
{
    //例外発生の可能性のある処理を記述
}
catch(例外) when(条件)
{
    //例外が発生した場合の処理を記述
}

  1. 例外クラスの一覧はこちら 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】例外処理クラス一覧

クラス名 throw条件
名前空間:System --------
ArgumentException メソッドの引数がない
ArgumentNullExceptionやArgumentOutOfRangeException以外
ArgumentNullException 引数がNull
ArgumentOutOfRangeException メソッドの許容範囲外の値が引数として渡された
ArithmeticException 算術演算によるエラーの基本クラス
OverflowException,DivideByZeroException,NotFiniteNumberException以外の算術エラーを示したいとき
OverflowException 算術演算やキャストでオーバーフローが起きたとき
DivideByZeroException 0で割ったとき
NotFiniteNumberException 浮動小数点数値が無限大のとき
FormatException 引数の書式が仕様に一致していない場合
IndexOutOfRangeException 配列のインデックスが異常なとき
InvalidCastException 無効なキャストのとき
InvalidOperationException 引数以外が原因のとき
ObjectDisposedException Dispose済みのオブジェクトで操作が実行されたとき
NotImplementedException メソッドが未実装の場合
NotSupportedException 呼び出されたメソッドがサポートされてないとき
もしくは呼び出された機能を備えておらず、ストリームに対して読み取り、シーク、書き込みが試行されたとき
NullReferenceException Nullオブジェクト参照を逆参照しようとしたとき
PlatformNotSupportException 特定のプラットフォームで機能が実行されないとき
TimeoutException 指定したタイムアウト時間が経過したとき
名前空間:System.Collections.Generics --------
KeyNotFoundException コレクションに該当するキーがないとき
名前空間:System.IO --------
DirectoryNotFoundException ディレクトリが存在しないとき
FileNotFoundException ファイ府が存在しないとき
EndOfStreamException ストリームの末尾を超えて読み込もうとしたとき
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity/C#】ノームソートとは(コード付き)

ノームソートとは

ノームソートはソートアルゴリズムの一種で、挿入ソートに似ているが、要素の移動は挿入ではなくバブルソートのような一連の交換で行う。その名称の由来は、オランダのノームが一列に並んだ鉢植えの花をソートする話である。

実行結果

ノームソート.gif

サンプルコード

    void GnomeSort(int[] _array)
    {

        int gnome = 1;

        while (gnome < _array.Length)
        {
            if (_array[gnome] < _array[gnome - 1])
            {

                _array[gnome - 1] ^= _array[gnome];
                _array[gnome] ^= _array[gnome - 1];
                _array[gnome - 1] ^= _array[gnome];

                gnome--;


                if (gnome == 0)
                {
                    gnome++;
                }
            }
            else
            {

                gnome++;
            }
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマートフォンアプリケーション開発について

スマートフォンのアプリケーション開発を勉強し始めたので、勉強をした事を復習の意味も込めて書き残そうと思います。

0.スマートフォンアプリケーション開発

スマートフォンが普及をし始めてから約10年、これまで様々な手法で
アプリケーション(以降アプリ)が開発されてきました。
Windowsアプリの開発しか経験のない私には、スマートフォンのアプリ開発にも興味が沸き、これまでどのような言語があるのか代表的な言語を調べてみました。

1.開発言語

  • iOS
    • Objetive-C

       当初はこの「Objetive-C」を開発言語とされていましたが、
       現在ではSwiftが主流となりつつあります。また、独特な文法表現があるため、
       これからiOS向けの開発を始める人にとっては、Swiftがおすすめです。

    • Swift

       SwiftはAppleが2014年に新しく開発したMacやiOS向けの言語です
       動作するにはXcodeというMacの統合開発環境が必要であるため、
       コンパイルするにはMac端末が必要となります。

  • Android
    • Java

       開発に携わっている人はほとんど聞いたことがある言語「Java」です。
       Androidアプリの多数はJavaで作られています。
       2010年前半は、Javaの開発環境であるEclipseを使用されていましたが、
       現在ではAndroid Studioが使用されています。

  • Windows(UWP)
    • C#

       C#はもともとC言語とオブジェクト指向の要素を持つ言語で、
       Microsoft .NET Framework上で開発をします。
       C++++とも呼ばれましたが、C#のほうがしっくりきます。(笑)

    • VisualBasic

       こちらの言語もC#と同様、.NET Framework上で開発をします。
       もともとはBASIC言語から派生した言語でした。BASIC6.0までは
       オブジェクト指向の要素を持っていなかったがVisualBasic.NETでは
       その要素も実装されており、C#に劣らない言語です。

2.まとめ

各言語を使えばそれぞれのプラットフォーム上でアプリを開発することはできますが、
クロスプラットフォームをターゲットとしたい場合は各言語で開発する必要がでてきます。

面倒なので私は絶対に嫌です!(笑)
クロスプラットフォーム上で動作する言語があれば開発が便利になりますね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Win32APIのCreateWindowExでウィンドウ生成に失敗する時の対処法 C#

はじめに

自分の知識を共有するページを書くのはこれが初めてです。
初心者なので稚拙な文章かもしれません。

更新

@ktz_aliasさんにコメントを頂いて、おそらく一番正しい方法が分かったため、記事を修正します。
コメントありがとうございます。

目的

CreateWindowExでウィンドウを生成する時に、追加したWindowClassを指定すると、ウィンドウの生成に失敗する」
という問題の解決

解決方法

必要な部分にCharSet=CharSet.Unicode属性を記述すれば解決します。

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
struct WNDCLASSEX { ... }

[DllImport("user32.dll", CharSet=CharSet.Unicode, EntryPoint="CreateWindowEx")]
public static extern IntPtr CreateWindowEx(
    int dwExStyle,
    string lpClassName,
    string lpWindowName,
    uint dwStyle,
    ... );

[DllImport("user32.dll", CharSet=CharSet.Unicode, EntryPoint="RegisterClassEx")]
static extern System.UInt16 RegisterClassEx([In] ref WNDCLASSEX lpWndClass);

解決方法(その2)

CreateWindowExの値WindowClassNameは byte配列で受け取る。
WindowClassNameに渡す文字列は Unicode で byte配列に変換して渡す。

[DllImport("user32.dll", EntryPoint="CreateWindowEx")]
public static extern IntPtr CreateWindowEx(
    int dwExStyle,
    byte[] WindowClassName,     // byte配列で受け取る。
    string WindowName,
    uint dwStyle,
    int x,
    int y,
    int nWidth,
    int nHeight,
    IntPtr hWndParent,
    IntPtr hMenu,
    IntPtr hInstance,
    IntPtr lpParam );

void create()
{
    // WindowClassを作成
    WNDCLASSEX wind_class = new WNDCLASSEX();
    wind_class.cbSize = Marshal.SizeOf(typeof(WNDCLASSEX));
    wind_class.WindowClassName= "ClassName";
    wind_class.lpfnWndProc = Marshal.GetFunctionPointerForDelegate(delegWndProc);

    // 作成したWindowClassを登録する
    ushort regResult = RegisterClassEx(ref wind_class);

    // ウィンドウの生成
    IntPtr hWnd = CreateWindowEx(0,
        // Unicodeでbyte配列に変換して渡してあげる
        new UnicodeEncoding().GetBytes(wind_class.WindowClassName),
        "Title",
        WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 300, 400,
        IntPtr.Zero, IntPtr.Zero, wind_class.hInstance, IntPtr.Zero );
}

原因

おそらく、この記事を読んでいる人は
CreateWindowExの定義部分WindowClassNameは string型になっていると思う。

[DllImport("user32.dll", EntryPoint="CreateWindowEx")]
    public static extern IntPtr CreateWindowEx(
        int dwExStyle,
        string WindowClassName,     // ここがstringになっている
        string WindowName,
        uint dwStyle,
        ... );

実はWindowClassNameを1文字の string 又は char にしてもウィンドウの生成に成功する。
が、複数文字列の string だと失敗する。
難しいことはあまり分からないが、色々試した結果たどり着いた答えは「文字コードの違い」だった。

別解

CreateWindowExの定義部分で、WindowClassNameの型をushortにして、
CreateWindowExを呼ぶ時に、RegisterClassExの戻り値をWindowClassに渡すことでも成功する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DI って何のためにするのかわからない人向けに頑張って説明してみる

ここでは、最近のそこそこの規模のアプリだと大体使われてる(と私は思ってる)Dependency Injection(DI)について、何故使ってるのか?というのを私の理解で書いていきたいと思います。

今回の対象言語は C# ですが、DI 使ってる言語であれば大体同じ事情なのかなと思います。

単体テストしたいよね

アプリケーションを作るとうまく動いているかテストをすると思います。
たとえ、そのアプリがハローワールドだとしても動かして目視で確認してると思います。

もうちょっとアプリの規模が大きくなってくるとクラス単位やクラスのメソッド単位でのテストもしたくなってきます。俗にいう単体テストというやつですね。いちいちアプリを起動して、そのクラスを使う画面まで移動してテキストを入力したりしてボタンを押さないと動かせないのはテスト効率が悪いので、テストコードを書いて実行して緑のバーが出たらテスト成功!そういう環境を作っておくと何かと便利になります。

image.png

コードを変更したときにテストを実行してグリーンだったら少なくともテストコードを書いてる範囲内についてはバグってることは、ほぼないという感じなので安心感が違います。

単体テストをするというのは、世の中全体としてみても「単体テストなんて不要だ!しないほうがいい!」という人はかなり少数派だと思うので、単体テストは出来るならする方が良いというのは多くの人にとって合意してもらえることなのかなと思います。

単体とは…?

テスト書いてみましょう。例えば以下のようなクラスがあるとします。単純ですね。足し算するだけです。

class Calc
{
  public int Add(int x, int y) => x + y;
}

色々な単体テストフレームワークがありますが、大体 Assert というクラスを使ってメソッドの結果が思った値と同じか確認します。大体以下のようになります。

var calc = new Calc();
Assert.Equal(10, calc.Add(8, 2)); // 8 + 2 は 10 になるはず

でも 1 クラスで完結するようなものはなかなかありません。大体クラス内で別のクラスを使ってます。

例えば以下のような感じで DataGenerator クラスで生成されたデータに対して Aggregator クラスで集計(とりあえず合計)するような感じです。

public class DataGenerator
{
    public int[] Generate() => new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
}

public class Aggregator
{
    public int Sum()
    {
        var dataGenerator = new DataGenerator();
        return dataGenerator.Generate().Sum();
    }
}

Aggregator を単体テストしようとすると DataGenerator が必要ですよね。
今回は DataGenerator は固定値を返すので問題になりませんが例えばインターネット上のデータをもとにデータを生成したり、日付に応じて返すデータが違う場合はどうでしょう?例えばこんな感じです。

public class DataGenerator
{
    public int[] Generate() => new[] { DateTime.Now.Year, DateTime.Now.Day, DateTime.Now.Second };
}

現実には以下のように配列に年月日などの情報を入れて返すようなことはないと思いますが DB の情報によって返す値が変わったり、Web API の結果によって戻り値が変わったり、特定の状態(しかも、その状態になるには、そこそこの手順を踏まないといけない)じゃないとテストしたい値を返さないなど、その時の状況によって結果が異なるようなものが実際には色々あると思います。

そうなると Aggregator のテストはどうすればいいでしょうか?Aggregator は DataGenerator が返す値を集計するのが仕事です。集計処理をテストしたいのに、依存先クラスの実装にひっぱられてテストができないという問題が起きます。

解決方法!!

手段は色々あります。例えばテストしたいロジックだけをメソッドに切り出してテストを行う。

public class Aggregator
{
    public int Sum()
    {
        var dataGenerator = new DataGenerator();
        return SumInternal(dataGenerator.Generate());
    }

    // こっちをテストする
    public int SumInternal(int[] data) => data.Sum();
}

内部ロジックを public にする点が Aggregator の利用者にとって優しくないですね…。あまりイケてません。

ここら辺から DI の話になってきます。Aggregator は データを Generate 出来る人がいればいいのですが、これが DataGenerator クラスに固定化されているためにテストがしづらいという問題が起きています。じゃぁクラス固定じゃなくて Generate 出来る人であればいいということを表す interface を使うように変更してみましょう。

public interface IDataGenerator
{
    int[] Generate();
}

public class DataGenerator : IDataGenerator
{
    public int[] Generate() => new[] { DateTime.Now.Year, DateTime.Now.Day, DateTime.Now.Second };
}

Aggregator は IDataGenerator であれば OK なようにしたいのですが、メソッド内で DataGenerator クラスを new してると実装を切り離すことが出来ないですよね…。

public class Aggregator
{
    public int Sum()
    {
        // インターフェースを導入しても実装を new してたら意味がない…
        IDataGenerator dataGenerator = new DataGenerator();
        return dataGenerator.Generate().Sum();
    }
}

じゃぁ new するのは諦めて外部から受け取るようにしましょう。Aggregator を使う人に責任を押し付けます。

public class Aggregator
{
    // DataGenerator の実装を new するのは外部にお任せ
    public Aggregator(IDataGenerator dataGenerator)
    {
        DataGenerator = dataGenerator;
    }

    private IDataGenerator DataGenerator { get; }

    public int Sum() => DataGenerator.Generate().Sum();
}

これでやっと Aggregator がテストできるようになりました。こんな感じですね。

public class UnitTest1
{
    // テスト用の DataGenerator
    private class MockDataGenerator : IDataGenerator
    {
        public int[] Generate() => new[] { 1, 2, 3 };
    }

    [Fact]
    public void Test1()
    {
        // テスト時はテスト用の DataGenerator を使う
        var aggregator = new Aggregator(new MockDataGenerator());
        // テスト用の DataGenerator は固定値を返すのでテスト出来る
        Assert.Equal(6, aggregator.Sum());
    }
}

こんな感じで内部で依存先を new するのではなく、外から依存先の実装を設定してもらうという考え方が Dependency (依存性) Injection (注入) になります。Dependency Injection とインターフェースを組み合わせて実装を入れ替える仕組みを入れることで、クラス単体でテストが可能になります。やったね。

例え、1 回呼ぶのに 100 万円かかるような Web API があったとしても、今回のように呼び出す処理を行うクラスに対して interface を定義しておいて、実装を差し替えるようにすれば OK ですね。

interface IReallyExpensiveService
{
    string Call();
}

class ReallyExpensiveService: IReallyExpensiveService
{
    public string Call()
    {
        // 1 回呼ぶのに 100 万円の外部サービスを呼んでるとする
    }
}

class MockReallyExpensiveService : IReallyExpensiveService
{
    public string Call()
    {
        // テストのときはテスト用の結果を返す実装でいいよね
        return "テスト用の結果";
    }
}

すべて解決?

こんな感じで、テスト時に差し替え可能に出来たほうが嬉しいものに対して interface を定義して実装を外部から設定するようにするといい感じですが、クラスの利用者側視点から見ると割と最悪です。例えば Aggregator を使う場合は Aggregator の他に DataGenerator を new しないといけないです。

以下のような感じですね。

var aggregator = new Aggregator(new DataGenerator());

もうちょっと複雑になってくると、1 つのオブジェクトを組み立てるのに、こんな風にコードを書かないといけなくなるかもしれません。

var x = new ProductOrderService(
  new ProductRepository(
    new PostgreSQLConnectionProvider(
      new SettingsProvider(),
    )
  ),
  new OrderManager(
    new CustomerManager(
      new CustomerDataAccessor(new SettingsProvider()),
    )
  ),
);

ちょっとこれをやるのは現実的ではないですね…。

DI コンテナ

ということで、オブジェクトの依存関係を外部から設定するようにすると、オブジェクトを組み立てるのが大変になるので、そこを省力化しようというライブラリが登場してきます。

それが DI コンテナです。ほとんどの DI コンテナは、このインターフェースには、この実装クラスを使ってくれというルールを設定しておくと、いい感じにルールに従ってオブジェクトを組み立ててくれます。

イメージとしては以下のような感じです。

var c = new Container(); // Container クラスが DI コンテナだとする

// アプリの起動時あたりで、以下のようにアプリで使うインターフェースと実装クラスを登録していく
c.RegisterType<ISettingsProvider, SettingsProvider>();
c.RegisterType<IDbConnectionProvider, IPostgreSQLConnectionProvider>();
c.RegisterType<IProductRepository, ProductRepository>();
c.RegisterType<ICustomerDataAccessor, CustomerDataAccessor>();
c.RegisterType<ICustomerManager, CustomerManager>();
c.RegisterType<IOrderManager, OrderManager>();
c.RegisterType<IProductOrderService, ProductOrderService>();

// インスタンスが欲しいときは、コンテナから取得
var productOrderService = c.Resolve<IProductOrderService>();

こうすることで、DI コンテナがコンストラクターの引数とかから依存先を解決していって綺麗に組み立てたオブジェクトを返してくれます。便利。

実際にはフレームワークに DI コンテナが組み込まれていることが多くてコンテナのインスタンスを自分で作ったり、コンテナから自分でインスタンスを明示的に取得することは少ないです。利用者がするのは、インターフェースと実装クラスの対応を登録するだけというのが多いです。

まとめ

DI を何故するのか?ということでさくっとまとめましょう。

  • 単体テストを容易にするためにインターフェースに依存するようにクラスを作る
  • 依存先の実装はコンストラクターなどで外部から渡してもらうようにする
  • ↑のように作るとオブジェクトの組み立てが大変になるので、それを省力化するための DI コンテナがある

という感じなので、個人的な感想としては DI しなくても単体テストさえ可能で現実的な労力で実装可能な方法があれば、別に DI コンテナとか DI 使わなくてもいいかなぁとは思ってます。
ただ、黒魔術的なことをしない限りは C# などの静的な型付け言語だとめんどくさいので、今のところおとなしく DI を使うのが現実的です。

おまけとして、DI コンテナを使うことでオブジェクトの生成時に付加価値を追加するようなことも出来るので、フレームワークを提供する側から見ても便利だったりします。まぁ、それは今回の主題とは関係ないのでまた今度。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DI (依存性注入) って何のためにするのかわからない人向けに頑張って説明してみる

ここでは、最近のそこそこの規模のアプリだと大体使われてる(と私は思ってる)Dependency Injection(DI)について、何故使ってるのか?というのを私の理解で書いていきたいと思います。

今回の対象言語は C# ですが、DI 使ってる言語であれば大体同じ事情なのかなと思います。

単体テストしたいよね

アプリケーションを作るとうまく動いているかテストをすると思います。
たとえ、そのアプリがハローワールドだとしても動かして目視で確認してると思います。

もうちょっとアプリの規模が大きくなってくるとクラス単位やクラスのメソッド単位でのテストもしたくなってきます。俗にいう単体テストというやつですね。いちいちアプリを起動して、そのクラスを使う画面まで移動してテキストを入力したりしてボタンを押さないと動かせないのはテスト効率が悪いので、テストコードを書いて実行して緑のバーが出たらテスト成功!そういう環境を作っておくと何かと便利になります。

image.png

コードを変更したときにテストを実行してグリーンだったら少なくともテストコードを書いてる範囲内についてはバグってることは、ほぼないという感じなので安心感が違います。

単体テストをするというのは、世の中全体としてみても「単体テストなんて不要だ!しないほうがいい!」という人はかなり少数派だと思うので、単体テストは出来るならする方が良いというのは多くの人にとって合意してもらえることなのかなと思います。

単体とは…?

テスト書いてみましょう。例えば以下のようなクラスがあるとします。単純ですね。足し算するだけです。

class Calc
{
  public int Add(int x, int y) => x + y;
}

色々な単体テストフレームワークがありますが、大体 Assert というクラスを使ってメソッドの結果が思った値と同じか確認します。大体以下のようになります。

var calc = new Calc();
Assert.Equal(10, calc.Add(8, 2)); // 8 + 2 は 10 になるはず

でも 1 クラスで完結するようなものはなかなかありません。大体クラス内で別のクラスを使ってます。

例えば以下のような感じで DataGenerator クラスで生成されたデータに対して Aggregator クラスで集計(とりあえず合計)するような感じです。

public class DataGenerator
{
    public int[] Generate() => new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
}

public class Aggregator
{
    public int Sum()
    {
        var dataGenerator = new DataGenerator();
        return dataGenerator.Generate().Sum();
    }
}

Aggregator を単体テストしようとすると DataGenerator が必要ですよね。
今回は DataGenerator は固定値を返すので問題になりませんが例えばインターネット上のデータをもとにデータを生成したり、日付に応じて返すデータが違う場合はどうでしょう?例えばこんな感じです。

public class DataGenerator
{
    public int[] Generate() => new[] { DateTime.Now.Year, DateTime.Now.Day, DateTime.Now.Second };
}

現実には以下のように配列に年月日などの情報を入れて返すようなことはないと思いますが DB の情報によって返す値が変わったり、Web API の結果によって戻り値が変わったり、特定の状態(しかも、その状態になるには、そこそこの手順を踏まないといけない)じゃないとテストしたい値を返さないなど、その時の状況によって結果が異なるようなものが実際には色々あると思います。

そうなると Aggregator のテストはどうすればいいでしょうか?Aggregator は DataGenerator が返す値を集計するのが仕事です。集計処理をテストしたいのに、依存先クラスの実装にひっぱられてテストができないという問題が起きます。

解決方法!!

手段は色々あります。例えばテストしたいロジックだけをメソッドに切り出してテストを行う。

public class Aggregator
{
    public int Sum()
    {
        var dataGenerator = new DataGenerator();
        return SumInternal(dataGenerator.Generate());
    }

    // こっちをテストする
    public int SumInternal(int[] data) => data.Sum();
}

内部ロジックを public にする点が Aggregator の利用者にとって優しくないですね…。あまりイケてません。

ここら辺から DI の話になってきます。Aggregator は データを Generate 出来る人がいればいいのですが、これが DataGenerator クラスに固定化されているためにテストがしづらいという問題が起きています。じゃぁクラス固定じゃなくて Generate 出来る人であればいいということを表す interface を使うように変更してみましょう。

public interface IDataGenerator
{
    int[] Generate();
}

public class DataGenerator : IDataGenerator
{
    public int[] Generate() => new[] { DateTime.Now.Year, DateTime.Now.Day, DateTime.Now.Second };
}

Aggregator は IDataGenerator であれば OK なようにしたいのですが、メソッド内で DataGenerator クラスを new してると実装を切り離すことが出来ないですよね…。

public class Aggregator
{
    public int Sum()
    {
        // インターフェースを導入しても実装を new してたら意味がない…
        IDataGenerator dataGenerator = new DataGenerator();
        return dataGenerator.Generate().Sum();
    }
}

じゃぁ new するのは諦めて外部から受け取るようにしましょう。Aggregator を使う人に責任を押し付けます。

public class Aggregator
{
    // DataGenerator の実装を new するのは外部にお任せ
    public Aggregator(IDataGenerator dataGenerator)
    {
        DataGenerator = dataGenerator;
    }

    private IDataGenerator DataGenerator { get; }

    public int Sum() => DataGenerator.Generate().Sum();
}

これでやっと Aggregator がテストできるようになりました。こんな感じですね。

public class UnitTest1
{
    // テスト用の DataGenerator
    private class MockDataGenerator : IDataGenerator
    {
        public int[] Generate() => new[] { 1, 2, 3 };
    }

    [Fact]
    public void Test1()
    {
        // テスト時はテスト用の DataGenerator を使う
        var aggregator = new Aggregator(new MockDataGenerator());
        // テスト用の DataGenerator は固定値を返すのでテスト出来る
        Assert.Equal(6, aggregator.Sum());
    }
}

こんな感じで内部で依存先を new するのではなく、外から依存先の実装を設定してもらうという考え方が Dependency (依存性) Injection (注入) になります。Dependency Injection とインターフェースを組み合わせて実装を入れ替える仕組みを入れることで、クラス単体でテストが可能になります。やったね。

例え、1 回呼ぶのに 100 万円かかるような Web API があったとしても、今回のように呼び出す処理を行うクラスに対して interface を定義しておいて、実装を差し替えるようにすれば OK ですね。

interface IReallyExpensiveService
{
    string Call();
}

class ReallyExpensiveService: IReallyExpensiveService
{
    public string Call()
    {
        // 1 回呼ぶのに 100 万円の外部サービスを呼んでるとする
    }
}

class MockReallyExpensiveService : IReallyExpensiveService
{
    public string Call()
    {
        // テストのときはテスト用の結果を返す実装でいいよね
        return "テスト用の結果";
    }
}

すべて解決?

こんな感じで、テスト時に差し替え可能に出来たほうが嬉しいものに対して interface を定義して実装を外部から設定するようにするといい感じですが、クラスの利用者側視点から見ると割と最悪です。例えば Aggregator を使う場合は Aggregator の他に DataGenerator を new しないといけないです。

以下のような感じですね。

var aggregator = new Aggregator(new DataGenerator());

もうちょっと複雑になってくると、1 つのオブジェクトを組み立てるのに、こんな風にコードを書かないといけなくなるかもしれません。

var x = new ProductOrderService(
  new ProductRepository(
    new PostgreSQLConnectionProvider(
      new SettingsProvider(),
    )
  ),
  new OrderManager(
    new CustomerManager(
      new CustomerDataAccessor(new SettingsProvider()),
    )
  ),
);

ちょっとこれをやるのは現実的ではないですね…。

DI コンテナ

ということで、オブジェクトの依存関係を外部から設定するようにすると、オブジェクトを組み立てるのが大変になるので、そこを省力化しようというライブラリが登場してきます。

それが DI コンテナです。ほとんどの DI コンテナは、このインターフェースには、この実装クラスを使ってくれというルールを設定しておくと、いい感じにルールに従ってオブジェクトを組み立ててくれます。

イメージとしては以下のような感じです。

var c = new Container(); // Container クラスが DI コンテナだとする

// アプリの起動時あたりで、以下のようにアプリで使うインターフェースと実装クラスを登録していく
c.RegisterType<ISettingsProvider, SettingsProvider>();
c.RegisterType<IDbConnectionProvider, IPostgreSQLConnectionProvider>();
c.RegisterType<IProductRepository, ProductRepository>();
c.RegisterType<ICustomerDataAccessor, CustomerDataAccessor>();
c.RegisterType<ICustomerManager, CustomerManager>();
c.RegisterType<IOrderManager, OrderManager>();
c.RegisterType<IProductOrderService, ProductOrderService>();

// インスタンスが欲しいときは、コンテナから取得
var productOrderService = c.Resolve<IProductOrderService>();

こうすることで、DI コンテナがコンストラクターの引数とかから依存先を解決していって綺麗に組み立てたオブジェクトを返してくれます。便利。

実際にはフレームワークに DI コンテナが組み込まれていることが多くてコンテナのインスタンスを自分で作ったり、コンテナから自分でインスタンスを明示的に取得することは少ないです。利用者がするのは、インターフェースと実装クラスの対応を登録するだけというのが多いです。

まとめ

DI を何故するのか?ということでさくっとまとめましょう。

  • 単体テストを容易にするためにインターフェースに依存するようにクラスを作る
  • 依存先の実装はコンストラクターなどで外部から渡してもらうようにする
  • ↑のように作るとオブジェクトの組み立てが大変になるので、それを省力化するための DI コンテナがある

という感じなので、個人的な感想としては DI しなくても単体テストさえ可能で現実的な労力で実装可能な方法があれば、別に DI コンテナとか DI 使わなくてもいいかなぁとは思ってます。
ただ、黒魔術的なことをしない限りは C# などの静的な型付け言語だとめんどくさいので、今のところおとなしく DI を使うのが現実的です。

おまけとして、DI コンテナを使うことでオブジェクトの生成時に付加価値を追加するようなことも出来るので、フレームワークを提供する側から見ても便利だったりします。まぁ、それは今回の主題とは関係ないのでまた今度。

追記

DI って何でするのかわからない人向けに頑張って説明してみる「本来の意味」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む