標準出力の倒し方

前回は

ローカル変数への書き込みを新しい変数を宣言し初期化することで倒せることを示した。

現在のコードは以下のようになっている

Option Explicit On
Option Strict On
Option Infer On

Module Program
    Sub PrintApplePrice(applePrice As Integer)
        Console.WriteLine("{0}円", applePrice)
    End Sub

    Function UpdateApplePrice(originalPrice As Integer) As Integer
        Console.Write("{0}円から、", originalPrice)
        Dim updatedPrice = 80
        Console.WriteLine("{0}円に更新", updatedPrice)
        Return updatedPrice
    End Function

    Sub Main()
        PrintApplePrice(100)

        Dim originalPrice = 100
        Dim updatedPrice = UpdateApplePrice(originalPrice)

        PrintApplePrice(updatedPrice)
    End Sub
End Module

今回は

みなが気づかない標準出力の性質について説明したいと思う。

標準出力というのは実はグローバル変数である。そして、標準出力への書き込みと言うのはプログラム側からは非常に扱いにくい副作用でもある。悪しきものの組み合わせだ。

標準出力はグローバル変数

C言語の影響を受けた言語であれば `stdout` としても知られる標準出力は実は悪しきグローバル変数である。`Console.WriteLine()`  という呼び出しは、イメージとしては `stdout.WriteLine()` であり、この `stdout` に当たるものがグローバル変数として存在している。(`Console` は C# でいう `static` クラスで、暗にグローバル変数相当であることを示している)

この `stdout` は、具体的には `TextWriter` 型の `Console.Out` で取得するか、 `Stream` 型の `Console.OpenStandardOutput()` で取得する。

標準出力への書き込みは副作用

過去に今日や現在時刻といった動いている時間というのはテストしづらい副作用であることを書いた

同様に標準出力への書き込みと言うのもテストしづらい副作用である。コンソールに特定の文字列が出たことをどのようにテストするのだろう?目視?画像認識?まさかね。

プログラムを改善する

グローバル変数を倒す方法は、メソッドの引数にコピーして渡すことだった。`stdout` に対してもそうしよう。今までのコードには `Console.Write` や `Console.WriteLine` が使われていた。これらをコピーした `stdout` に相当するものに対してメソッドを呼び出す形にする。

副作用を倒すために出力先を抽象化する。現在は固定でコンソールに出力されるのでテストができない形になっている。これがプログラムの中で扱える文字列として出力できたら、単純な文字列の比較としてテストできる。コンソールでもいいし、文字列でもいいような便利な出力先として `TextWriter` を使える。

コードは以下のようになる

Option Explicit On
Option Strict On
Option Infer On

Imports TextWriter = System.IO.TextWriter

Module Program
    Sub PrintApplePrice(applePrice As Integer, textWriter As TextWriter)
        textWriter.WriteLine("{0}円", applePrice)
    End Sub

    Function UpdateApplePrice(price As Integer, textWriter As TextWriter) As Integer
        textWriter.Write("{0}円から、", price)
        price = 80
        textWriter.WriteLine("{0}円に更新", price)
        Return price
    End Function

    Sub Main()
        Dim textWriter = Console.Out

        PrintApplePrice(100, textWriter)

        Dim price = 100
        Dim updatedPrice = UpdateApplePrice(price, textWriter)

        PrintApplePrice(updatedPrice, textWriter)
    End Sub
End Module

もはや `PrintApplePrice` と `UpdateApplePrice` は `stdout` に直接依存していない。

テストしづらい副作用はどうなっただろうか?例として、以下のような感じでテストすることができる。文字列の比較の形でテストできるように `Console.Out` を `New StringWriter()` に置き換えた。

    Function TestTextWriterOutput(textWriter As TextWriter) As Boolean
        Dim expected =
"100円
100円から、80円に更新
80円
"
        Dim actual = textWriter.ToString()
        Return expected = actual
    End Function

    Sub Main()
        Dim textWriter = New StringWriter()

        PrintApplePrice(100, textWriter)

        Dim price = 100
        Dim updatedPrice = UpdateApplePrice(price, textWriter)

        PrintApplePrice(updatedPrice, textWriter)
        Console.WriteLine($"出力のテストは?{If(TestTextWriterOutput(textWriter), "成功!", "失敗……")}")
    End Sub

今までの `PrintApplePrice` と `UpdateApplePrice` メソッドのように、`Console` クラスを直接使っていたらできなかった芸当だ。

いいなと思ったら応援しよう!