本誌からのお知らせ

「実演!リファクタリング」 WEB+DB PRESS Vol.37特集 特設ページ

この記事を読むのに必要な時間:およそ 12 分

複合リファクタリング
『Commandによる条件つきディスパッチャの置き換え』

第7章の実演ムービー全11本です。複合リファクタリング『Commandによる条件つきディスパッチャの置き換え』を行います。

複合リファクタリングとは,小さなリファクタリングを連続させて比較的大きな変更を安全に行うためのリファクタリング手法です。誌面では第7章で『Commandによる条件つきディスパッチャの置き換え』という複合リファクタリングを行っています。誌面では紙幅の関係で細部までは解説できませんでしたので,実際にどうやって長いリファクタリングを行っていくのかをムービーの形で提供させていただきます。

第7章の実演ムービー全11本-簡易目次

手順1:条件分岐の中に『メソッドの抽出』を適用する
その1:条件分岐の中にロジックを集める
その2:「実行メソッド」を抽出する
手順2:各実行メソッドに『クラスの抽出』を適用する
その1:メソッドの移動(リストアクセス編)
その1:メソッドの移動(IDアクセス編)
その2:メソッドからコンストラクタへの引数の移行(リストアクセス編)
その2:メソッドからコンストラクタへの引数の移行(IDアクセス編)
その3:メソッドwriteXmlTo(response, xml)の具象コマンドクラスへの移動
手順3:具象コマンドに『スーパークラスの抽出』『インタフェースの抽出』を適用することで「コマンド」を作成する
「コマンド」の作成[前編]
「コマンド」の作成[後編]
手順4:クライアントコードをコマンドを使うように修正する
クライアントコードの修正
双方向関連の解消
双方向関連の解消

⁠複合リファクタリング『Commandによる条件つきディスパッチャの置き換え⁠⁠」ムービーを一括してダウンロード(約35MB)

手順1:条件分岐の中に『メソッドの抽出』を適用する

その1:条件分岐の中にロジックを集める[映像時間:3:56]
『Commandによる条件つきディスパッチャの置き換え』を行うために,まず別クラスに抽出したいコードをメソッドに抽出します。その前に,メソッド抽出の前準備として,抽出したいロジックを条件分岐の中に集めます。⁠一時変数のインライン化』やコピー&ペーストを使用してif文の中に移動させています。

リスト1 RestOperationDelegateImpl.java(6章リスト3を変更)

public void doGet(HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    if(pathInfoTokenizer.isListAccess(request.getPathInfo())) {
        String daoPrefix =
            pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        String listUri = request.getRequestURL().toString();
        List resourceList = daoOperation.getAll(daoPrefix);
        String xml =
            entityListMarshaller.marshal(
                resourceList, daoPrefix, listUri);
        writeXmlTo(response, xml);
    } else {
        String daoPrefix =
            pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        long entityId =
            pathInfoTokenizer.getEntityId(request.getPathInfo());
        Object result = daoOperation.getById(daoPrefix, entityId);
        String xml = entityMarshaller.marshal(result);
        writeXmlTo(response, xml);
    }
}
その2:「実行メソッド」を抽出する[映像時間:1:52]
手順1その1で条件分岐の中に集めたコードを,⁠メソッドの抽出」⁠[Alt]+[Shift]+[M])でメソッドに抽出します。⁠Commandによる条件つきディスパッチャの置き換え』では,このような形で抽出したメソッドを「実行メソッド」と呼んでいます。

リスト2 RestOperationDelegateImpl.java(リスト1を変更)

public class RestOperationDelegateImpl
        implements RestOperationDelegate {

    public void doGet(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        if(pathInfoTokenizer.isListAccess(request.getPathInfo())) {
            handleListAccess(request, response);
        } else {
            handleIdAccess(request, response);
        }
    }

    private void handleListAccess(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String daoPrefix =
            pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        String listUri = request.getRequestURL().toString();
        List resourceList = daoOperation.getAll(daoPrefix);
        String xml =
            entityListMarshaller.marshal(
                resourceList, daoPrefix, listUri);
        writeXmlTo(response, xml);
    }

    private void handleIdAccess(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String daoPrefix =
            pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        long entityId =
            pathInfoTokenizer.getEntityId(request.getPathInfo());
        Object result = daoOperation.getById(daoPrefix, entityId);
        String xml = entityMarshaller.marshal(result);
        writeXmlTo(response, xml);
    }
…
}

