JSP & Servletで作る英和辞書アプリ

JSP&Servlet

6回に分けて連載してきた英語辞書アプリもいよいよ最終回だ。すでに動いているこのアプリのリファクタリング(機能はそのままでコードを洗練させること)を行う。このページに最初に訪れた人はまずは以下のリンクから手順を追って作成してもらいたい。

フォルダ構成図

今回は変更箇所が多くファイルも増える。迷子になりそうなときはこの構成図を参照してもらいたい。

リファクタリング方針

○未使用部分の削除
コードの追記を重ねてきたので完全に不要となっている部分があるのでこれを削除する。
○重複コードの削除
DRYの原則(Don’t Repeat Yourself)->同じことを二度書かない。というプログラマーにとって最重要な部分が守られていない部分があるのでこれを修正
○処理の分割
サーブレットの処理が肥大化してしまっているのであらたにモデルを作成し、MVCときっちり役割分担をしていく。

リファクタリングの実施

では実際にリファクタリングをしていこう。

未使用部分の削除

DAOにあるオーバーロードされている2つのメソッドが不要となっているので以下ハイライトしてある部分を削除

package dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import model.Word;

public class WordDAO {
	private Connection db;
	private PreparedStatement ps;
	private ResultSet rs;

	private void connect() throws NamingException, SQLException {
			Context context=new InitialContext();
			DataSource ds=(DataSource)context.lookup("java:comp/env/ejword");
			this.db=ds.getConnection();
	}
	private void disconnect() throws SQLException {
		if(rs !=null) {
			rs.close();
		}
		if(ps !=null) {
			ps.close();
		}
		if(db != null) {
			db.close();
		}
	}
	public List<Word> getListBySearchWord(String searchWord,String mode){
		List<Word> list=new ArrayList<>();
		switch(mode) {
		case "startsWith":
			searchWord=searchWord+"%";
			break;
		case "contains":
			searchWord="%"+searchWord+"%";
			break;
		case "endsWith":
			searchWord="%"+searchWord;
		}
		try {
			this.connect();
			ps=db.prepareStatement("SELECT * FROM words WHERE title LIKE ?");
			ps.setString(1, searchWord);
			//System.out.println(ps);
			rs=ps.executeQuery();
			while(rs.next()) {
				String title=rs.getString("title");
				String body=rs.getString("body");
				Word w=new Word(title,body);
				list.add(w);
			}

		} catch (NamingException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}finally {
			try {
				this.disconnect();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		return list;
	}
	public List<Word> getListBySearchWord(String searchWord,String mode,int limit){
		List<Word> list=new ArrayList<>();
		switch(mode) {
		case "startsWith":
			searchWord=searchWord+"%";
			break;
		case "contains":
			searchWord="%"+searchWord+"%";
			break;
		case "endsWith":
			searchWord="%"+searchWord;
		}
		try {
			this.connect();
			ps = db.prepareStatement("SELECT * FROM words WHERE title LIKE ? LIMIT ?");
			ps.setString(1, searchWord);
			ps.setInt(2, limit);
			rs = ps.executeQuery();
			while (rs.next()) {
				int id = rs.getInt("id");
				String title = rs.getString("title");
				String body = rs.getString("body");
				list.add(new Word(id, title, body));
			}
		} catch (NamingException | SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				this.disconnect();
			} catch (SQLException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			}
		}
		return list;
	}
	public List<Word> getListBySearchWord(String searchWord,String mode,int limit,int offset){
		List<Word> list=new ArrayList<>();
		switch(mode) {
		case "startsWith":
			searchWord=searchWord+"%";
			break;
		case "contains":
			searchWord="%"+searchWord+"%";
			break;
		case "endsWith":
			searchWord="%"+searchWord;
		}
		try {
			this.connect();
			ps = db.prepareStatement("SELECT * FROM words WHERE title LIKE ? LIMIT ? OFFSET ?");
			ps.setString(1, searchWord);
			ps.setInt(2, limit);
			ps.setInt(3, offset);
			rs = ps.executeQuery();
			while (rs.next()) {
				int id = rs.getInt("id");
				String title = rs.getString("title");
				String body = rs.getString("body");
				list.add(new Word(id, title, body));
			}
		} catch (NamingException | SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				this.disconnect();
			} catch (SQLException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			}
		}
		return list;
	}

