ポリモーフィズムの例をもうちっと実用的に書いてみた。


参照元を書き忘れて、トラバが飛んでなかった。元ネタはこちら。http://d.hatena.ne.jp/j5ik2o/20080508/1210246936

Jiemamy作者っぽく、テーブルデータをSQLに変換するプログラムを例に。

モデル

複雑にしないために、わざとpublicフィールド。

import java.util.ArrayList;
import java.util.List;

public class Table {
  public String name;
  public List<Column> columns = new ArrayList<Column>();

  public Table(String name) {
     this.name = name;
  }
}
public class Column {
  public String name;
  public String type;

  public Column(String name, String type) {
    this.name = name;
    this.type = type;
  }
}

で、こんな感じで作ったモデルをCREATE TABLE文に直したい。

Table table = new Table("T_HOGE");
table.columns.add(new Column("ID", "integer"));
table.columns.add(new Column("CONTENTS", "string"));

こんな風に。

CREATE TABLE T_HOGE(
 ID INT,
 CONTENTS TEXT
);

ロジック部分(ポリモ未使用版)

public class MySQLConverter {

  public String convert(Table table) {
    StringBuilder sb = new StringBuilder();
    sb.append("CREATE TABLE ").append(table.name).append("(\n");
    for(Column column : table.columns) {
      sb.append("  ").append(column.name).append(" ");
      if("integer".equals(column.type)) {
        sb.append("INT");
      } else if("string".equals(column.type)) {
        sb.append("TEXT");
      } else {
        sb.append(column.type);
      }
      sb.append(",\n");
    }
    sb.delete(sb.length()-2, sb.length()-1);
    sb.append(");\n");
    return sb.toString();
  }
}
public class PostgreSQLConverter {

  public String convert(Table table) {
    StringBuilder sb = new StringBuilder();
    sb.append("CREATE TABLE ").append(table.name).append("(\n");
    for(Column column : table.columns) {
      sb.append("  ").append(column.name).append(" ");
      if("integer".equals(column.type)) {
        sb.append("INTEGER");
      } else if("string".equals(column.type)) {
        sb.append("VARCHAR(32)");
      } else {
        sb.append(column.type);
      }
      sb.append(",\n");
    }
    sb.delete(sb.length()-2, sb.length()-1);
    sb.append(");\n");
    return sb.toString();
  }
}

という個別のコンバータクラスを用意して、こんな感じになるかと。

public class BusinessLogic {

  public void doBusiness(String database, Table table) {
    if(database.equals("MySQL")) {
      MySQLConverter converter = new MySQLConverter();
      String sql = converter.convert(table);
      System.out.println(sql);
    } else if(database.equals("PostgreSQL")) {
      PostgreSQLConverter converter = new PostgreSQLConverter();
      String sql = converter.convert(table);
      System.out.println(sql);
    } else {
      throw new IllegalArgumentException("そのデータベース知らない: " + database);
    }
  }
}
public class Main {

  public static void main(String[] args) {
    Table table = new Table("T_HOGE");
    table.columns.add(new Column("ID", "integer"));
    table.columns.add(new Column("CONTENTS", "string"));
    new BusinessLogic().doBusiness("MySQL", table);
  }
}

この doBusinessん中のif〜else ifをなんとかしたい。後でDBが増えるたびにこの列が増えるの、嫌ですよね。

ロジック部分(ポリモ版)

そこでこんなインターフェイスを用意してみる。

public interface Converter {
  String convert(Table table);
}

で、各DB用のコンバータはこいつをimplements

public class PostgreSQLConverter implements Converter {
  // 同上なので略
}
public class MySQLConverter implements Converter {
  // 同上なので略
}

そうすると、BusinessLogic部分がこんな風に書ける。

public class BusinessLogic {

  private Converter converter;

  public void doBusiness(String database, Table table) {
    if(database.equals("MySQL")) {
      converter = new MySQLConverter();
    } else if(database.equals("PostgreSQL")) {
      converter = new PostgreSQLConverter();
    } else {
      throw new IllegalArgumentException("そのデータベース知らない: " + database);
    }
    String sql = converter.convert(table);
    System.out.println(sql);
  }
}

ちょっとすっきりした。けどまだifが残ってるよね。

ロジック部分(ファクトリ導入版)

if文を「ファクトリ」というクラスに追い出してみる。

public class ConverterFactory {
  public static Converter createConverter(String database) {
    Converter converter;
    if(database.equals("MySQL")) {
      converter = new MySQLConverter();
    } else if(database.equals("PostgreSQL")) {
      converter = new PostgreSQLConverter();
    } else {
      throw new IllegalArgumentException("そのデータベース知らない: " + database);
    }
    return converter;
  }
}
public class BusinessLogic {

  private Converter converter;

  public BusinessLogic(Converter converter) {
    this.converter = converter;
  }

  public void doBusiness(Table table) {
    String sql = converter.convert(table);
    System.out.println(sql);
  }
}
public class Main {

  public static void main(String[] args) {
    Converter converter = ConverterFactory.createConverter("MySQL");
    Table table = new Table("T_HOGE");
    table.columns.add(new Column("ID", "integer"));
    table.columns.add(new Column("CONTENTS", "string"));
    new BusinessLogic(converter).doBusiness(table);
  }
}

ほらスッキリした。これで、対応するDatabaseの種類が増えたときは、新しいConverterを書いて、ファクトリだけを修正すれば、メインのロジックに変更が及ばない。これがファクトリ+ポリモのコンボ。

「まぁメリットがあるのは分かった。でも、結局ifをファクトリに移動させただけじゃね?」

という話は、DIのお話へと続きます。。。反響あったら書くw