2012 11 28

フレームワークを作るの7

サービス層の改善の続き。

インターフェースに対して処理をするようにコードを書くのに、プロキシのファクトリーでサービスの実装クラスを指定してるってのが、ちょっとかっこ悪い。 なのでこれをどうにかしたいのだが、どうにかと言ったところで、とり得る手段はそう無いんだよね。

結論から言うと、実装クラス名をjavaのコードの外、プロパティファイルかXMLか、何らかの設定ファイルに持たせる。

とは言っても、現状のプロキシファクトリーの引数に実装クラスを指定しているのは、こいつが特定の実装クラスに依存しない書き方をしているからであって、実はもうこの時点で目的はほぼ達成できているのだな。 後は、現状はサービスの実装クラスを指定しているこいつの引数を、idを指定してインスタンスを得るオブジェクトに置き換えればいい。 そのオブジェクトの内部で、設定ファイルを読む。

で、既にSQLの定義でXMLを使っていることだし、サービスの実装クラスもXMLから取得することにした。

まずは設定のXMLファイルから。 serviceDef.xml に、とりあえず最低限の定義をする。

<?xml version="1.0" encoding="UTF-8"?> <services> <service id="booksService" class="service.impl.BooksServiceImpl" /> </services>

本当に最低限。

この設定ファイルを読んで指定の id の定義情報を返すための ServiceDefLoader を、SQLの定義ファイルを読むのと同様に作る。

package framework.service; import java.io.IOException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import framework.exception.SystemException; public class ServiceDefLoader { private static final String SERVICE_XPATH_FMT = "//service[@id='%s']"; private static XPath xpath = null; private static Document doc = null; public ServiceDefLoader(final String serviceDefFileName) { xpath = XPathFactory.newInstance().newXPath(); DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); domFactory.setNamespaceAware(true); try { doc = domFactory.newDocumentBuilder().parse(serviceDefFileName); } catch (SAXException | ParserConfigurationException | IOException e) { throw new SystemException(e); } } public String getServiceClass(final String id) { try { XPathExpression expr = xpath.compile(String.format(SERVICE_XPATH_FMT, id)); NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); return nodes.item(0).getAttributes().getNamedItem("class").getNodeValue(); } catch (XPathExpressionException e) { throw new SystemException(e); } } }

今回のサンプルは単純だが、仮にこれがもっと複雑であっても、サービスクラスの定義ファイルなんてそう肥大化するものでもないだろうし、システムで一つあれば十分だろう。 ローダーをプロキシファクトリーの中に隠して、外からはidを指定するようにした方がすっきりする気がする。

という方向で ServiceFactory を変更。

package framework.service; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.sql.Connection; import framework.exception.SystemException; public class ServiceFactory { private static ServiceDefLoader loader = null; public static <T> T getService(Connection conn, final String serviceId) { if (loader == null) { loader = new ServiceDefLoader("serviceDef.xml"); } try { Class<?> serviceClass = Class.forName(loader.getServiceClass(serviceId)); Object serviceImpl = serviceClass.getConstructor(Connection.class).newInstance(conn); return (T) Proxy.newProxyInstance( serviceClass.getClassLoader(), serviceClass.getInterfaces(), new TransactionInvocationHandler(conn, serviceImpl)); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException(e); } } }

これに伴い、アプリケーション層もちょっと変更。

package apl; import java.sql.Connection; import service.BooksService; import util.DBUtil; import dto.BookDto; import framework.service.ServiceFactory; public class Main { public static void main(String[] args) { try (Connection conn = DBUtil.getConnection()) { BooksService service = ServiceFactory.getService(conn, "booksService"); BookDto book = service.selectBook("4-635-09016-7"); System.out.println(book.toString()); book.setPrice("3400"); service.updateBook(book); book = service.selectBook("4-635-09016-7"); System.out.println(book.toString()); } catch (Exception e) { e.printStackTrace(); } } }

実装クラスがidになっただけだが、こうして実装クラスと切り離したことで、テスト用のクラスに置き換えるとかが簡単にできるようになって便利になる。 きっと。 たぶん。

あ、これでアプリケーション層のテストが楽になるなら、同じ様にデータアクセス層のクラスを入れ替え可能とすることでサービス層のテストも楽になるはずなんだよな。 具体的には、データアクセス層の実装クラスとして特定の値を返すものを用意し、これでサービス層のクラスをテストするとか。 当然、逆向きのテストも可能。 サンプルコードのレベルだと過剰な実装という感じがしないでも無いが、せっかくなので対応してみよう。

まずはそうした入れ替えを可能にするために、データアクセス層のクラスをインターフェースと実装クラスに分離する。

本のテーブルへのアクセスを規定するインターフェース BooksDao

package dao; import java.sql.SQLException; import java.util.List; import dto.BookDto; public interface BooksDao { public Integer insert(final BookDto book) throws SQLException; public BookDto selectByISBN(final String isbn) throws SQLException; public List<BookDto> selectAll() throws SQLException; public Integer updateByISBN(final BookDto book) throws SQLException; public Integer deleteByISBN(final BookDto book) throws SQLException; }

そしてその実装 BooksDaoImpl

package dao.impl; import java.sql.Connection; import java.sql.SQLException; import java.util.List; import dao.BooksDao; import dto.BookDto; import framework.dao.CmnDao; import framework.dao.SqlDefLoader; public class BooksDaoImpl implements BooksDao { private final CmnDao dao; public BooksDaoImpl(Connection conn) { this.dao = new CmnDao(conn, new SqlDefLoader("sqlDef.xml")); } @Override public Integer insert(final BookDto book) throws SQLException { return dao.executeUpdate("insert", book); } @Override public BookDto selectByISBN(final String isbn) throws SQLException { BookDto criteria = new BookDto(); criteria.setIsbn(isbn); List<BookDto> list = dao.selectWhereAs("selectByIsbn", criteria, BookDto.class); return list.size() == 1 ? list.get(0) : new BookDto(); } @Override public List<BookDto> selectAll() throws SQLException { return dao.selectAs("selectAll", BookDto.class); } @Override public Integer updateByISBN(final BookDto book) throws SQLException { return dao.executeUpdate("updateByIsbn", book); } @Override public Integer deleteByISBN(final BookDto book) throws SQLException { return dao.executeUpdate("deleteByIsbn", book); } }

さて、データアクセス層の実装クラスをどう取得するか。

サービスクラスの生成同様にデータアクセス層用に定義ファイルを作成し、そこからidを指定してインスタンスを取得する形にしてもいいのだが、これだと設定ファイルが増えるばかりで、逆にその管理に手を取られそうな気がする。 それに、何が何を使うという関係は、プログラムのコードを介してよりも直接その関係のみが見えた方が人に優しいだろう。

ということで、データアクセス層のクラスとサービス層のクラスの依存関係を、クラス定義の serviceDef.xml の中で表現してみる。

<?xml version="1.0" encoding="UTF-8"?> <defs> <services> <service id="booksService" class="service.impl.BooksServiceImpl"> <dao id="booksDao" /> </service> </services> <daos> <dao id="booksDao" class="dao.impl.BooksDaoImpl" /> </daos> </defs>

service は、その子要素として定義されている dao を使う。 dao の実装クラスは、 daos の定義から取得する。 親子関係と実装クラスの定義を分離併存させたために、要素の階層が一つ増えてしまった。

クラスの情報を取得する時は、親子の情報をセットにする。 これを格納するために ServiceDef を定義する。

package framework.service; import java.util.Map; public class ServiceDef { private final String serviceClassName; private final Map<String, String> daoDef; public ServiceDef(final String serviceClassName, final Map<String, String> daoDef) { this.serviceClassName = serviceClassName; this.daoDef = daoDef; } public String getServiceClassName() { return serviceClassName; } public Map<String, String> getDaoDef() { return daoDef; } }

Mapは、XMLのserviceの子要素を格納するためのもので、キーにDAOのidを、対応する値にDAOのクラス名を持つ。 SqlDef と同じく、一度保持した値を変更することは無いだろうからgetterだけ。 データの設定はコンストラクターとする。

idを指定して ServiceDef の形で返すように ServiceDefLoader も変更する。

package framework.service; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import framework.exception.SystemException; public class ServiceDefLoader { private static final String SERVICE_DEF_XPATH_FMT = "//services/service[@id='%s']"; private static final String DAO_DEF_XPATH_FMT = "//daos/dao[@id='%s']"; private static XPath xpath = null; private static Document doc = null; public ServiceDefLoader(final String serviceDefFileName) { xpath = XPathFactory.newInstance().newXPath(); DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); domFactory.setNamespaceAware(true); try { doc = domFactory.newDocumentBuilder().parse(serviceDefFileName); } catch (SAXException | ParserConfigurationException | IOException e) { throw new SystemException(e); } } public ServiceDef getServiceDef(final String id) throws SystemException { try { Node serviceNode = getNode(SERVICE_DEF_XPATH_FMT, id); String className = getAttributeValue(serviceNode, "class"); Map<String, String> daoDef = getDaoDef(serviceNode.getChildNodes()); return new ServiceDef(className, daoDef); } catch (XPathExpressionException e) { throw new SystemException(e); } } private static Map<String, String> getDaoDef(final NodeList nodes) throws XPathExpressionException { Map<String, String> daoDef = new HashMap<>(); for (int i = 0; i < nodes.getLength(); i++) { if (nodes.item(i).getNodeName().equalsIgnoreCase("dao")) { String id = getAttributeValue(nodes.item(i), "id"); String className = getAttributeValue(getNode(DAO_DEF_XPATH_FMT, id), "class"); daoDef.put(id, className); } } return daoDef; } private static Node getNode(final String fmt, final String id) throws XPathExpressionException { return ((NodeList) xpath.compile(String.format(fmt, id)).evaluate(doc, XPathConstants.NODESET)).item(0); } private static String getAttributeValue(final Node node, final String attributeName) { return node.getAttributes().getNamedItem(attributeName).getNodeValue(); } }

ここまでで、サービス層の実装クラスとデータアクセス層の実装クラスがセットで取得できるようになった。 これを、今度はサービス層の実装クラスの中に、データアクセス層の実装クラスを入れ込むために使う。 いわゆるDIだな。

そんな規則を導入して、サービスクラスのインスタンス生成時に、一緒にDAOクラスのインスタンスも生成して設定してしまうのが手っ取り早いだろう。

ということで、サービス層の実装クラス BooksServiceImpl に、DAOのためのsetterを追加する。 定義ファイルには、DAOは一つだけでidが booksDao だから setBooksDao を追加することになる。

package service.impl; import java.sql.SQLException; import java.util.List; import service.BooksService; import dao.BooksDao; import dto.BookDto; import framework.exception.BusinessException; public class BooksServiceImpl implements BooksService { private BooksDao booksDao; public void setBooksDao(BooksDao booksDao) { this.booksDao = booksDao; } @Override public void insertBook(final BookDto book) throws SQLException { booksDao.insert(book); } @Override public BookDto selectBook(final String isbn) throws SQLException { return booksDao.selectByISBN(isbn); } @Override public List<BookDto> selectAllBooks() throws SQLException { return booksDao.selectAll(); } @Override public void updateBook(final BookDto book) throws SQLException, BusinessException { if (!booksDao.updateByISBN(book).equals(1)) { throw new BusinessException(book.getIsbn() + " : deleted or updated by someone else."); } } @Override public void deleteBook(final BookDto book) throws SQLException, BusinessException { if (!booksDao.deleteByISBN(book).equals(1)) { throw new BusinessException(book.getIsbn() + " : deleted or updated by someone else."); } } }

サービス層の実装クラスから、データアクセス層の実装クラスが消えて、依存関係を一つ減らすことが出来た。

このサービスの実装クラスのインスタンスを作る。 インスタンス生成時に、さっき追加したsetterでDAOの実装クラスを設定する。 厳密には、インスタンス生成後、初回使用よりも前、というタイミングなのだが、まあどうでもいいか。 インスタンス生成は ServiceClassLoader にやらせる。

package framework.service; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.util.Map; import framework.exception.SystemException; public class ServiceClassLoader { public static Object getServiceImpl(final ServiceDef serviceDef, final Connection conn) { try { Object serviceImpl = getServiceInstance(serviceDef.getServiceClassName()); Map<String, String> daoDef = serviceDef.getDaoDef(); for (String daoId : daoDef.keySet()) { Object daoImpl = getDaoInstance(daoDef.get(daoId), conn); Method setter = serviceImpl.getClass().getMethod(toSetterName(daoId), daoImpl.getClass().getInterfaces()); setter.invoke(serviceImpl, daoImpl); } return serviceImpl; } catch (IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException(e); } } private static Object getServiceInstance(final String serviceClass) { try { return Class.forName(serviceClass).newInstance(); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { throw new SystemException(e); } } private static Object getDaoInstance(final String daoClass, final Connection conn) { try { return Class.forName(daoClass).getConstructor(Connection.class).newInstance(conn); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | ClassNotFoundException e) { throw new SystemException(e); } } private static String toSetterName(final String fieldName) { return "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); } }

サービスが使用するDAOは一つとは限らないので、実装クラスのインスタンスを作っては注入を繰り返す。 DAOの指定が無ければ、サービスの実装クラスを作るだけ。 引数を取るコンストラクターを使う手順がちょっと面倒。

これらを使うように、 ServiceFactory を再び変更する。

package framework.service; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.sql.Connection; import framework.exception.SystemException; public class ServiceFactory { private static ServiceDefLoader loader = null; public static <T> T getService(final Connection conn, final String serviceId) { if (loader == null) { loader = new ServiceDefLoader("serviceDef.xml"); } Object serviceImpl = ServiceClassLoader.getServiceImpl(loader.getServiceDef(serviceId), conn); return (T) Proxy.newProxyInstance( serviceImpl.getClass().getClassLoader(), serviceImpl.getClass().getInterfaces(), new TransactionInvocationHandler(conn, serviceImpl)); } }

実装クラスのインスタンス生成が別のクラスに移ったので、一気にシンプルになった。 プロクシを作るところで見やすいように改行しているが、処理内容は実質3行だからね。

アプリケーション層には変更無し。

現在の構成は以下の通り。

apl Main.java service BooksService.java impl BooksServiceImpl.java dao BooksDao.java impl BooksDaoImpl.java dto BookDto.java util DbUtil.java framework annotation Commit.java Rollback.java service ServiceClassLoader.java ServiceDef.java ServiceDefLoader.java ServiceFactory.java TransactionInvocationHandler.java dao CmnDao.java DtoConverter.java SqlDefLoader.java SqlDef.java exception BusinessException.java SystemException.java serviceDef.xml sqlDef.xml

だいぶコードが増えて来たな。 そして、コードが増える一方でコメントは全く増えないのだな。 というか1文字も無い。

沈黙は金。