	//一致件数を求めるメソッド
	public int getCount(String searchWord,String mode){
		switch(mode) {
		case "startsWith":
			searchWord=searchWord+"%";
			break;
		case "contains":
			searchWord="%"+searchWord+"%";
			break;
		case "endsWith":
			searchWord="%"+searchWord;
		}
		int total=0;
		try {
			this.connect();
			ps = db.prepareStatement("SELECT count(*) AS total FROM words WHERE title LIKE ?");
			ps.setString(1, searchWord);
			rs = ps.executeQuery();
			if (rs.next()) {
				total = rs.getInt("total");
			}
		} catch (NamingException | SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				this.disconnect();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		return total;
	}

}

重複コードの削除

WordDAOの中に同じ同じswitch分の記述箇所がある。これをメソッドに括りだそう。
以下のように修正する。

package dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import model.Word;

public class WordDAO {
	private Connection db;
	private PreparedStatement ps;
	private ResultSet rs;

	private void connect() throws NamingException, SQLException {
		Context context = new InitialContext();
		DataSource ds = (DataSource) context.lookup("java:comp/env/ejword");
		this.db = ds.getConnection();
	}

	private void disconnect() throws SQLException {
		if (rs != null) {
			rs.close();
		}
		if (ps != null) {
			ps.close();
		}
		if (db != null) {
			db.close();
		}
	}

