S-JIS[2011-07-17] 変更履歴

Scala デバッグログクラスを作ってみる

Scalaでデバッグログ出力を行うクラスを作ってみる。

 

概要

デバッグログ出力を行う仕組みを考えてみる。

デバッグログのメッセージは、開発時(テスト時)には出力するけれども本番環境では出力しないようにしたい。
この制御はデバッグログ出力メソッド内で判定する。

デバッグログを出力する為のメソッドは、使う側からすると、自分のところにあるメソッド(あるいはオブジェクト)として呼び出せるようにしたい。

  def myMethod() = {
    debug("メッセージ")
  }
  def myMethod() = {
    DEBUG.write("メッセージ")
  }

こういったデバッグ出力メソッドを、色々なクラスから使えるようにしたい。


メソッド方式

    debug("メッセージ")

メソッドを別の場所で定義して取り込む方法を考えてみる。

Scala Java 備考
trait Debug {
  def debug(s: String) {
    println(s)
  }
}
class Use extends Debug {
  def myMethod() = {
    debug("メッセージ")
  }
}
class Use2 extends Use {
  def myMethod2() = {
    debug("メッセージ2")
  }
}
  トレイトにメソッドを定義し、使うクラス側はそのトレイトをミックスインする方法。
(トレイトはJavaのインターフェースに相当するものだが、実装を持てるところが大きく異なる)

何段階か継承しても、あるいは継承先クラスでさらにDebugをミックスインしても問題ない。
object Debug {
  def debug(s: String) {
    println(s)
  }
}
import Debug._

class Use {
  def myMethod() = {
    debug("メッセージ")
  }
}
import Debug._

class Use2 extends Use {
  def myMethod2() = {
    debug("メッセージ2")
  }
}
public class Debug {
  public static void debug(String s) {
    System.out.println(s);
  }
}
import static Debug.*;

public class Use {
  public void myMethod() {
    debug("メッセージ");
  }
}
import static Debug.*;

public class Use2 extends Use {
  public void myMethod2() {
    debug("メッセージ2");
  }
}
object内にメソッドを定義し、メソッドをインポートする方法。
このインポート方法は、Javaのstaticインポートに相当する。
package object sample {
  def debug(s: String) {
    println(s)
  }
}
package sample

class Use {
  def myMethod() = {
    debug("メッセージ")
  }
}
package sample

class Use2 extends Use {
  def myMethod2() = {
    debug("メッセージ2")
  }
}
  パッケージオブジェクトにメソッドを定義する方法。
そのパッケージ内のクラスであれば、自由にメソッドを呼び出せる。

オブジェクト方式

    DEBUG.write("メッセージ")

DEBUGオブジェクトを取り込む方法を考えてみる。

Scala Java 備考
object DEBUG {
  def write(s: String) {
    println(s)
  }
}
import DEBUG

class Use {
  def myMethod() = {
    DEBUG.write("メッセージ")
  }
}
public class DEBUG {
  public static void write(String s) {
    System.out.println(s);
  }
}
import DEBUG;

public class Use {
  public void myMethod() {
    DEBUG.write("メッセージ");
  }
}
オブジェクトを定義し、それをインポートする方法。
(デバッグオブジェクト内で呼び出し元クラス名を保持したりしたい場合は
呼び出し元クラス毎にインスタンスを作る必要があるので、
この方法では駄目)
class Debug {
  def write(s: String) {
    println(s)
  }
}
class Use {
  val DEBUG = new Debug

  def myMethod() = {
    DEBUG.write("メッセージ")
  }
}
class Use2 extends Use {
//val DEBUG = new Debug

  def myMethod2() = {
    DEBUG.write("メッセージ2")
  }
}
public class Debug {
  public void write(String s) {
    System.out.println(s);
  }
}
public class Use {
  final Debug DEBUG = new DEBUG();

  public void myMethod() {
    DEBUG.write("メッセージ");
  }
}
クラスを定義し、使う側でインスタンス化する方法。
(普通はファクトリークラスを使ってインスタンス化する)

