2009年12月28日月曜日

MapReduceをJUnitでTDDで作る(9 さらにリファクタリング)

こんばんは、yoshitsuguです。もう、年の瀬ですね。
2009年は皆さんにとってどんな1年だったでしょうか?

私は怒濤のように過ぎていった1年でした。

さてさて、
前回、テストを追加して、JUnitによるテストがGREENになるところまで実行しました。
TDDの開発は、RED→GREEN→Refactoringの繰り返しであるというのは最初の頃に述べました。
ということで、再びリファクタリングしましょう。

LogAnalysisMapper.java
package jp.co.littel.hadoop;

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

/**
 * ログ解析Mapper
 * @author yoshitsugu
 */
public class LogAnalysisMapper extends Mapper {

    /**
     * map処理を実行します。
     * @param key キー
     * @param value 値
     * @param cotext コンテクスト
     */
    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        if (value.toString().contains("INFO")) {
            context.write(value, new LongWritable(1));
        }
    }
}

このプログラムで気になるのは、"INFO"というリテラル値がハードコーディングされている点ですね。
Eclipseのリファクタリング機能でpublic定数化してしまいましょう。

    /** ログ種別:情報 */
    public static final String INFO = "INFO";

このように定数ができました。public定数化した理由は、以下INFOを用いる際にはこのクラスを参照させればよくなるからです。(それが他のクラスに強く依存している場合は除きます)

続いてテストクラスもリファクタリングしてしまいましょう。

TestLogAnalysisMapper.java
package jp.co.littel.hadoop;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Matchers.any;


import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.internal.verification.Times;

/**
 * ログ解析Mapperのテストクラスです。
 * @author yoshitsugu
 */
public class TestLogAnalysisMapper {

    /**
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() throws Exception {
    }
    /**
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
    }

    /**
     * 抽出するパターン
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract() throws Exception {
        Mapper.Context context = mock(Mapper.Context.class);
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu"
        };
        LogAnalysisMapper mapper = new LogAnalysisMapper();
        mapper.map(null, new Text(lines[0]), context);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
    }
   
    /**
     * 抽出する行が複数の場合
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract_ComplexRows() throws Exception {
        Mapper.Context context = mock(Mapper.Context.class);
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,341 ERROR fatal @ exception has occurred",
        };
        LogAnalysisMapper mapper = new LogAnalysisMapper();
       for (String line : lines) {
         mapper.map(null, new Text(line), context);
       }
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));
    }
}

気になるのは黄色く着色した部分が繰り返している点と、逆に同じロジックの筈なのに異なっているオレンジの着色をした部分ですね。

contextとmapperオブジェクトはテストクラスのフィールドとして持たせても何ら問題ないと思われます。現在のテストの形なら各メソッドで一度ずつインスタンスを生成してそれきりだからです。
オレンジの部分は2つめのメソッドのロジックで統一が可能だと言うことに気がつきます。

では、そのように修正してみましょう。

Eclipseの機能をフル活用します。
testMapExtract()メソッド内のcontextを選択して、「リファクタリング」→「ローカル変数をフィールドに変換」を実行します。初期化はフィールド宣言を選択します。
 フィールド宣言で初期化され、testMapExtract()メソッド内の記述は消えました。
また、testMapExtract_ComplexRows()メソッド内のcontextのインスタンス生成処理は冗長なので削除してしまいましょう。

同じく、testMapExtract()メソッドのmapperオブジェクトもフィールドに変換してしまいます。mapperオブジェクトの初期化もフィールド宣言で行ってしまいましょう。
また、同様にtestMapExtract_ComplexRows()メソッド内のmapperオブジェクトの初期化も削除してしまいましょう。

あとはオレンジの部分ですね。
まず、testMapExtract_ComplexRows()メソッド内のfor文を選択し、リファクタリング→メソッドの抽出を実行します。メソッド名はmapでよいでしょう。
for文がmap()メソッドで置き換えられました。

mapper.map()処理を呼び出している部分もmap()メソッドで置き換えてしまいます。

リファクタリング完了。実行してみましょう。

見事にGREENのままです。


今回はここまで。

ここまでの修正で、ソースは次のようになりました。

LogAnalysisMapper.java

package jp.co.littel.hadoop;

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

/**
 * ログ解析Mapper
 * @author yoshitsugu
 */
public class LogAnalysisMapper extends Mapper {

    /** ログ種別:情報 */
    public static final String INFO = "INFO";

    /**
     * map処理を実行します。
     * @param key キー
     * @param value 値
     * @param cotext コンテクスト
     */
    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        if (value.toString().contains(INFO)) {
            context.write(value, new LongWritable(1));
        }
    }
}
TestLogAnalysisMapper.java


package jp.co.littel.hadoop;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Matchers.any;

import java.io.IOException;


import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.internal.verification.Times;

/**
 * ログ解析Mapperのテストクラスです。
 * @author yoshitsugu
 */
public class TestLogAnalysisMapper {

    private Mapper.Context context = mock(Mapper.Context.class);
    private LogAnalysisMapper mapper = new LogAnalysisMapper();

    /**
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() throws Exception {
    }

    /**
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
    }

    /**
     * 抽出するパターン
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract() throws Exception {
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu"
        };
        map(lines);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
    }
   
    /**
     * 抽出する行が複数の場合
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract_ComplexRows() throws Exception {
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,341 ERROR fatal @ exception has occurred",
        };
        map(lines);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));
    }

    /**
     * map処理を呼び出します。
     * @param lines 行の配列
     * @throws IOException
     * @throws InterruptedException
     */
    private void map(String[] lines) throws IOException, InterruptedException {
        for (String line : lines) {
            mapper.map(null, new Text(line), context);
        }
    }
}

ではでは、よいお年をお迎えください!!

1 件のコメント:

  1. こんばんは。

    リファクタリングの仕方、勉強になります!

    返信削除