	public List<Word> getListBySearchWord(String searchWord,String mode,int limit,int offset){
		List<Word> list=new ArrayList<>();
		searchWord=modifySearchWord(searchWord,mode);
		try {
			this.connect();
			ps = db.prepareStatement("SELECT * FROM words WHERE title LIKE ? LIMIT ? OFFSET ?");
			ps.setString(1, searchWord);
			ps.setInt(2, limit);
			ps.setInt(3, offset);
			rs = ps.executeQuery();
			while (rs.next()) {
				int id = rs.getInt("id");
				String title = rs.getString("title");
				String body = rs.getString("body");
				list.add(new Word(id, title, body));
			}
		} catch (NamingException | SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				this.disconnect();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		return list;
	}
	//一致件数を求めるメソッド
		public int getCount(String searchWord,String mode){
			searchWord=modifySearchWord(searchWord,mode);
			int total=0;
			try {
				this.connect();
				ps = db.prepareStatement("SELECT count(*) AS total FROM words WHERE title LIKE ?");
				ps.setString(1, searchWord);
				rs = ps.executeQuery();
				if (rs.next()) {
					total = rs.getInt("total");
				}
			} catch (NamingException | SQLException e) {
				e.printStackTrace();
			} finally {
				try {
					this.disconnect();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			return total;
		}
		private String modifySearchWord(String searchWord,String mode) {
			switch(mode) {
			case "startsWith":
				searchWord=searchWord+"%";
				break;
			case "contains":
				searchWord="%"+searchWord+"%";
				break;
			case "endsWith":
				searchWord="%"+searchWord;
			}
			return searchWord;
		}
}

新たにmodifySearchWordというメソッド作りswitch文の部分を作成した。これを2つのメソッドから呼び出すように修正した。

処理の分割

サーブレットが煩雑になっているので新たにモデルを作成し、処理の見通しをよくしていこう。modelパッケージの中に以下の2つのクラスを新たに作成する。

○model.EJWord.java

package model;

import java.io.Serializable;
import java.util.List;

public class EJWord implements Serializable{
	private String searchWord="";
	private String mode="";
	private int pageNo;
	private int total;
	private int limit;
	private List<Word> list=null;
	private String[][] pager=null;
	public EJWord() {}
	public EJWord(String searchWord, String mode, int pageNo,int limit) {
		this.searchWord = searchWord;
		this.mode = mode;
		this.pageNo = pageNo;
		this.limit=limit;
	}
	public String getSearchWord() {
		return searchWord;
	}
	public void setSearchWord(String searchWord) {
		this.searchWord = searchWord;
	}
	public String getMode() {
		return mode;
	}
	public void setMode(String mode) {
		this.mode = mode;
	}
	public int getPageNo() {
		return pageNo;
	}
	public void setPageNo(int pageNo) {
		this.pageNo = pageNo;
	}
	public int getTotal() {
		return total;
	}
	public void setTotal(int total) {
		this.total = total;
	}
	public int getLimit() {
		return limit;
	}
	public void setLimit(int limit) {
		this.limit = limit;
	}
	public String[][] getPager() {
		return pager;
	}
	public void setPager(String[][] pager) {
		this.pager = pager;
	}
	public List<Word> getList() {
		return list;
	}
	public void setList(List<Word> list) {
		this.list = list;
	}

}

現状リクエストスコープに格納する項目が多くなっているので一つのクラスにまとめた。

○model.EJWordLogic.java

package model;

import java.util.List;

import dao.WordDAO;

public class EJWordLogic {
	public void execute(EJWord ejw) {
		WordDAO dao=new WordDAO();
		int total=dao.getCount(ejw.getSearchWord(), ejw.getMode());
		ejw.setTotal(total);
		List<Word> list=dao.getListBySearchWord(
				ejw.getSearchWord(),
				ejw.getMode(),
				ejw.getLimit(),
				(ejw.getPageNo()-1)*ejw.getLimit()
				);
		ejw.setList(list);
		if(total > ejw.getLimit()) {
			int pageCount=total%ejw.getLimit() == 0? total/ejw.getLimit(): total/ejw.getLimit()+1;
			String[][] pager;
			if(pageCount < 20) {
				pager=new String[pageCount][];
				for(int i=0;i<pageCount;i++) {
					pager[i]=new String[] {ejw.getPageNo()==i+1?"active":"",i+1+"",i+1+""};
				}
			}else {
				//このページの前に何ページ分リンクを設定するか(最大5件)
				int before=Math.min(ejw.getPageNo()-1, 5);
				int after=Math.min(pageCount-ejw.getPageNo(),5);
				int len=1+before+1+after+1;
				pager=new String[len][];
				//先頭ページへのリンク{クラス名,リンク先ページ,表示文言}
				pager[0]=new String[] {ejw.getPageNo()==1? "disabled":"",1+"","«"};
				for(int i=1,page=ejw.getPageNo()-before;i<len-1;i++,page++) {
					pager[i]=new String[] {page==ejw.getPageNo()?"active":"",page+"",page+""};
				}
				//末尾ページへのリンク
				pager[len-1]=new String[] {ejw.getPageNo()==pageCount? "disabled":"",pageCount+"","»"};
			}
			ejw.setPager(pager);
		}
	}
}

サーブレットに記述してある多くのロジックをこのクラスにまとめた。ページャー部分に配列を利用することでメンテナンス性の悪い文字列連結からも開放された。

controllerの変更

先程作ったクラスを利用するようにサーブレットのMain.javaを以下のように修正する。

○controller.Main.java

package controller;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import model.EJWord;
import model.EJWordLogic;

@WebServlet("/main")
public class Main extends HttpServlet {
	private static final int LIMIT=20;
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		request.setCharacterEncoding("utf-8");
		String searchWord=request.getParameter("searchWord");
		EJWord ejw;
		if(searchWord != null) {
			String mode=request.getParameter("mode");
			if(mode == null) {
				mode="startsWith";
			}
			String page=request.getParameter("page");
			int pageNo=page==null? 1:Integer.parseInt(page);
			ejw=new EJWord(searchWord,mode,pageNo,LIMIT);
			EJWordLogic logic=new EJWordLogic();
			logic.execute(ejw);

		}else {
			ejw=new EJWord();
		}
		request.setAttribute("ejw", ejw);

		RequestDispatcher rd=request.getRequestDispatcher("/WEB-INF/view/main.jsp");
		rd.forward(request, response);
	}

}

リクエストパラメータの取得、インスタンスの生成、スコープへの格納、viewへのフォワード以外の記述が消え見通しがよくなったことがわかる。

viewの修正

コントローラーの修正にあわせてviewも変更しよう。/WEB-INF/view/main.jspを以下のように修正する。

○main.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" import="model.*,java.util.*"%>
<%
EJWord ejw=(EJWord)request.getAttribute("ejw");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>EJWord</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="container">
<form action="/ejword/main" method="get" class="form-inline">
<input type="text" name="searchWord" value="<%=ejw.getSearchWord()%>" class="form-control" placeholder="検索後を入力" required>
<select name="mode" class="form-control">
<option value="startsWith" <%=ejw.getMode().equals("startsWith")? " selected":"" %>>で始まる</option>
<option value="contains" <%=ejw.getMode().equals("contains")? " selected":"" %>>含む</option>
<option value="endsWith" <%=ejw.getMode().equals("endsWith")? " selected":"" %>>で終わる</option>
<option value="match" <%=ejw.getMode().equals("match")? " selected":"" %>>一致する</option>
</select>
<button type="submit" class="btn btn-primary">検索</button>
</form>
<%if(ejw.getList() != null && ejw.getList().size()==0){ %>
<p>1件も一致しませんでした</p>
<%} %>
<% if( ejw.getList() !=null && ejw.getList().size() > 0){ %>
	<% if(ejw.getTotal() <=ejw.getLimit()){ %>
		<p>全<%=ejw.getTotal() %>件</p>
	<%}else{ %>
		<p>全<%=ejw.getTotal() %>件中
		<%=(ejw.getPageNo()-1)*ejw.getLimit()+1 %>~
		<%=ejw.getPageNo()*ejw.getLimit() > ejw.getTotal()? ejw.getTotal():ejw.getPageNo()*ejw.getLimit()  %>
		件を表示</p>
		<ul class="pager">
		<%if(ejw.getPageNo()>1){ %>
			<li><a href="/ejword/main?searchWord=<%=ejw.getSearchWord() %>&mode=<%=ejw.getMode() %>&page=<%=ejw.getPageNo()-1 %>">←前へ</a></li>
		<%} %>
		<%if(ejw.getPageNo()*ejw.getLimit() < ejw.getTotal()){ %>
			<li><a href="/ejword/main?searchWord=<%=ejw.getSearchWord() %>&mode=<%=ejw.getMode() %>&page=<%=ejw.getPageNo()+1 %>">次へ→</a></li>
		<%} %>
		</ul>
	<%} %>
	<table class="table table-borderd table-striped">
	<% for(Word w:ejw.getList()){ %>
		<tr><th><%=w.getTitle() %></th><td><%=w.getBody() %></td></tr>
	<%} %>
	</table>
<%} %>
<% if (ejw.getPager() !=null){ %>
<div class='paginationBox'>
	<ul class='pagination'>
	<% for(String[] row:ejw.getPager()){ %>
		<li class="<%=row[0]%>">
		<a href="/ejword/main?searchWord=<%=ejw.getSearchWord() %>&mode=<%=ejw.getMode() %>&page=<%=row[1] %>"><%=row[2] %></a>
		</li>
	<%} %>
	</ul>
</div>
<%} %>
</div>
<footer>
© 2021 Joytas.net
</footer>
</body>
</html>

cssファイルも外部から読み込むように変更した。WebContentの中にcssフォルダを作成し、以下のようにmain.cssを作成しよう。

○main.css

.container{
min-height:calc(100vh - 70px);
}
form{
margin:20px auto;
}
input,select{
margin-right:5px;
}
.pager{
text-align:left;
}
.paginationBox{
text-align:center;
}
footer{
height:40px;
color:white;
background:#347ab7;
text-align:center;
line-height:40px;
margin-top:30px;
}

完成

以上で完成だ。JSP/ServletといったサーバーサイドJavaにHTML,cssとデータベースが加わって実用的なアプリになっている。オリジナルなデザインや機能を追加してポートフォリオの一つとして加えてほしい。

コメント

タイトルとURLをコピーしました