このシリーズの前回までの 2 回の記事では、テスト駆動開発 (TDD: Test-Driven Development) を使用することでインクリメンタルに設計を発見できることを説明しました。この方法はゼロからプロジェクトを開始する場合には非常に有効です。しかし、非常に優れているというわけではないコードが大量にあるような、もっと一般的な場合にはどうすればよいのでしょう。どのようにすれば、古いコード・ベースの中に隠れている再利用可能な資産や設計を見つけられるのでしょう。
今回の記事では、コードをリファクタリングして再利用可能な資産を見つける上で役立つ、古くからある 2 つのパターン、Composed Method と SLAP (Single Level of Abstraction Principle) について説明します。適切な設計の要素は既にコードの中に存在しています。皆さんにとって必要なものは、既に作成されている隠れた資産を掘り出すために役立つツールのみなのです。
残念なことに、技術変化の速いことが影響して、私たち開発者はソフトウェアを開発する際の古くからある教えを無視しがちです。私達は、数年以上前のものはどれも古すぎて使えないと考えてしまいがちです。しかしもちろん、それは真実ではありません。開発者が重要な知識を身につける上で役に立つ本は数多くあります。現在ではほとんど読まれていない、そうした古い本の 1 つが、Kent Beck による『Smalltalk ベストプラクティス・パターン―シンプル・デザインへの宝石集 』です (「参考文献」を参照)。Java 開発者である皆さんは、「Smalltalk に関する 13 年も前の本が一体自分とどう関係するのだろう」と思うかもしれませんが、実はこの本を読むとわかるように、Smalltalk を使用していた人達はオブジェクト指向言語でプログラミングを行った最初の開発者であり、彼らは優れたアイデアを沢山生み出したのです。そのなかの 1 つが Composed Method です。
Composed Method パターンの重要なポイントは以下の 3 文で表されます。
- プログラムを複数のメソッドに分割し、識別可能な 1 つのタスクをそれぞれのメソッドが実行するようにします。
- 1 つのメソッドの中のすべての操作を同じ抽象化レベルに保ちます。
- これにより、数行の長さの簡単なメソッドを数多く持つ複数のプログラムが自然に出来上がります。
私は「Test-driven design, Part 1」の中で、実際のコードを作成する前にユニット・テストを作成するという状況で Composed Method について説明しました。TDD に厳密に従って開発を行えば、Composed Method に忠実に従うメソッドが自然に作成されます。しかしコードが既に存在している場合にはどうすればよいのでしょう。その方法を知るために、Composed Method を使って隠れた設計を見つけ出すことにしましょう。
皆さんはおそらく、非常に影響力のある Gang of Four が著した書籍『Design Patterns』(「参考文献」を参照) によって一般的になったデザイン・パターンの正式な動作についてはよく理解していることと思います。デザイン・パターンでは、あらゆるプロジェクトに適用可能な汎用的なパターンを扱います。一方でどのソリューションにも、本にまとめるほどではないにせよ至る所に登場するイディオムのようなパターンが含まれています。イディオムのようなパターンはコードの中にある共通の設計イディオムを表現しています。新方式の設計での重要なポイントは、こうしたパターンを発見する点にあります。こうしたパターンには、純粋に技術的なパターン (例えば、あるプロジェクトでのトランザクションの処理方法など) の場合もあれば、その領域に特有の問題のためのパターン (例えば「発送処理に進む前に必ず顧客の信用情報をチェックする」など) の場合もあるなど、非常に幅があります。
リスト 1 の簡単なメソッドについて考えてみましょう。このメソッドは、下位レベルの JDBC を使ってデータベースに接続し、Part オブジェクトを収集して List の中に配置するためのメソッドです。
リスト 1.
Part を収集するための簡単なメソッド
public void populate() throws Exception {
Connection c = null;
try {
Class.forName(DRIVER_CLASS);
c = DriverManager.getConnection(DB_URL, USER, PASSWORD);
Statement stmt = c.createStatement();
ResultSet rs = stmt.executeQuery(SQL_SELECT_PARTS);
while (rs.next()) {
Part p = new Part();
p.setName(rs.getString("name"));
p.setBrand(rs.getString("brand"));
p.setRetailPrice(rs.getDouble("retail_price"));
partList.add(p);
}
} finally {
c.close();
}
}
|
リスト 1 では特に複雑なことは何も行っていません。また明らかなことですが、再利用可能なコードも含まれていません。このメソッドは非常に簡単なものですが、やはりリファクタリングが必要です。Composed Method の方式では各メソッドが 1 つのことのみを行う必要がありますが、このメソッドはそのルールに違反しているのです。Java プロジェクトに関する私の経験則では、コードが約 10 行を超えるメソッドにはすべてリファクタリングが必要です。なぜなら、そうしたメソッドは複数のことをしている可能性が高いからです。そこで、Composed Method を念頭にこのメソッドをリファクタリングし、アトミックな部分を分離できないかどうか調べてみましょう。リファクタリングしたバージョンがリスト 2 です。
リスト 2. リファクタリングした
populate() メソッド
public void populate() throws Exception {
Connection c = null;
try {
c = getDatabaseConnection();
ResultSet rs = createResultSet(c);
while (rs.next())
addPartToListFromResultSet(rs);
} finally {
c.close();
}
}
private ResultSet createResultSet(Connection c)
throws SQLException {
return c.createStatement().
executeQuery(SQL_SELECT_PARTS);
}
private Connection getDatabaseConnection()
throws ClassNotFoundException, SQLException {
Connection c;
Class.forName(DRIVER_CLASS);
c = DriverManager.getConnection(DB_URL,
"webuser", "webpass");
return c;
}
private void addPartToListFromResultSet(ResultSet rs)
throws SQLException {
Part p = new Part();
p.setName(rs.getString("name"));
p.setBrand(rs.getString("brand"));
p.setRetailPrice(rs.getDouble("retail_price"));
partList.add(p);
}
|
これで、populate() メソッドは大幅に短くなりました。しかも、このメソッドが行うさまざまなタスクの概要を読み取ることができ、各タスクの実装は private メソッドの中にあります。アトミックな部分をすべて抽出してみると、実際にどんな資産があるのかを見ることができます。getDatabaseConnection() メソッドが parts とは何も関係がないことに注目してください。このメソッドはデータベースに接続するための汎用的な機能です。つまり、このメソッドはこのクラスの中にあるべきではないということです。そこで、PartDb クラスの親として動作する BoundaryBase クラスの中にこのメソッドが入るようにリファクタリングします。
リスト 2 の中で、他にも親クラスに入れた方がよい汎用的なメソッドはあるでしょうか。createResultSet() は非常に汎用的な名前のメソッドですが、このメソッドには parts (つまり SQL_SELECT_PARTS 定数) へのリンクがあります。この SQL ストリングの値を子クラス (PartDb) から親クラスに強制的に伝える方法がわかれば、このメソッドも抽出することができます。抽象メソッドは、まさにこのためにあります。そこで createResultSet() を取り出し、その仲間である getSqlForEntity() メソッドという名前の抽象メソッドと共に BoundaryBase クラスの中に入れます (リスト 3)。
リスト 3. ここまでの段階での
BoundaryBase クラス
abstract public class BoundaryBase {
private static final String DRIVER_CLASS =
"com.mysql.jdbc.Driver";
private static final String DB_URL =
"jdbc:mysql://localhost/orderentry";
protected Connection getDatabaseConnection() throws ClassNotFoundException,
SQLException {
Connection c;
Class.forName(DRIVER_CLASS);
c = DriverManager.getConnection(DB_URL, "webuser", "webpass");
return c;
}
abstract protected String getSqlForEntity();
protected ResultSet createResultSet(Connection c) throws SQLException {
Statement stmt = c.createStatement();
return stmt.executeQuery(getSqlForEntity());
}
|
これは楽しい作業でした。子クラスから取り出して汎用の親クラスに入れられるメソッドは他にもあるのでしょうか。リスト 2 の populate() メソッド自体を見ると、このメソッドと PartDb クラスとを結びつけているのは、getDatabaseConnection() メソッドと createResultSet() メソッド、そして addPartToListFromResultSet() メソッドです。最初の 2 つのメソッドは既に親クラスに移動してあります。addPartToListFromResultSet() メソッドを抽象化すると (それと同時に適切な汎用の名前に変更すると)、populate() メソッド全体を取り出して親の中に入れることができます。これを行ったものがリスト 4 です。
リスト 4.
BoundaryBase クラス
abstract public class BoundaryBase {
private static final String DRIVER_CLASS =
"com.mysql.jdbc.Driver";
private static final String DB_URL =
"jdbc:mysql://localhost/orderentry";
protected Connection getDatabaseConnection() throws ClassNotFoundException,
SQLException {
Connection c;
Class.forName(DRIVER_CLASS);
c = DriverManager.getConnection(DB_URL, "webuser", "webpass");
return c;
}
abstract protected String getSqlForEntity();
protected ResultSet createResultSet(Connection c) throws SQLException {
Statement stmt = c.createStatement();
return stmt.executeQuery(getSqlForEntity());
}
abstract protected void addEntityToListFromResultSet(ResultSet rs)
throws SQLException;
public void populate() throws Exception {
Connection c = null;
try {
c = getDatabaseConnection();
ResultSet rs = createResultSet(c);
while (rs.next())
addEntityToListFromResultSet(rs);
} finally {
c.close();
}
}
}
|
これらのメソッドをすべて取り出して親クラスに移動すると、PartDb クラスは大幅に単純になります (リスト 5)。
リスト 5. 単純化され、リファクタリングされた
PartDb クラス
public class PartDb extends BoundaryBase {
private static final int DEFAULT_INITIAL_LIST_SIZE = 40;
private static final String SQL_SELECT_PARTS =
"select name, brand, retail_price from parts";
private static final Part[] TEMPLATE = new Part[0];
private ArrayList partList;
public PartDb() {
partList = new ArrayList(DEFAULT_INITIAL_LIST_SIZE);
}
public Part[] getParts() {
return (Part[]) partList.toArray(TEMPLATE);
}
protected String getSqlForEntity() {
return SQL_SELECT_PARTS;
}
protected void addEntityToListFromResultSet(ResultSet rs)
throws SQLException {
Part p = new Part();
p.setName(rs.getString("name"));
p.setBrand(rs.getString("brand"));
p.setRetailPrice(rs.getDouble("retail_price"));
partList.add(p);
}
}
|
このようにリファクタリングした結果、何を実現できたのでしょう。第 1 に、以前よりも特定のジョブに明確に焦点を絞った 2 つのクラスが得られました。どちらのクラスのメソッドもすべて簡潔であり、そのためそれらのメソッドを容易に理解することができます。第 2 に、PartDb クラスが parts のみに関係しており、parts 以外のものとは無関係であることに注目してください。汎用的な接続用のボイラープレート・コードはすべて親クラスの中に移動されました。第 3 に、今や各メソッドは (populate() を除き) 1 つのことしか実行しないため、これらのメソッドはすべてテスト可能です。これらのクラスの実際のワークフローを実行するメソッドは populate() メソッドです。populate() メソッドは他のすべての (private な) メソッドを使用して作業を行い、また実行されるステップの概要は populate() メソッドを見るとわかります。第 4 に、ビルディング・ブロックが小さくなったため、メソッドをさまざまに組み合わせることができ、メソッドが再利用しやすくなりました。つまり最初に紹介した populate() メソッドのような大きなメソッドを使う可能性は低く、これ以降のクラスで、最初の populate() メソッドとまったく同じことをまったく同じ順序で行うことは、ほとんど考えられません。アトミックなメソッドにすることで、さまざまな機能の組み合わせが可能になるのです。
こうしたリファクタリングによる本当に重要なメリットは、再利用可能なコードが得られることです。リスト 1 のコードを見ても再利用可能な資産は見つからず、単純にコードが羅列されているにすぎません。ところが olio メソッドを分解してみると、再利用可能な資産が見つかります。しかしメリットは再利用可能なコードが得られることだけではありません。リファクタリングによって、アプリケーションの中にある一貫したものを扱う単純なフレームワークの基礎も作成できたのです。データベースから何らかのエンティティーを収集するために単純なバウンダリー・クラスを別途作成する際には、そのために使用できるコードが既に用意されているのです。象牙の塔の中でフレームワークを作成するのではなく、実際のコードからフレームワークを抽出することの本質が、ここにあります。
再利用可能な資産を抽出することによって、そのアプリケーションを構成する山のように大量のコードをとおしてアプリケーションの設計全体が輝き始めます。新方式の設計の目標の 1 つは、アプリケーションの中でイディオムのようなパターンで使われている部分を見つけることです。BoundaryBase と PartDb とを組み合わせると、このアプリケーションの中に繰り返し現れる有効なパターンが得られます。可変部分を小さくすることで、それらをどのように組み合わせればよいかを把握しやすくなります。
Composed Method の重要なポイントの 2 番目として、「1 つのメソッドの中のすべての操作を同じ抽象化レベルに保つ」必要があります。この原則を 1 つの例に適用してみると、この原則の意味、またこの原則が設計に及ぼす影響を理解しやすくなります。
簡単な E コマース・アプリケーションから引用したリスト 6 のコードを考えてみてください。addOrder() メソッドはいくつかのパラメーターを引数に取り、注文 (order) の情報をデータベースの中に格納します。
リスト 6. E コマースのサイトから引用した
addOrder() メソッド
public void addOrder(ShoppingCart cart, String userName,
Order order) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
Statement s = null;
ResultSet rs = null;
boolean transactionState = false;
try {
s = c.createStatement();
transactionState = c.getAutoCommit();
int userKey = getUserKey(userName, c, ps, rs);
c.setAutoCommit(false);
addSingleOrder(order, c, ps, userKey);
int orderKey = getOrderKey(s, rs);
addLineItems(cart, c, orderKey);
c.commit();
order.setOrderKeyFrom(orderKey);
} catch (SQLException sqlx) {
s = c.createStatement();
c.rollback();
throw sqlx;
} finally {
try {
c.setAutoCommit(transactionState);
dbPool.release(c);
if (s != null)
s.close();
if (ps != null)
ps.close();
if (rs != null)
rs.close();
} catch (SQLException ignored) {
}
}
}
|
addOrder() メソッドには面倒な処理が大量に含まれています。ただし私にとって特に関心があるものは、try ブロックの先頭の近くにあるワークフローです。連続した下記の 2 行に注目してください。
c.setAutoCommit(false); addSingleOrder(order, c, ps, userKey); |
この 2 行のコードを見ると SLAP に違反していることがわかります。最初の行 (そしてその上にあるメソッド群) はデータベースの基盤をセットアップするための下位レベルの詳細を処理しています。2 番目の行はビジネス・アナリストが理解するような上位レベルのメソッドです。この 2 行は 2 つの異なる世界のものです。場所によって抽象化のレベルを切り換えて考える必要があると、コードが読みにくくなります。それを避けるためのものが SLAP です。読みやすさに問題があると、必然的な結果として、コードが実行する内容のベースとなっている設計を理解しにくくなり、この特定のアプリケーションのイディオムのようなパターンを分離しにくくなります。
リスト 6 のコードを改善するために、SLAP を念頭に置いて、このコードをリファクタリングしてみます。メソッドを抽出するリファクタリングを 2、3 回行うと、リスト 7 のようなコードになります。
リスト 7.
addOrder() メソッドの抽象化を改善する
public void addOrderFrom(ShoppingCart cart, String userName,
Order order) throws SQLException {
setupDataInfrastructure();
try {
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
completeTransaction();
} catch (SQLException sqlx) {
rollbackTransaction();
throw sqlx;
} finally {
cleanUp();
}
}
private void setupDataInfrastructure() throws SQLException {
_db = new HashMap();
Connection c = dbPool.getConnection();
_db.put("connection", c);
_db.put("transaction state",
Boolean.valueOf(setupTransactionStateFor(c)));
}
private void cleanUp() throws SQLException {
Connection connection = (Connection) _db.get("connection");
boolean transactionState = ((Boolean)
_db.get("transation state")).booleanValue();
Statement s = (Statement) _db.get("statement");
PreparedStatement ps = (PreparedStatement)
_db.get("prepared statement");
ResultSet rs = (ResultSet) _db.get("result set");
connection.setAutoCommit(transactionState);
dbPool.release(connection);
if (s != null)
s.close();
if (ps != null)
ps.close();
if (rs != null)
rs.close();
}
private void rollbackTransaction()
throws SQLException {
((Connection) _db.get("connection")).rollback();
}
private void completeTransaction()
throws SQLException {
((Connection) _db.get("connection")).commit();
}
private boolean setupTransactionStateFor(Connection c)
throws SQLException {
boolean transactionState = c.getAutoCommit();
c.setAutoCommit(false);
return transactionState;
}
|
これで、このメソッドがはるかに読みやすくなりました。メインの本体は Composed Method の目標に従っており、この本体を見ればこの本体が実行するステップの概要を読み取ることができます。メソッド群はかなり上位のレベルで記述されているため、そのメソッドが実行する内容を説明するために技術者以外の人にも見せられるほどです。completeTransaction() メソッドをよく見ると、このコードが 1 行であることがわかります。この 1 行のコードを addOrder() メソッドの中に戻すことができないかと思うかもしれませんが、戻してしまうと、コードが読みにくくなり、抽象化レベルが一定ではなくなってしまいます。上位レベルのビジネス・ワークフローからトランザクションの本質的な詳細にまで抽象化レベルを大幅に変化させることは SLAP に違反します。completeTransaction() メソッドがあることによって、このコードは概念的なものに抽象化され、具体的な詳細から遠ざけられます。将来、データベースへのアクセス方法を変更する場合には、completeTransaction() メソッドの内容を変更すればよく、呼び出し側のコードを変更する必要がありません。
SLAP の目標はコードを読みやすく、理解しやすいものにすることです。しかし SLAP は、コードの中に存在するイディオムのようなパターンを発見するためにも役立ちます。トランザクション・ブロックによって更新が保護されている様子を見ると、そうしたパターンの 1 つが見えてくることに注目してください。addOrder() メソッドをさらにリファクタリングすると、メソッドの組み合わせにすることができます (リスト 8)。
リスト 8. トランザクション型のアクセス・パターン
public void wrapInTransaction(Command c) throws SQLException {
setupDataInfrastructure();
try {
c.execute();
completeTransaction();
} catch (RuntimeException ex) {
rollbackTransaction();
throw ex;
} finally {
cleanUp();
}
}
public void addOrderFrom(final ShoppingCart cart, final String userName,
final Order order) throws SQLException {
wrapInTransaction(new Command() {
public void execute() throws SQLException{
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
}
});
}
|
ここには wrapInTransaction() メソッドを追加してあります。このメソッドは、このアプリケーションの中にあるこの共通パターンを Gang of Four による Command デザイン・パターン (「参考文献」を参照) のインライン・バージョンを使って実装しています。このコードを確実かつ適切に動作させるために必要な詳細は、すべて wrapInTransaction() メソッドによって処理されます。見た目の悪いボイラープレート・コード (addOrderFrom() メソッドの本体の中にある 2 行のコード) が少し残っていますが、それはこのメソッドの真の目的をラップする匿名内部クラスのためです。このリソース保護ブロックはコードの中に繰り返し現れるため、上の階層に移動させる候補になりそうです。
匿名内部クラスを使って wrapInTransaction() コードを実装した理由から、言語の構文の表現に関する重要なポイントがわかります。このコードを Groovy で作成すると、もっとスマートに同じことを行うバージョンを、ネイティブのクロージャー・ブロックを使って作成することができます (リスト 9)。
リスト 9. トランザクション型のアクセスを Groovy のクロージャーを使ってラップする
public class OrderDbClosure {
def wrapInTransaction(command) {
setupDataInfrastructure()
try {
command()
completeTransaction()
} catch (RuntimeException ex) {
rollbackTransaction()
throw ex
} finally {
cleanUp()
}
}
def addOrderFrom(cart, userName, order) {
wrapInTransaction {
add order, userKeyBasedOn(userName)
addLineItemsFrom cart, order.getOrderKey()
}
}
}
|
Groovy 言語の高度な構文と機能 (「参考文献」を参照) によって、コードがはるかに読みやすくなります。特に Composed Method と SLAP という補完し合う手法と組み合わせた場合には、なおさらのことです。
今回の記事では、コードの設計と読みやすさに関する 2 つの重要なパターンについて説明しました。貧弱な設計の既存のコードを扱う際の第 1 のステップは、その既存のコードを変形させ、作業対象となりうるものにすることです。設計や再利用の観点から見ると、300 行もあるメソッドには使い道がありません。そうしたメソッドでは、構成要素となりうる重要部分に焦点を絞ることができないからです。そうしたメソッドをリファクタリングし、アトミックな部分に分解すると、そこにどのような資産があるのかを理解しやすくなります。そうした資産を明確に理解できれば、再利用可能な部分を集めてイディオムのような設計原則を適用することができます。
次回の記事では、Composed Method と SLAP の概念を基に、設計のためのリファクタリングについて説明します。その中で、コード・ベースの中に既に潜んでいる設計を発見する方法についても説明します。
学ぶために
- 『Smalltalk ベストプラクティス・パターン―シンプル・デザインへの宝石集』(Kent Beck 著、2003年、ピアソンエデュケーション刊) を読み、Composed Method パターンについて学んでください。
- 『プロダクティブ・プログラマ - プログラマのための生産性向上術』(Neal Ford 著、2009年、オライリー・ジャパン刊) を読んでください。この本の第 2 章には、Composed Method と SLAP の両方について、さらに詳しい例が紹介されています。
- 『オブジェクト指向における再利用のためのデザインパターン』(Erich Gamma らの共著、1999年、ソフトバンククリエイティブ刊) は Command パターンを含めたデザイン・パターンに関する古典作です。
- Spring フレームワークは、実際のコードから収集したフレームワークの好例です。
- 新たに復活した「実用的な Groovy」シリーズの「Java プログラマーのための DSL としての Groovy」(Scott Davis 著、developerWorks、2009年2月) を読んでください。Groovy の高度な構文によって読みやすいコードを作成できること (そしてコード量が少なくなること) が解説されています。
- technology bookstore には、この記事や他の技術的な話題に関する本が豊富に取り揃えられています。
- developerWorks の Java technology ゾーンには Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
議論するために
- developerWorks blogs から developerWorks のコミュニティーに加わってください。
Neal Ford は世界的な IT コンサルティング企業である ThoughtWorks のソフトウェア・アーキテクトであり、Meme Wrangler でもあります。また彼は、アプリケーション、教育資料、雑誌記事、コースウェア、ビデオや DVD によるプレゼンテーションなどの設計と開発も行っています。さまざまな技術に関する本の著者、編集者でもあり、最新の著書は『プロダクティブ・プログラマ - プログラマのための生産性向上術』です。彼は大規模なエンタープライズ・アプリケーションの設計や構築を専門にしています。また彼は世界各地で開催される開発者会議での講演者としても国際的に有名です。彼の Web サイトをご覧ください。