手順2:各実行メソッドに『クラスの抽出』を適用する

その1:メソッドの移動(リストアクセス編)[映像時間:5:01]
手順1その2で抽出したメソッドを,Eclipseのリファクタリング機能「メソッドの移動」⁠[Alt]+[Shift]+[V])を使って新規作成したクラスに移動します。⁠メソッドの移動」を効率的に行うために,Eclipseの機能に合わせたトリッキーなコーディングを行っています。

リスト3 RestOperationDelegateImpl.java(リスト2を変更)

public class RestOperationDelegateImpl
        implements RestOperationDelegate {
    ListAccessHandler handler = new ListAccessHandler();

    public void doGet(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        if(pathInfoTokenizer.isListAccess(request.getPathInfo())) {
            ListAccessHandler handler = new ListAccessHandler();
            handler.handleListAccess(this, request, response);
        } else {
            handleIdAccess(request, response);
        }
    }

    private void handleListAccess(
            HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String daoPrefix =
            pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        String listUri = request.getRequestURL().toString();
        List resourceList = daoOperation.getAll(daoPrefix);
        String xml =
            entityListMarshaller.marshal(
                resourceList, daoPrefix, listUri);
        writeXmlTo(response, xml);
    }

    private void handleIdAccess(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String daoPrefix =
            pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        long entityId =
            pathInfoTokenizer.getEntityId(request.getPathInfo());
        Object result = daoOperation.getById(daoPrefix, entityId);
        String xml = entityMarshaller.marshal(result);
        writeXmlTo(response, xml);
    }

    private void writeXmlTo(
            HttpServletResponse response,
            String xml) throws IOException {
…
    }

    private EntityListMarshaller entityListMarshaller;
…
    private EntityMarshaller entityMarshaller;
…
    private PathInfoTokenizer pathInfoTokenizer;
…
    private DaoOperation daoOperation;
…
}

リスト4 ListAccessHandler.java

public class ListAccessHandler {

    void handleListAccess(RestOperationDelegateImpl facade,
            HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String daoPrefix =
            facade.pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        String listUri = request.getRequestURL().toString();
        List resourceList = facade.daoOperation.getAll(daoPrefix);
        String xml =
            facade.entityListMarshaller.marshal(
            resourceList, daoPrefix, listUri);
        facade.writeXmlTo(response, xml);
    }
}
その1:メソッドの移動(IDアクセス編)[映像時間:(5:14)]
同様の作業をもう1つのメソッド(IDに対するアクセスを扱うメソッド)に対して行います。紙幅の関係で誌面には登場しません。
その2:メソッドからコンストラクタへの引数の移行(リストアクセス編)[映像時間:(5:14)]
Eclipseのクイックフィクス機能([Ctrl]+[1])を使用して新しいコンストラクタを効率的に作成し,さらにEclipseのリファクタリング機能「メソッドシグニチャの変更」⁠[Alt]+[Shift]+[C])を使用して既存のメソッドのシグニチャを変更しています。

リスト5 RestOperationDelegateImpl.java(リスト3を変更)

public void doGet(HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    if(pathInfoTokenizer.isListAccess(request.getPathInfo())) {
        ListAccessHandler handler =
            new ListAccessHandler(this, request, response);
        handler.handle();
    } else {
        IdAccessHandler handler = new IdAccessHandler();
        handler.handleIdAccess(this, request, response);
    }
}

リスト6 ListAccessHandler.java(リスト4を変更)

public class ListAccessHandler {

    private final RestOperationDelegateImpl facade;
    private final HttpServletRequest request;
    private final HttpServletResponse response;

    public ListAccessHandler(RestOperationDelegateImpl facade,
            HttpServletRequest request,
            HttpServletResponse response) {
        this.facade = facade;
        this.request = request;
        this.response = response;
    }

    void handle() throws IOException {
…
    }

}
その2:メソッドからコンストラクタへの引数の移行(IDアクセス編)[映像時間:2:15]
同様の作業をもう1つのクラス(IdAccessHandler)に対して行います。紙幅の関係で本誌には登場しません。
その3:メソッドwriteXmlTo(response, xml)の具象コマンドクラスへの移動[映像時間:3:53]
Eclipseのリファクタリング機能「インライン化」⁠[Alt]+[Shift]+[I])を活用して,メソッドの移動相当の作業を行うテクニックを紹介します。

リスト7 RestOperationDelegateImpl.java(リスト5を変更)

public class RestOperationDelegateImpl
        implements RestOperationDelegate {

    public void doGet(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
…
    }

    void writeXmlTo(
            HttpServletResponse response,
            String xml) throws IOException {
        response.setContentType("text/xml");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = response.getWriter();
        IOUtils.copy(new StringReader(xml), writer);
        writer.flush();
    }
…
}

リスト8 ListAccessHandler.java(リスト6を変更)

public class ListAccessHandler {
…
    void handle() throws IOException {
        String daoPrefix = facade.pathInfoTokenizer.getDaoPrefix(request.getPathInfo());
        String listUri = request.getRequestURL().toString();
        List resourceList = facade.daoOperation.getAll(daoPrefix);
        String xml =
            facade.entityListMarshaller.marshal(
                resourceList, daoPrefix, listUri);
        writeXml(xml);
    }

    void writeXml(String xml) throws IOException {
        response.setContentType("text/xml");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = response.getWriter();
        IOUtils.copy(new StringReader(xml), writer);
        writer.flush();
    }
}

手順3:具象コマンドに『スーパークラスの抽出』『インタフェースの抽出』を適用することで「コマンド」を作成する

「コマンド」の作成[前編][映像時間:7:43]
「コマンド」の作成[後編][映像時間:7:30]
前編では,2つのコマンドクラスの間に生じた『重複したコード』を,⁠スーパークラスの抽出』『メソッドの引き上げ』を行うことで解消します。
まずEclipseのリファクタリング機能「スーパークラスの抽出」⁠[Alt]+[Shift][T⁠⁠ → ⁠T⁠⁠)を使用して『スーパークラスの抽出』を行います。次にEclipseのリファクタリング機能「プルアップ」⁠Alt][Shift][T⁠⁠ → ⁠U⁠⁠)を使用して『フィールドの引き上げ』を行い,最後にもう一度「プルアップ」を使用して今度は『メソッドの引き上げ』を行います。

後編では,Eclipseのリファクタリング機能を使って抽象メソッドやインタフェースなどの継承構造を扱う方法を解説します。
まず前編で抽出したスーパークラスに対して「プルアップ」を使用して抽象メソッドを引き上げます。次にそのスーパークラスに対して『インタフェースの抽出』⁠Alt][Shift][T⁠⁠ → ⁠E⁠⁠)を行います。最後に抽出したインタフェースにメソッド定義を「プルアップ」することで,メソッド宣言をインタフェースまで移動します。

リスト9 AbstractGetMethodAccessHandler.java

public abstract class AbstractGetMethodAccessHandler
        implements GetMethodAccessHandler {
    protected RestOperationDelegateImpl facade;
    protected HttpServletRequest request;
    protected HttpServletResponse response;

    public AbstractGetMethodAccessHandler(
            RestOperationDelegateImpl facade,
            HttpServletRequest request,
            HttpServletResponse response) {
        this.facade = facade;
        this.request = request;
        this.response = response;
    }

    protected void writeXml(String xml) throws IOException {
        response.setContentType("text/xml");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = response.getWriter();
        IOUtils.copy(new StringReader(xml), writer);
        writer.flush();
    }

    public abstract void handle() throws IOException;
}

リスト10 ListAccessHandler.java(リスト8を変更)

public class ListAccessHandler
        extends AbstractGetMethodAccessHandler {

    public ListAccessHandler(RestOperationDelegateImpl facade,
            HttpServletRequest request,
            HttpServletResponse response) {
        super(facade, request, response);
    }

    public void handle() throws IOException {
…
    }
}

リスト11 GetMethodAccessHandler.java

public interface GetMethodAccessHandler {
    void handle() throws IOException;
}

手順4:クライアントコードをコマンドを使うように修正する

クライアントコードの修正[映像時間:1:14]
手順3で作成したインタフェースGetMethodAccessHandlerを使うように,クライアントコード(ここではRestOperationDelegateImpl)を修正しています。

リスト12 RestOperationDelegateImpl.java(リスト7を変更)

public void doGet(HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    if(pathInfoTokenizer.isListAccess(request.getPathInfo())) {
        GetMethodAccessHandler handler =
            new ListAccessHandler(this, request, response);
        handler.handle();
    } else {
        GetMethodAccessHandler handler =
            new IdAccessHandler(this, request, response);
        handler.handle();
    }
}

双方向関連の解消

双方向関連の解消[映像時間:10:06]
RestOperationDelegateImplと2つのコマンドクラスとの間の双方向関連を解消し,DIコンテナに対応するための布石を打ちます。

リスト13 RestOperationDelegateImpl.java(リスト12を変更)

public void doGet(HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    if(pathInfoTokenizer.isListAccess(request.getPathInfo())) {
        GetMethodAccessHandler handler =
            new ListAccessHandler(
                this, request, response,
                pathInfoTokenizer, daoOperation,
                entityListMarshaller);
        handler.handle();
    } else {
        GetMethodAccessHandler handler =
            new IdAccessHandler(
                this, request, response,
                pathInfoTokenizer, daoOperation,
                entityMarshaller);
        handler.handle();
    }
}

リスト14 ListAccessHandler.java(リスト10を変更)

public class ListAccessHandler
        extends AbstractGetMethodAccessHandler {

    private final EntityListMarshaller entityListMarshaller;
    private final DaoOperation daoOperation;
    private final PathInfoTokenizer pathInfoTokenizer;

    public ListAccessHandler(
            RestOperationDelegateImpl facade,
            HttpServletRequest request,
            HttpServletResponse response,
            PathInfoTokenizer pathInfoTokenizer,
            DaoOperation daoOperation,
            EntityListMarshaller entityListMarshaller) {
        super(facade, request, response);
        this.pathInfoTokenizer = pathInfoTokenizer;
        this.daoOperation = daoOperation;
        this.entityListMarshaller = entityListMarshaller;
    }

    public void handle() throws IOException {
        String daoPrefix =
            facade.pathInfoTokenizer.getDaoPrefix(
                request.getPathInfo());
        String listUri = request.getRequestURL().toString();
        List resourceList =
            facade.daoOperation.getAll(daoPrefix);
        String xml =
            facade.entityListMarshaller.marshal(
                resourceList, daoPrefix, listUri);
        writeXml(xml);
    }

}

リスト15 RestOperationDelegateImpl.java(リスト13を変更)

public class RestOperationDelegateImpl
        implements RestOperationDelegate {

    public void doGet(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        if(pathInfoTokenizer.isListAccess(
                request.getPathInfo())) {
            GetMethodAccessHandler handler =
                new ListAccessHandler(
                    this, request, response,
                    pathInfoTokenizer, daoOperation,
                    entityListMarshaller);
            handler.handle();
        } else {
            GetMethodAccessHandler handler =
                new IdAccessHandler(
                    this, request, response,
                    pathInfoTokenizer, daoOperation,
                    entityMarshaller);
            handler.handle();
        }
    }

    private EntityListMarshaller entityListMarshaller;
…
    private EntityMarshaller entityMarshaller;
…
    private PathInfoTokenizer pathInfoTokenizer;
…
    private DaoOperation daoOperation;
…
}

リスト16 AbstractGetMethodAccessHandler.java(リスト9を変更)

public abstract class AbstractGetMethodAccessHandler
        implements GetMethodAccessHandler {
    protected RestOperationDelegateImpl facade;
    protected HttpServletRequest request;
    protected HttpServletResponse response;

    public AbstractGetMethodAccessHandler(
            RestOperationDelegateImpl facade,
            HttpServletRequest request,
            HttpServletResponse response) {
        this.facade = facade;
        this.request = request;
        this.response = response;
    }
…
}

リスト17 ListAccessHandler.java(リスト14を変更)

public ListAccessHandler(
        RestOperationDelegateImpl facade,
        HttpServletRequest request,
        HttpServletResponse response,
        PathInfoTokenizer pathInfoTokenizer,
        DaoOperation daoOperation,
        EntityListMarshaller entityListMarshaller) {
    super(facade, request, response);
    this.pathInfoTokenizer = pathInfoTokenizer;
    this.daoOperation = daoOperation;
    this.entityListMarshaller = entityListMarshaller;
}