- 投稿日:2020-05-20T23:53:27+09:00
Javaの例外処理?
0. 始めに
例によって、Javaの例外が分かっていなかったので、まとめてみた。
今回は(こそ?)自分用メモとして記事を書く。1. 例外処理とは?
例外の定義も色々あったので、参考1 p.264 から引用したところ、
Javaにおけるプログラムの実行中に発生するトラブルには、大きく分けて2つの種類があります。
実行環境のトラブルなど、プログラムからは対処しようのない事態を表すエラーと、プログラムが対処できる例外の2つです。例外はさらに、検査例外と非検査例外に分かれます。検査例外とは、例外処理を記述したかをコンパイラが検査する例外を指します。
もう一方の非検査例外は、例外処理を記述したかどうかをコンパイラが検査しない例外を指します。とのこと。要するに3つに分けられる。
さらにそれぞれの例外に応じた例外クラスが存在し、その関係は下記の通り。(サブクラスも載せたい)
2. 各例外の基本
①検査例外
検査例外のクラスであるExceptionを継承している例外クラスは、try-catchやthrows句で宣言するなどの処理が必須になる。
ここで言う検査とは、catchやthrowのこと。検査例外を検査していない場合はコンパイルエラーになる。②非検査例外
Exceptionのサブクラスでも、RuntimeExceptionを継承していれば非検査例外として扱われ、try-catchなどの処理は不要になる。
もちろん、例外処理を記載しても問題ない。③エラー
メモリ不足やファイルの読み込みに失敗したなどの例外。非検査例外と同じく、例外処理とかしなくていい。
3. 例外処理書き方あれこれ
3.1 標準的な書き方
①Exceptionをキャッチする
明示的に例外をスローするときは、throw
を付ける(検査例外のみ?検証する)
catch
ブロックのパラメータはThrowable
なので、全ての例外をキャッチできる。try { // 例外が発生するかもしれない処理をくくる throw new Exception(); } catch (Exception e) { ... // try 中で例外が発生した時に処理される } finally { // 例外が発生してもしなくても実行される }②Exceptionをスローする メソッド宣言にthrowsを使う。
private void hogehoge() throws Exception { throw new Exception(); }3.2 マルチキャッチ
キャッチする例外クラスを複数指定できる。参考2より引用させていただきました。
// Java6以前の場合 try { . . . } catch (IOException ex) { logger.log(ex); throw ex; } catch (SQLException ex) { logger.log(ex); throw ex; } // Java7以降の場合 try { . . . } catch (IOException|SQLException ex) { logger.log(ex); throw ex; }3.3 検査例外のスローを記載したら、絶対にキャッチも書く
検査例外をスロー宣言しているメソッドは、キャッチするか再スローしないとコンパイルエラーになる。。。
public void method() throws Exception {} // case1 try { method(); } catch (Exception e) { // catchが無いとエラー ... } // case 2 public void method2() throws Exception { // 再スローしないとエラー method(); }4. 例外発生後の挙動
4.1 for文中で例外が発生したら、それ以降のループ処理はされない
参考1 p.349より引用。
i = 1
の時にStirngIndexOutOfboundsException
が発生するので、i > 1
のループ処理は実行されない。String[] array = {"abcde", "fgh", "ijk"}; String[] array2 = new String[3]; int i = 0; try { for (String s: array) { array2[i] = s.substring(1, 4); i++; } } catch (Exception e) { System.out.println("Error"); } for (String s: array2) { System.out.println(s); } // 実行結果 // Error エラーをキャッチしたことによる出力 // bcd array2[0] // null array2[1] // null array2[2]4.2 例外をスローしているメソッドをオーバーライドする時の注意
例外をスローしているメソッドをオーバーライドした場合、そのメソッドには以下の制約が追加される(参考3 p. 299を元に一部表現を変えて引用)。
①サブクラスのメソッドでスローできる例外は、スーパークラスのメソッドがスローする例外と同じかそのサブクラスの例外
②スーパークラスのメソッドがどんな例外をスローしていようと、サブクラスのメソッドではRuntimeException
かそのサブクラスの例外としてスロー出来る(実行時例外に倒せる)
③そもそも、スーパークラスのメソッドが例外をスローしているからと言って、サブクラスのメソッドでスローするのが必須ではない具体例を出す(参考3 p.300より引用)。ここで、NG:コンパイルエラーとする。
class Super { void method() throws IOException {} } class SubA { void method() } // OK 例外をスローしなくても良い class SubB { void method() throws FileNotFoundException {} } // OK IOExceptionのサブクラスでスローしている class SubC { void method() throws Exception {} } // NG IOExceptionのスーパークラスでスローしている class SubD { void method() throws ClassNotFoundException {} } // NG IOExceptionと継承関係にない例外クラスでスローしている class SubE { void method() throws RuntimeException {} } // OK 実行時例外に倒している5. 参考
1.志賀澄人 (2019) 「徹底攻略 Java SE 8 Silver 問題集 [1Z0-808] 対応」 ㈱インプレス 発行
2.【社内勉強会】Javaの例外処理(2017/04/26)
3.山本道子 (2015) Java プログラマ Silver SE 7(第5刷発行) ㈱翔泳社 発行
- 投稿日:2020-05-20T23:03:19+09:00
部分和問題から再帰的なプロセスを網羅する
0. はじめに
深さ優先探索を行うときには、再帰的な書き方をしなければいけない場面が多々あると思います。
実際、他人の書いたコードを見てみると中でreurn文が多用されており、どの順番でコードが動いているのか私には全然分かりませんでした。そこで今回は、具体的にコードの動きを観察します。1 . 部分和問題
実際の問題はけんちょんさんの再帰関数を学ぶと、どんな世界が広がるかの記事の3-1を参考にしてください。
ちなみにjavaでのコードはこうなります。
import java.util.Scanner; public class Main { static int indexA = 0; static int indexB = 0; static boolean func(int i, int x, int[] a) { //ベースケース if(i == 0) { if(x == 0) { return true; }else { return false; } } //a[i-1]を選ばない場合(func(i-1,x,a)がokならok) if(func(i-1,x,a)) { return true; } System.out.println("indexA:"+(++indexA)+" i:"+i+" x:"+x); //a[i-1]を選ぶ場合(func(i-1,x-a[i-1],a)がokならok) if(func(i-1,x-a[i-1],a)) { return true; } System.out.println("indexB:"+(++indexB)+" i:"+i+" x:"+x); //どっちもダメだったらダメ return false; } public static void main(String[] args) { Scanner stdIn = new Scanner(System.in); System.out.print("コインの枚数:"); int n = stdIn.nextInt(); int[] a = new int[n]; System.out.println("コインの価値:"); for(int i = 0; i < n; i++) { a[i] = stdIn.nextInt(); } System.out.print("金額の設定:"); int x = stdIn.nextInt(); //再帰的に書く if(func(n,x,a)) { System.out.println("Yes"); }else { System.out.println("No"); } } }1.1 n = 3, a =(2,3,4) x = 6 のとき
なぜこのような順番でコードが出てきたかを以下の図で確認できます。
〇の中に書かれた順番で動いているのが分かると思います。
※ここからさらに詳細な動きを書くので、理解できた方は次の見出しまでスキップしてください。
iが0のとき、xが0でない場合は、すべてfalseが返るので、①のfunc(0,6)はfalseが返る、そのためfunc(1,6)のとき、の最初の1個目のif文はif(false)であるため、何も何も表示されない。
if(func(i-1,x,a)) { //ここが表示されない System.out.println("選ばない"+" i:"+i+" x:"+x); return true; }次に, i==1,x == 6を今見ているので、表示されているのが分かる。
System.out.println("indexA:"+(++indexA)+" i:"+i+" x:"+x+"\n"); //1行目に ""indexA:1 i:1 x:6""と表示されているこの後、func(1,6)の2つ目のif文の②が動作しており、func(0,4)はfalseであるため、このif文も表示されない。
if(func(i-1,x-a[i-1],a)) { //ここが表示されない System.out.println("選ぶ"+" i:"+i+" x:"+x); return true; }そして、そのif文の後のprintlnが動作していることが分かる。(②のindexB:1 i:1 x:6が表示されている)
System.out.println("indexB:"+(++indexB)+" i:"+i+" x:"+x+"\n");func(1,6)のどちらのif文もfalseであったため、//どっちもダメだったらダメと書かれた行のreturn falseが返ってくる。
ここでやっとfunc(2,6)の最初のif文が終わったことを意味するのである。
再帰はベースケースまでたどり着いてから戻り値がどんどん返ってくるイメージを持つと分かりやすいだろう。
1.2 n = 3, a =(2,3,4) x = 10 のとき
ここで絶対x=10の目標には達しないときの動きも確認する。
深さ優先探索が行われていることがよくわかると思う。先程と同じようなコードの動きをしているので説明は省略する。
2 まとめ
再起関数を用いるときは、まず ベースとなるケースを考えて循環を止める。
その後、if文内で再帰を行い、trueになるケースが一つでも存在するかを確かめた。bit全探索もとる、とらないの2^n通りのケースを考えられるため、今回の問題であればbit全探索でも再帰的に書いても良いとは思います。
しかし、if文でさらに分岐した条件を書くことができる点が再帰のさらなる魅力だと思います。(まだ使ったことはありませんが...。)
- 投稿日:2020-05-20T22:23:43+09:00
JavaのBigDecimal
BigDecimalについて少しまとめた
BigDecimalはJavaのAPIの一つで、
普通の数値型は2進数で処理されるため、意図しない数値が返って来ることがある。
しかし、BigDecimalを使うことで、10進数として扱うことができる。Number.javaimport java.math.BigDecimal; import java.math.RoundingMode; public class Number { public static void main(String[] args) { BigDecimal number1 = new BigDecimal("0.2"); //引数は""で囲む BigDecimal number2 = new BigDecimal("4"); System.out.println(number1); System.out.println(BigDecimal.ZERO); //0 System.out.println(BigDecimal.ONE); //1 System.out.println(BigDecimal.TEN); //10 System.out.println(number1.add(number2)); //加算 number1 + number2 System.out.println(number1.subtract(number2)); //減算 number1 - number2 System.out.println(number1.multiply(number2)); //乗算 number1 * number2 System.out.println(number1.divide(number2, 3, RoundingMode.UP)); //除算 number1 / number2, 小数点第3位まで表示, 切り上げ //RoundingMode.は切り上げ、切り捨て、四捨五入などがある。 BigDecimal number3 = new BigDecimal("0.22"); BigDecimal value1 = number3.scaleByPowerOfTen(2); //10の2乗 System.out.println(value1); //22 BigDecimal value2 = number3.scaleByPowerOfTen(-2); //10の-2乗 System.out.println(value2); //0.0022 BigDecimal value3 = number3.negate(); //マイナス化 System.out.println(value3); //-0.22 } }以上。
まだまだたくさんの機能があるので試してみたいです。
- 投稿日:2020-05-20T20:20:24+09:00
Javaで1件だけ検索するSQL文の結果の条件分岐
最近 JDBC (Java Database Connectivity) の勉強を少しだけしました。
データベースから1件だけ検索し、その結果を取得するときの条件分岐の仕方で困ったのでメモとして残しておきます。Connection con = null; Statement stmt = null; ResultSet res = null; String sql = "SELECT * FROM DATABASE WHERE COLUMN = 〇〇"; try{ con.DriveManager.getConnection(url, user, password); stmt.con.createStatement(); res.stmt.executeQuery(sql); if(res.next()){ //結果があるときの処理 }else{ //結果がない時の処理 }catch(SQLException e){ //SQL文が間違っていた時などの処理 }finally{ //クローズ処理など }
- 投稿日:2020-05-20T17:40:14+09:00
java(コンストラクタ)
コンストラクの定義方法
①メソッド名がクラス名と完全に等しい
②メソッド宣言に戻り値が記述されていない(voidもダメ)生まれた直後の動作を定義したHeroクラス
public class Hero { String name; int hp; public Hero() { //コントラストの定義 this.hp = 100; // hpフィールドを100で初期化 } }newされた直後に自動的に実行される処理を書いたメソッド
コストラクタが定義されたHeroを生み出す
public class Main { public static void main(String[] args) { Hero h = new Hero(); //インスタン生成と同時にHPに100が代入されている h.hp = 100; //いらない System.out.println(h.hp); //100と表示される } }コンストラクタで引数を追加情報として受け取る
public class Hero { String name; int hp; public Hero(String name) { //引数として文字列を1つ受け取る this.hp = 100; // hpフィールドを100で初期化 this.name = name; //引数の値でnameフィールドを初期化 } }newで引数を渡す
public class Main { public static void main(String[] args) { Hero h = new Hero("勇者"); //インスタン生成後、JVMによって自動的にコンストラクタが実行され、引数として"勇者"が利用される System.out.println(h.hp); //100と表示される System.out.println(h.name); //勇者と表示される } }コンストラクタのオーバーロード
public class Hero { String name; int hp; public Hero(String name) { //コンストラスタ① this.name = name; } public Hero() { //コンストラクタ② this.name = "勇者2" //"勇者2"を設定 } }コンストラスタをオーバーロードしたクラスの使用
public class Main { public static void main(String[] args) { Hero h = new Hero("勇者"); //文字列引数があるのでコンストラスタ①が呼び出される System.out.println(h.name); //勇者と表示される Hero h2 = new Hero(); //引数がないのでコンストラクタ②が呼び出される System.out.println(h2.name); //勇者2と表示される } }
- 投稿日:2020-05-20T17:30:12+09:00
JavaはExcelの隣接する行に異なる背景色を設定します
Excelテーブルを作成するとき、データテーブルの2つの隣接する行を異なる背景色で塗りつぶすことにより、各行のデータをより明確に見せ、行の読み違いを避け、Excelテーブルの美観を向上させることがで。この記事では、JavaプログラムでExcelの奇数行と偶数行に代替の背景色を設定する方法を紹介します。
使用ツール: Free Spire.XLS for Java(無料版)
JARファイルのインポート方法
方法1: Free Spire.XLS for Javaパッケージをダウンロードして解凍し、Spire.Xls.jarパッケージをlibフォルダーからJavaアプリケーションにインポートします。方法2: mavenを使用している場合は、pom.xmlファイルに次の依存関係を追加する必要があります。
<repositories> <repository> <id>com.e-iceblue</id> <name>e-iceblue</name> <url>http://repo.e-iceblue.com/nexus/content/groups/public/</url> </repository> </repositories> <dependencies> <dependency> <groupId>e-iceblue</groupId> <artifactId>spire.xls.free</artifactId> <version>2.2.0</version> </dependency> </dependencies>Javaコード例:
import com.spire.xls.*; import java.awt.*; public class ConditionalFormatting { public static void main(String[] args) { //Workbookオブジェクトを作成する Workbook workbook = new Workbook(); //Excelドキュメントを読み込む workbook.loadFromFile("test.xlsx"); //ワークシートを入手する Worksheet sheet = workbook.getWorksheets().get(0); //データ領域を取得する CellRange dataRange = sheet.getAllocatedRange(); //条件付き書式を使用して、偶数行の背景色を薄い灰色に設定します ConditionalFormatWrapper format1 = dataRange.getConditionalFormats().addCondition(); format1.setFirstFormula("=MOD(ROW(),2)=0"); format1.setFormatType(ConditionalFormatType.Formula); format1.setBackColor(Color.lightGray); //条件付き書式を使用して、奇数行の背景色を黄色に設定します ConditionalFormatWrapper format2 = dataRange.getConditionalFormats().addCondition(); format2.setFirstFormula("=MOD(ROW(),2)=1"); format2.setFormatType(ConditionalFormatType.Formula); format2.setBackColor(Color.yellow); //ドキュメントを保存します workbook.saveToFile("AlternateColor.xlsx", ExcelVersion.Version2016); } }
- 投稿日:2020-05-20T16:45:29+09:00
java(クラス型をフィールドに用いる)
has-aの関係
Swordクラスを定義
public class Sword { String name; int damage; }Heroクラスを定義
public class Hero { String name; int hp; Sword sword; //剣の情報 public void attack() { System.out.println(this.name + "攻撃した!"); System.out.println("敵に5ポイントのダメージを与えた!"); } }int型、String型ではなく、
Sword型
をフィールドにクラス型の変数を宣言することもできる。
has-aの関係
Hero has-a Sword(勇者は剣を持っている)
- 投稿日:2020-05-20T12:04:13+09:00
【JPA】ManyToMany関係の交差テーブルにキー以外の項目を持った場合の保存方法
はじめに
交差テーブルを利用したマッピングには毎回、頭を悩まされます。
今回は、交差テーブルに複合キー以外を持った場合の登録方法をご紹介します。複合キーのみの登録
まず、以下のような関係をもったエンティティがあるとします。
この場合の保存方法は以下のようになります。
ユーザマスタ@Getter @NoArgsConstructor @EqualsAndHashCode(of = {"id"}) @Entity @Table public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String status; @ManyToMany @JoinTable(name = "user_service",joinColumns = @JoinColumn(name="user_id"), referencedColumnName="id" inverseJoinColumns = @JoinColumn(name="service_id", referencedColumnName="id")) private List<Service> services = new ArrayList<>(); public User(String name,String status){ this.name = name; this.status = status; } // 利用するサービスを追加します public void addService(Service service){ services.add(service); } }サービスマスタ@Getter @NoArgsConstructor @EqualsAndHashCode(of = {"id"}) @Entity @Table public class Service implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String status; public Service(String name,String status) { this.name = name; this.status = status; } }登録処理User user = entityManager.find(User.class,<ユーザID>); Service service = entityManager.find(Service.class,<利用するサービスID>); user.addService(service); entityManager.persist(user);複合キー以外の項目がある場合の登録
利用サービステーブルに「利用開始日」を追加します。
この場合、
@JoinTable
のみでは利用開始日の保存ができないので、利用サービスとしてエンティティクラスを作成することになります。利用サービス@Getter @NoArgsConstructor @EqualsAndHashCode(of = {"id"}) @Entity @Table(name = "user_service") public class UserService implements Serializable { @EmbeddedId private PrimaryKey pk; @Column(name = "begin_date") private LocalDate beginDate; @ManyToOne @MapsId("userId") private User user; @ManyToOne @MapsId("serviceId") private Service service; public UserService(User user, Service service,LocalDate beginDate) { this.user = user; this.service = service; this.beginDate = beginDate; } @Getter @NoArgsConstructor @Embeddable public static class PrimaryKey implements Serializable { @Column(name = "user_id") private Long userId; @Column(name = "service_id") private Long serviceId; } }ここで重要なのは
@MapsId
です。引数にマッピングしたいPrimaryKeyクラスで定義したフィールド名を指定します。これによって、エンティティクラスと複合キーが関連づけられます。ユーザマスタも修正します。元々、ユーザマスタから利用しているサービスを取得できたのですが、
利用サービスクラス経由で取得するようにします。ユーザマスタ@Getter @NoArgsConstructor @EqualsAndHashCode(of = {"id"}) @Entity @Table public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String status; @OneToMany(mappedBy = "user") private List<UserService> services = new ArrayList<>(); public User(String name,String status){ this.name = name; this.status = status; } // 利用するサービスを追加します public void addService(Service service){ services.add(new UserService(this, service, LocalDate.now())); } // 交差テーブルのリストではなく、サービスのリストを返却してあげる public List<Service> getServices(){ return services.stream().map(UserService::getService).collect(Collectors.toList()); } }登録処理サンプルUser user = entityManager.find(User.class,<ユーザID>); Service service = entityManager.find(Service.class,<利用するサービスID>); user.addServices(service); entityManager.persist(user);
- 投稿日:2020-05-20T03:30:09+09:00
単体テストを書くためにstaticメソッドから脱却しよう ~DI編~
概要
前回の単体テストを書くためにstaticメソッドから脱却しよう~原則編~では、下記の2点を説明した。
- 実行環境により結果が変わるメソッドをstaticにしてはいけない
- 原則を破っている既存コードも、ステップを分けて影響範囲を小さくしながら修正できる
- たとえば、メソッドをラップすることで解決できる
今回の記事では、既存コードを修正するための他の方法として、他のクラスのstaticメソッドを呼び出している場合を例に、DI(Dependency Injection - 依存性の注入)という手法を紹介する。
なお、この記事では、Spring FrameworkなどのDIコンテナのフレームワークには言及しない。
問題
下記の
LegacyClient#execute()
のようなメソッドに対して、うまく単体テストを書くことができない。メソッド内の変数の
host
やport
の値を代入する処理では、GlobalControl
クラスのインスタンスメソッドにアクセスするのだが、データベースへのアクセスを必要としている。このため、アクセスが不可能な環境から実行した際には、例外が発生してしまう。LegacyClient.javapublic class LegacyClient { public void execute() { // ここにテスト対象のコードが入る // 問題のコード String host = GlobalControl.getInstance().get("host", "localhost"); int port = GlobalControl.getInstance().getInt("port", 8080); // ここにテスト対象のコードが入る } }GlobalControl.javapublic final class GlobalControl { private static final GlobalControl instance = new GlobalControl(); public static GlobalControl getInstance() { return instance; } public String get(String key, String defaultValue) { // データベースにアクセスするような処理 } public int getInt(String key, int defaultValue) { // データベースにアクセスするような処理 } }ラップメソッドには欠点がある
前回の記事では、メソッドをラップして解決する方法を紹介した。今回のケースも、その方法を適用することはできる。
しかし、この方法だと、複数のラップメソッドを作る必要があり、コードが冗長になる。
複数のラップメソッドを作る必要があるimport com.google.common.annotations.VisibleForTesting; public class LegacyClient { public void execute() { // ここにテスト対象のコードが入る // 問題のメソッドをラップした String host = get("host", "localhost"); int port = getInt("port", 8080); // ここにテスト対象のコードが入る } // テストクラスで上書きするラップメソッド1 @VisibleForTesting String get(String key, String defaultValue) { return GlobalControl.getInstance().get(key, defaultValue); } // テストクラスで上書きするラップメソッド2 @VisibleForTesting int getInt(String key, int defaultValue) { return GlobalControl.getInstance().getInt(key, defaultValue); } }DIによる解決方法
このため、今回はDIによる解決を試みる。
前回と同じように、commitを分けて、ステップを追って修正していこう。
ステップ0: 失敗する単体テストを用意する
失敗する単体テストpublic class LegacyClientTest { @Test public void test() { new LegacyClient().execute(); } }ステップ1: 問題のクラスのインタフェースを抽出する
単体テストが用意できたところで、実際の修正に入る。
(IDEのリファクタリング機能などを利用して、)問題の
GlobalControl
クラスで宣言されているインスタンスメソッドをインタフェースに抽出する。GlobalControlクラスから抽出したインタフェースpublic interface GlobalControlHolder { public String get(String key, String defaultValue); public int getInt(String key, int defaultValue); }抽出したインタフェースを付与public final class GlobalControl implements GlobalControlHolder { private static final GlobalControl instance = new GlobalControl(); public static GlobalControl getInstance() { return instance; } @Override public String get(String key, String defaultValue) { // データベースにアクセスするような処理 } @Override public int getInt(String key, int defaultValue) { // データベースにアクセスするような処理 } }ステップ2: staticメソッドの呼び出しをインスタンス作成時に変更する
GlobalControl
インスタンスをメソッド内で取得するのではなく、インスタンス作成時(コンストラクタ)に取得するようにする。また、このときのメンバ変数は先ほど抽出したインタフェースGlobalControlHolder
で宣言するようにする。そして、executeメソッドでは、メンバ変数
globalControl
を参照するように書き換える。LegacyClient.javapublic class LegacyClient { // インスタンスをローカルからメンバ変数に移し、保持する private final GlobalControlHolder globalControl; public LegacyClient() { this.globalControl = GlobalControl.getInstance(); } public void execute() { // ここにテスト対象のコードが入る // メンバ変数 "globalControl" を参照するように書き換え String host = globalControl.get("host", "localhost"); int port = globalControl.getInt("port", 8080); // ここにテスト対象のコードが入る } }ステップ3: DI用のコンストラクタを追加する
ステップ2で作成したコンストラクタは、コンストラクタ内で
GlobalControlHolder
インスタンスを取得していた。それに加えて、GlobalControlHolder
を引数に取るような、別のコンストラクタを追加する。LegacyClient.javapublic class LegacyClient { private final GlobalControlHolder globalControl; public LegacyClient() { this.globalControl = GlobalControl.getInstance(); } // DI用のコンストラクタ public LegacyClient(GlobalControlHolder globalControl) { this.globalControl = globalControl; } public void execute() { // ここにテスト対象のコードが入る String host = globalControl.get("host", "localhost"); int port = globalControl.getInt("port", 8080); // ここにテスト対象のコードが入る } }これにより、呼び出し元から
GlobalControl
インスタンスを渡せるようになった。コンストラクタを利用して、呼び出し元から依存モジュール(今回は
GlobalControl
があたる)を注入することをコンストラクタインジェクションと呼ぶ。ステップ4: テストコードで依存性を注入する
テストコードでは、
GlobalControlHolder
インタフェースを実装したMockクラスを作成し、それをnewしてLegacyClient
のコンストラクタに渡すようにする。テストコード用のMockクラスpublic class MockGlobalControl implements GlobalControlHolder { @Override public String get(String key, String defaultValue) { // デフォルト値を返す return defaultValue; } @Override public int getInt(String key, int defaultValue) { // デフォルト値を返す return defaultValue; } }修正したテストコードpublic class LegacyClientTest { @Test public void test() { // テストコード用のMockクラスを注入する new LegacyClient(new MockGlobalControl()).execute(); } }これにより、単体テストでは、データベースにアクセスしない実装で依存モジュールを差し替えることができる。
まとめ
別クラスのstaticメソッドを呼び出している場合の解決は、DI(Dependency Injection - 依存性の注入)が有効である。
また、前回同様、ステップを分けて修正することで、リファクタリングによる影響範囲を限定しつつ単体テストを書くことができる。
- 投稿日:2020-05-20T03:30:09+09:00
単体テストを書くためにstaticメソッドから脱却しよう ~②DI編~
概要
前回の単体テストを書くためにstaticメソッドから脱却しよう ~①原則・ラップメソッド編~では、下記の2点を説明した。
- 実行環境により結果が変わるメソッドをstaticにしてはいけない
- 原則を破っている既存コードも、ステップを分けて影響範囲を小さくしながら修正できる
- たとえば、メソッドをラップすることで解決できる
今回の記事では、既存コードを修正するための他の方法として、他のクラスのstaticメソッドを呼び出している場合を例に、DI(Dependency Injection - 依存性の注入)という手法を紹介する。
なお、この記事では、Spring FrameworkなどのDIコンテナのフレームワークには言及しない。
問題
下記の
LegacyClient#execute()
のようなメソッドに対して、うまく単体テストを書くことができない。メソッド内の変数の
host
やport
の値を代入する処理では、GlobalControl
クラスのインスタンスメソッドにアクセスするのだが、データベースへのアクセスを必要としている。このため、アクセスが不可能な環境から実行した際には、例外が発生してしまう。LegacyClient.javapublic class LegacyClient { public void execute() { // ここにテスト対象のコードが入る // 問題のコード String host = GlobalControl.getInstance().get("host", "localhost"); int port = GlobalControl.getInstance().getInt("port", 8080); // ここにテスト対象のコードが入る } }GlobalControl.javapublic final class GlobalControl { private static final GlobalControl instance = new GlobalControl(); public static GlobalControl getInstance() { return instance; } public String get(String key, String defaultValue) { // データベースにアクセスするような処理 } public int getInt(String key, int defaultValue) { // データベースにアクセスするような処理 } }ラップメソッドには欠点がある
前回の記事では、メソッドをラップして解決する方法を紹介した。今回のケースも、その方法を適用することはできる。
しかし、この方法だと、複数のラップメソッドを作る必要があり、コードが冗長になる。
複数のラップメソッドを作る必要があるimport com.google.common.annotations.VisibleForTesting; public class LegacyClient { public void execute() { // ここにテスト対象のコードが入る // 問題のメソッドをラップした String host = get("host", "localhost"); int port = getInt("port", 8080); // ここにテスト対象のコードが入る } // テストクラスで上書きするラップメソッド1 @VisibleForTesting String get(String key, String defaultValue) { return GlobalControl.getInstance().get(key, defaultValue); } // テストクラスで上書きするラップメソッド2 @VisibleForTesting int getInt(String key, int defaultValue) { return GlobalControl.getInstance().getInt(key, defaultValue); } }DIによる解決方法
このため、今回はDIによる解決を試みる。
前回と同じように、commitを分けて、ステップを追って修正していこう。
ステップ0: 失敗する単体テストを用意する
失敗する単体テストpublic class LegacyClientTest { @Test public void test() { new LegacyClient().execute(); } }ステップ1: 問題のクラスのインタフェースを抽出する
単体テストが用意できたところで、実際の修正に入る。
(IDEのリファクタリング機能などを利用して、)問題の
GlobalControl
クラスで宣言されているインスタンスメソッドをインタフェースに抽出する。GlobalControlクラスから抽出したインタフェースpublic interface GlobalControlHolder { public String get(String key, String defaultValue); public int getInt(String key, int defaultValue); }抽出したインタフェースを付与public final class GlobalControl implements GlobalControlHolder { private static final GlobalControl instance = new GlobalControl(); public static GlobalControl getInstance() { return instance; } @Override public String get(String key, String defaultValue) { // データベースにアクセスするような処理 } @Override public int getInt(String key, int defaultValue) { // データベースにアクセスするような処理 } }ステップ2: staticメソッドの呼び出しをインスタンス作成時に変更する
GlobalControl
インスタンスをメソッド内で取得するのではなく、インスタンス作成時(コンストラクタ)に取得するようにする。また、このときのメンバ変数は先ほど抽出したインタフェースGlobalControlHolder
で宣言するようにする。そして、executeメソッドでは、メンバ変数
globalControl
を参照するように書き換える。LegacyClient.javapublic class LegacyClient { // インスタンスをローカルからメンバ変数に移し、保持する private final GlobalControlHolder globalControl; public LegacyClient() { this.globalControl = GlobalControl.getInstance(); } public void execute() { // ここにテスト対象のコードが入る // メンバ変数 "globalControl" を参照するように書き換え String host = globalControl.get("host", "localhost"); int port = globalControl.getInt("port", 8080); // ここにテスト対象のコードが入る } }ステップ3: DI用のコンストラクタを追加する
ステップ2で作成したコンストラクタは、コンストラクタ内で
GlobalControlHolder
インスタンスを取得していた。それに加えて、GlobalControlHolder
を引数に取るような、別のコンストラクタを追加する。LegacyClient.javapublic class LegacyClient { private final GlobalControlHolder globalControl; public LegacyClient() { this.globalControl = GlobalControl.getInstance(); } // DI用のコンストラクタ public LegacyClient(GlobalControlHolder globalControl) { this.globalControl = globalControl; } public void execute() { // ここにテスト対象のコードが入る String host = globalControl.get("host", "localhost"); int port = globalControl.getInt("port", 8080); // ここにテスト対象のコードが入る } }これにより、呼び出し元から
GlobalControl
インスタンスを渡せるようになった。コンストラクタを利用して、呼び出し元から依存モジュール(今回は
GlobalControl
があたる)を注入することをコンストラクタインジェクションと呼ぶ。ステップ4: テストコードで依存性を注入する
テストコードでは、
GlobalControlHolder
インタフェースを実装したMockクラスを作成し、それをnewしてLegacyClient
のコンストラクタに渡すようにする。テストコード用のMockクラスpublic class MockGlobalControl implements GlobalControlHolder { @Override public String get(String key, String defaultValue) { // デフォルト値を返す return defaultValue; } @Override public int getInt(String key, int defaultValue) { // デフォルト値を返す return defaultValue; } }修正したテストコードpublic class LegacyClientTest { @Test public void test() { // テストコード用のMockクラスを注入する new LegacyClient(new MockGlobalControl()).execute(); } }これにより、単体テストでは、データベースにアクセスしない実装で依存モジュールを差し替えることができる。
まとめ
別クラスのstaticメソッドを呼び出している場合の解決は、DI(Dependency Injection - 依存性の注入)が有効である。
また、前回同様、ステップを分けて修正することで、リファクタリングによる影響範囲を限定しつつ単体テストを書くことができる。
- 投稿日:2020-05-20T01:31:45+09:00
サーブレットでiPhoneアプリからのリクエストパラメータをうまく取得できなかった話
個人でiPhoneアプリを作成していたところ、getAttribute()とgetParameter()の違いを理解しておらず、
サーブレットでアプリからのリクエストをうまく取得できなかったのでメモとして残しておきます。今回発生した事象はこんな感じ。
iPhoneアプリからリクエスト送信
↓
サーブレットで受信(サーブレットに到達はするが、リクエスト内のパラメータが取得できない。。)原因
request.getAttributeで値を取得しようとしていたのがダメでした。
getParameterで取得しないとダメみたいですね。。
(jspはgetAttributeで取得できていたので完全に盲点でした)詳しい内容はこちらのサイトに記載してありました。
・getAttribute()とgetParameter()の違い検証
理解を深めるためにサンプルプログラムで検証してみました。
iPhoneのtextFieldに入力された文字列をコンソールに表示するサンプルプログラムです。・iPhone画面
結果
・コンソール画面
ソース
・iPhone画面 (入力画面)
ViewControllerimport UIKit class ViewController: UIViewController { @IBOutlet weak var testField: UITextField! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } //Servletに送信ボタンを押した際に実行 @IBAction func goServlet(_ sender: Any) { self.performSegue(withIdentifier: "goResultView", sender: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { print("prepare動作開始") //URLを設定 guard let req_url = URL(string: "http://localhost:8080/Test/TestServlet") else{return} print("urlセット完了") //リクエストに必要な情報を宣言 var req = URLRequest(url: req_url) print("リクエストの宣言") //POSTを指定 req.httpMethod = "POST" //POSTするデータをBODYとして設定 req.httpBody = "test=\(self.testField.text!)".data(using: .utf8) //sessionの作成 let session = URLSession(configuration: .default,delegate: nil, delegateQueue: OperationQueue.main) print("sessionの作成") //リクエストをタスクとして登録 let task = session.dataTask(with: req, completionHandler: { (data, response ,error) in }) //request送信 task.resume() } }・iPhone画面 (結果画面)
※iPhoneの結果画面にはswiftコードを定義してません・Java Servlet
TestServletpackage servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class testServlet */ @WebServlet("/TestServlet") public class TestServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public TestServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub request.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=UTF-8"); System.out.println("サーブレットのPOSTメソッドに到達"); System.out.println("getAttributeで受け取った場合:" + "iPhoneアプリから送られた文字列は" + request.getAttribute("test") + "です。"); System.out.println("getParameterで受け取った場合:" + "iPhoneアプリから送られた文字列は" + request.getParameter("test") + "です。"); } }