使う側で継承したクラスを作った場合、
親クラスでDebugをインスタンス化していればそれをそのまま使えるが、
親クラス側でインスタンス化しているかどうかを確認しなければらなない。
子クラスでもDebugをインスタンス化すると、
親クラス側でインスタンス化していたらDebugインスタンスが2つ出来てしまう。
trait Debug {
  object DEBUG {
    def write(s: String) {
      println(s)
    }
  }
}
class Use extends Debug {
  def myMethod() = {
    DEBUG.write("メッセージ")
  }
}
class Use2 extends Use with Debug {
  def myMethod2() = {
    DEBUG.write("メッセージ2")
  }
}
  トレイトを定義し、その中でDEBUGオブジェクトを生成しておく。
使う側はそのトレイトをミックスインする。

使う側で継承したクラスを作った場合、
子クラス側でさらにDebugトレイトをミックスインしても
トレイト部分は1個分しか作られない(初期化されない・インスタンス化されない)。
class Debug {
  def write(s: String) {
    println(s)
  }
}
object Use {
  val DEBUG = new Debug
}
class Use {
  def myMethod() = {
    DEBUG.write("メッセージ")
  }
}
object Use2 {
  val DEBUG = new Debug
}
class Use2 extends Use {
  def myMethod2() = {
    DEBUG.write("メッセージ2")
  }
}
  コンパニオンオブジェクトでDEBUGオブジェクトを用意する。
(コンパニオンは、classと同名のobjectを同一ソースファイル内に書くもの)

各クラス内でDebugをインスタンス化すると個別にインスタンスが作れるが、
コンパニオンオブジェクト内ならシングルトンなのでVM内で1つのインスタンスとなる。

メソッド定義(呼び出し方に着目)

デバッグメッセージ出力用のメソッドは、普通に書くと値渡しになる。つまり、メソッド呼び出し前に引数が評価されて、結果の値がメソッドに渡される。

//例:Mapの中身をデバッグログ出力する
  val map = Map(〜)
  DEBUG.write(map.toString) //常にtoStringが実行されてwriteが呼ばれる

しかしデバッグログは本番環境では出力されないので、map.toStringの実行が無駄・邪魔になる。
そこで、本番環境かどうか(デバッグログ出力を行うかどうか)を判定し、事前に引数の評価(map.toString)を行わないようにしたい。

C言語の場合
#if デバッグ出力あり
#define DEBUG(s) printf("%s\n", s)
#else
#define DEBUG(s)
#endif
  DEBUG("メッセージ");
デバッグ出力ルーチン自体をマクロ化し、
コンパイル時点でデバッグ出力を行うかどうかを判定する。
デバッグ出力なし状態であれば、DEBUG呼び出し自体が消える。
C言語の場合
#define DEBUG(s) do { \
  if (デバッグ出力あり) { \
    printf("%s\n", s); \
  } \
} while(0)
  DEBUG("メッセージ");  
デバッグ出力ルーチンをマクロ化し、マクロ内に判定のif文を入れる。
コンパイルのプリプロセスが終わった時点では、
各デバッグログ出力呼び出しが全てif文付きで展開されることになる。
Javaの場合
  if (デバッグ出力あり) {
    DEBUG.write("メッセージ");
    DEBUG.write(map.toString());
  }
Javaにはマクロというものが無いので、
判定用のif文自体をデバッグログ出力呼び出しに加える必要がある。
Scalaの場合
object DEBUG {
  def write(s: =>String) {
    if (デバッグ出力あり) {
      println(s)
    }
  }
}
  DEBUG.write("メッセージ")
  DEBUG.write(map.toString)
Scalaの場合、メソッドの引数の型が「String」だとStringの値渡しだが
「=>String」にすると名前渡しになる。
すなわち遅延評価に変わる(メソッドの中で実際に使われるまで評価されない)。

例えば「map.toString」という式そのものを渡している形になるので、
デバッグ出力あり状態ならprintlnの時にmap.toStringが実行され、
デバッグ出力なし状態であればmap.toStringは実行されない。

まとめ

Scalaでは共通な処理はトレイトに書けるので、デバッグログ出力メソッドもトレイトに書いておいてミックスインするのが良さそう。
コンパニオンオブジェクトを使う方法も面白いが、対象クラス全てにコンパニオンオブジェクトを用意するのは面倒そう。
 パッケージオブジェクトも同様)

デバッグログ出力メソッドの引数は名前渡しにするのが良い。


Scala実験へ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま