Scala で ReadWriteLock にローンパターンを使う
ReadWriteLock は確実にロックを解除しないといけません。
readWriteLock.writeLock().lock(); try { // 何かの処理 } finally { readWriteLock.writeLock().unlock(); }
いけませんが、よく忘れます。unlock()
がコピペの魔術かなにかで lock()
になったままとか。writeLock()
をロックして readLock()
をロック解除とか。
try-finally 句を使ってロックは確実にロック解除させるべきですが、ロックをかけるだけでコードが助長になります。
確実にリソースを閉じるときなど、ローンパターンを用いた using (with) 句がとても便利です。ということで、Scala で ReadWriteLock をするとき(あまり無いんじゃないかという話もありますが)、ローンパターンでロックを管理させるための、構文です。
object LoanPatternLock { import java.util.concurrent.locks.{Lock, ReadWriteLock} def lockWith[A <% Lock, B](l: A)(e: => B) = { l.lock() try e finally l.unlock() } def readLockWith[A <% ReadWriteLock, B](l: A)(e: => B) = lockWith(l.readLock)(e) def writeLockWith[A <% ReadWriteLock, B](l: A)(e: => B) = lockWith(l.writeLock)(e) }
使い方
import LoanPatternLock._ val lock = new java.util.concurrent.locks.ReentrantReadWriteLock val a = readLockWith(lock) { // どんな処理でもどんとこい }
オブジェクトで書いていますが、こういうパターンは自分の使っているエディタのコピペテンプレートに入れ、使いたいメソッドやクラスに貼付けることになります。
Glazed Lists を使っていて、ロックミスで油断しているとたまーにハマって悲しいのですが、ロックのバグはどこで起きたか、何が原因なのかが本当にわかりづらいです。個人用 PC でもマルチコアは普通な時代なので、デスクトップアプリケーション開発での平行処理はもはや必須です。なのできっとまたそのうち泣きを見ることになると思います。南無。
Eclipse と sbt を同時に使うとメモリを食い過ぎて我慢できなかったので
メモリを購入。
Scala 開発に使ってるビルドツールの sbt はメモリがそこそこ必要になります。
Java も平行してコーディングするので Eclipse も同時に起動しているのですが、Eclipse 64bit Mac 版は、Eclipse の SWT がやたらめったらメモリーリークします。ウインドウウィジェットのメモリを解放してくれないようなのですが、ツールチップやサジェストもウインドウウィジェットなので、このリーク具合は止めどありません。
といういきさつで、メモリを購入。Mac Pro の Early 2008 を使っているので、高価なFB-DIMMを載せなくてはなりません。なんてこった。
Memory America から輸入しました。円高も効いて送料込みでも安くていい感じ。4GB × 2 です。
今まで 6 GB だったのを、1GB × 2 を抜いて、新しいのを差し込み、12GB にしました。スロットには余裕ありますが、ちょっとだけ省エネです。
メモリスワップがなくなって、sbt も Eclipse も長く使ってももっさりしなくなりました。64 bit 機はメモリを乗っけてなんぼですねー。
よく使うテスト用のモックは生成クラスを作ってしまう
要旨
ユニットテスト時に使うモックオブジェクトは、static ファクトリクラスを作ってしまう。
通常のモックオブジェクトの使い方(Mockito使用)。
public class SourceTreeModelTest { @Test public void setTreeSource() { TreeSource<String> source = mock(TreeSource.class); String rootObject = "value"; when(source.getValue()).thenReturn(rootObject); model.setTreeSource(source); assertSame("source value", source, model.getTreeSource()); } }
インターフェイスはモックで挙動を確認することが多いですが、『mock(TreeSource.class)』ほにゃららの箇所はいろんなテストケースから使用することになるので、static ファクトリクラスを作ってしまえばいいんじゃないの、的発想です。
public class TreeSourceMock { private TreeSourceMock() { } @SuppressWarnings("unchecked") public static <T> TreeSource<T> of() { TreeSource<T> source = mock(TreeSource.class); return source; } public static <T> TreeSource<T> of(T rootObject) { TreeSource<T> source = of(); when(source.getValue()).thenReturn(rootObject); return source; } }
testソース用ディレクトリ以下に配置すれば、公開することもなく、テスト専門で利用できる算段。
MIN_VALUE は最小値かもしれないし最小値ではないかもしれない
浮動小数点プリミティブ型のラッパークラスの MIN_VALUE 定数フィールド、 Float.MIN_VALUE や Double.MIN_VALUE は、その型が表現できる最小の有限値であって、最小値ではない。
整数型の極値定数
Java にてプリミティブ型は、そのラッパークラスの静的定数フィールドに最大値と最小値が定義されています。
Integer.MAX_VALUE に最大値、Integer.MIN_VALUE に最小値です。
@Test public void limitOfInteger() { assertEquals(2147483647, Integer.MAX_VALUE); assertEquals(0x7fffffff, Integer.MAX_VALUE); assertEquals(-2147483648, Integer.MIN_VALUE); assertEquals(-0x80000000, Integer.MIN_VALUE); } @Test public void limitOfLong() { assertEquals(9223372036854775807L, Long.MAX_VALUE); assertEquals(0x7fffffffffffffffL, Long.MAX_VALUE); assertEquals(-9223372036854775808L, Long.MIN_VALUE); assertEquals(-0x8000000000000000L, Long.MIN_VALUE); }
Short 、Byte も同様です。
浮動小数点型の極値定数
Float.MIN_VALUE や Double.MIN_VALUE はその型の最小振幅値、すなわちビットが1個立った分の数を表します。
@Test public void limitOfFloatingPointTypes() { assertTrue("MIN_VALUE is not a negative", 0 < Double.MIN_VALUE); assertTrue("MIN_VALUE is not a negative", 0 < Float.MIN_VALUE); assertEquals(Double.longBitsToDouble(0x1), Double.MIN_VALUE, 0); assertEquals(Float.intBitsToFloat(0x1), Float.MIN_VALUE, 0); }
float や double が表現できる最小値は、MAX_VALUEの符号をマイナスにすることで得られます。
テストで極値を扱う時に、間違わないように気をつけたいです。
おまけ
整数型の最小値の符号を転換する(もしくは絶対値を求める)と、桁があふれてもとに戻ります。
@Test public void absoluteValueOfMinimumValue() { assertEquals(Integer.MIN_VALUE, -Integer.MIN_VALUE); assertEquals(Long.MIN_VALUE, -Long.MIN_VALUE); assertEquals(0x80000000, -0x80000000); assertEquals(0x8000000000000000L, -0x8000000000000000L); }
参考:numbers - Minimum values and Double.MIN_VALUE in Java? - Stack Overflow
Javaで16進数の文字からアルファ(半透明)値を持ったColorを作成する。
アルファ値が128以上の16進数文字列(0x80000000以上)をデコードしようとすると、Integerの桁があふれてうまくデコードできないよ、という話。
Color.decode(String) による作成
基本のデコード方法。アルファ値無し。
@Test public void decodeHexString() { Color color = Color.decode("#000000"); assertEquals("no-alpha", 255, color.getAlpha()); assertEquals("red", 0, color.getRed()); assertEquals("blue", 0, color.getBlue()); assertEquals("green", 0, color.getGreen()); }
Color#decode は解釈値にアルファ値を持っていても無視されるので、通常のコンストラクタ Color(int rgba, boolean hasalpha) から Integer#decode を利用して作成。
@Test public void decodeHexStringWithAlphaDigits() { assertEquals("decode method ignores alpha digits", 255, Color.decode("#00000000").getAlpha()); assertEquals("color construction with alpha", 0, new Color(Integer.decode("#00000000").intValue(), true).getAlpha()); }
ところがどっこい
Integerは符号付き32ビット整数なので、0x80000000以上はデコードできません。
@Test public void overflowToDecodeHexString() { assertEquals("max value of integer", 127, new Color(Integer.decode("#7FFFFFFF").intValue(), true).getAlpha()); Integer result = null; try { result = Integer.decode("#80000000"); } catch (NumberFormatException expected) { assertNull("cannot format #80000000", result); return; } fail("unreachable"); }
解決法
@Test public void decodeHexStringUsingDecodeOfLong() { Color decode = new Color(Long.decode("#FFFFFFFF").intValue(), true); assertEquals("alpha", 255, decode.getAlpha()); assertEquals("red", 255, decode.getRed()); assertEquals("blue", 255, decode.getBlue()); assertEquals("green", 255, decode.getGreen()); }
補足
Long.parseLong(String, 16)を使ってもOK。
SwingアプリケーションのMVC構造のクラス設計
Swingで、モデルとビューとコントローラを分離させたアプリケーションをどう書くか。MVCパターンクラス設計とは悩ましいもので、これぞという手順をいつまでも模索してしまいます。
現在の開発の手順を、自分のベストプラクティスをまとめます。
なにから作り始めるか
作るソフトウエアによりますが、可能ならビューから作り始めることにしています。開発前に画面のラフ書きやユーザーストーリーがある時は特に、最初に画面だけの試作型を作ることで定期的にユーザビリティのテストが行える利点があります。
それ以外にも、出来上がり図があるプログラムはイメージが湧いてきて楽しいことなども理由にあります。
View
フレームやダイアログ画面のような、ひとかたまりになるだろう構成を、1つのクラスにつめます。EclipseなどでGUIエディターを使うのが一般的だと思いますが、私はゴリゴリ書いてしまいます。
public class MainView { public final JLabel itemLabel = new JLabel("Item"); public final JTextField itemField = new JTextField("Item"); public final JTable dataListTable = new JTable(); // 続く...
使用するウィジェット(Swingコンポーネント)が変わるような複雑性の高い設計は、コントローラの設計も複雑にさせるので、finalフィールドで定義します。Java Beansになることもないのでpublicにします。もちろんアクセサメソッドを作ってもいいです。
親コンポーネントを一つ用意します(上のcontainer)。ここへ他のコンポーネントを配置します。JPanelやJFrameを継承してレイアウトする方法もありますが、親コンポーネントを隠蔽(カプセル化)できないデメリットがあるため、継承より内包構成を選びます。
// ...続き private final JPanel container = createContainer(itemLabel, itemField); private static JPanel createContainer(JLabel itemLabel, JTextField itemField) { JPanel panel = new JPanel(); panel.add(itemLabel); // その他のレイアウト return panel; } public JComponent getContainer() { return container; } }
親コンポーネントはJPanelであるときがほとんどですが、たまにJScrollPaneのような他のコンポーネントにもなり得ますので、親コンポーネント用のアクセサメソッド(getContainer)を作成します。
レイアウトは、レイアウト用メソッドを作ってイニシャライザ内で行ってしまいます。
レイアウト用メソッドはstatic宣言し、引数にコンテナー内で使用するウィジェットをとることにすると、うっかり別のウィジェットを配置してしまった、などレイアウトミスを減らすことができます。
レイアウトはすぐ確認したいので、画面を表示するmainメソッドを作成します。
public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { JFrame frame = new JFrame("MainViewTest"); MainView mainView = new MainView(); frame.setContentPane(mainView.getContentPane()); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } }); }
ビュークラスにこのmainメソッドを書いてしまってもかまいませんが、運用上必要ないメソッドを作るのはおもしろくないので、Testクラス(MainViewTestなど)にmainメソッドを書きます。
ビュークラスはウィジェットの配置とデザインがほとんどであるため、操作や計算の処理が紛れ込んでMVCパターンが崩れる、という心配が少ない領域です。
Model
データ処理は全てモデルとします。いくつかの要素(例えば名前や年齢や住所など)を持つ情報の固まりクラスや、入力から出力を得る計算クラスはモデルに該当します。ResourceBundleからデータを取得したり、ネットワークから取得するような重い処理をするクラスもモデルとして書きます。ただし重い処理のモデルは、SwingWorkerのような非同期実行クラスから使うことになります。
MVC構造には上の処理モデルクラスで得た情報をを表現するためのモデルが必要になります。Swingコンポーネントの専用のモデル(TableModelやListSelectionModel)を拡張や内包することになります。
タイトルや色や表示文字列の変更など少し凝ったアプリケーションを作るときは、これら表示の情報を納めたモデルを作成することになります。
目安として一つのビューに一つの表示モデルクラスを作ります。Java Beansの命名規則に沿って作ります。
class MainViewModel { private String title = "title"; private boolean fieldEditable = true; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public boolean isFieldEditable() { return fieldEditable; } public void setFieldEditable(boolean fieldEditable) { this.fieldEditable = fieldEditable; } // 続く...
表示に関与するモデルは、値の変化をコンポーネントに通知するために変化イベントの発行を実装することになります。ほとんどの場合PropertyChangeEventのイベント通知機構を実装することになります。
イベントの追加や発行の処理はPropertyChangeSupportを使って処理を委譲できます。IDEの委譲メソッド作成機能を利用するとより簡単です。
// ...続き private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener listener) { pcs.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { pcs.removePropertyChangeListener(listener); } protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { pcs.firePropertyChange(propertyName, oldValue, newValue); } }
モデルの変化をビューに伝えるのはコントローラの役目です。モデルがビューを保持しないように気をつけて設計します。
Controller
コントローラクラスには大きく分けて二つの役割があります。
1つめは、ボタンを押したときの反応、テーブルのセルを選択、などユーザーの入力の反応処理をうけとり、モデルに処理を伝える役割です。モデル−コントローラと呼ばれます。
2つめは、モデルやモデル−コントローラの値の変化をウィジェットに適用する役割で、ビュー−コントローラと呼ばれます。
モデル−コントローラは、多くの場合リスナーインターフェイスを実装する設計になります。
例えば「テーブルで選択した行の識別子をモデルに適用する」という処理は次のようになります。
class TableRowSelectionHandler implements ListSelectionListener { private final List<String> tableRowIdentifierList; private final CustomDataModel targetModel; public TableRowSelectionHandler(CustomDataModel targetModel, List<String> tableRowIdentifierList) { this.targetModel = targetModel; this.tableRowIdentifierList = tableRowIdentifierList; } @Override public void valueChanged(ListSelectionEvent e) { int index = ((ListSelectionModel) e.getSource()).getAnchorSelectionIndex(); String identifier = tableRowIdentifierList.get(index); setIdentifier(identifier); } void setIdentifier(String identifier) { targetModel.setIdentifier(identifier); } }
モデルを介さないときはこのコントローラクラスに値変化通知の実装をします。
void setIdentifier(String identifier) { firePropertyChange("identifier", this.identifier, this.identifier = identifier); } protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { // イベント通知の実装... }
ボタンを押してモデルを変化させる、という処理もActionインターフェイス(AbstractAction抽象クラス)を実装したモデル−コントローラとして作ります。
ユーザーからの操作の応答をとりまとめるクラスもまたコントローラクラスになります。操作に応答してモデルを変更することになるので、これも同じくモデル−コントローラとなります。
class MainViewController { private MainViewModel model; /** 内包したコントローラオブジェクト1 */ private final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { modelPropertyChange((MainViewModel) e.getSource(), e.getPropertyName()); } }; /** 内包したコントローラオブジェクト2 */ private final TableRowSelectionHandler handler = new TableRowSelectionHandler(model, identifierList); /** 内包したコントローラオブジェクト3 */ private final Action updateAction = new AbstractAction("Update") { @Override public void actionPerformed(ActionEvent e) { model.update(); } }; /** @param model 操作されるモデル */ public MainViewController(MainViewModel model) { this.model = model; } // その他の実装...
ボタンにアクションを設定したり、リストの変化を監視する処理を行うための結合メソッドを作成します。
public void bindUpdateButton(JButton button) { button.setAction(updateAction); } public void bindListSelection(JList list) { list.addListSelectionListener(handler); } // 実装の続き...
親ビューから一気に結合してしまうメソッドも作成してしまいます。
>|java|
public void bindMainView(MainView view) {
bindUpdateButton(view.updateButton);
bindListSelection(view.favoritesList);
}
|
ビュー−コントローラは、ビューを保持します。あるプロパティ値をフレームのタイトルに表示するというときはビュー−コントローラが処理することになります。たとえばidentifier値をフレームのタイトルに表示するクラスは次のような実装になります。
class FrameTitleController implements PropertyChangeListener { private final JFrame frame; public FrameTitleController(JFrame frame) { this.frame = frame; } @Override public void propertyChange(PropertyChangeEvent e) { if ("identifier".equals(e.getPropertyName())) { frame.setTitle(String.valueOf(e.getNewValue())); } } }
この時、監視するプロパティは"identifier"とハードコーディングしていますが、これを引数で指定できるようにすると、より再利用性が高まります。
class FrameTitleController implements PropertyChangeListener { private final JFrame frame; private final String propertyName; /** * @param propertyName 対応するプロパティ名。{@code null} の時はすべてのプロパティの変化に対応。 */ public FrameTitleController(JFrame frame, String propertyName) { this.frame = frame; this.propertyName = propertyName; } @Override public void propertyChange(PropertyChangeEvent e) { if (propertyName == null || propertyName.equals(e.getPropertyName())) { frame.setTitle(String.valueOf(e.getNewValue())); } } }
ビュー−コントローラの設計のとき、保持するビューは一つに限定して考えると、結合性を弱くなり、テストもしやすいクラスになります。
このコントローラクラスを利用する親クラスは、このオブジェクトをインスタンス化し、リスナー登録メソッド(addPropertyChangeListenerなど)を用いて追加します。
コントローラが別のコントローラに値を伝達させたいときがあります。これには二つの方法があり、コントローラが内包するモデルを介して、別のコントローラにこのモデルの変化を監視させるか、先に出てきたTableRowSelectionHandlerにあるような値変化通知をコントローラ自身が実装し、その変化を別のコントローラに監視させることになります。
ところでリスナーインターフェイスを実装するコントローラクラスは上記にあるように変化通知メソッド(#valueChanged(ListSelectionEvent) や#propertyChange(PropertyChangeEvent))が公開されることになります。カプセル化を強めるために、もう一工夫必要になります。
FrameTitleControllerはTableRowSelectionHandlerへリスナーの登録することを期待しているのでメソッドを作成します。
public void listenTo(TableRowSelectionHandler target) { target.addPropertyChangeListener(this); }
リスナー登録する対象をTableRowSelectionHandler以外にもするためには、同じメソッドオーバーロードします。リフレクションを使って全てのオブジェクトを対象にすることもできますが、通常の設計ではオーバーロードで十分です。
リスナー登録メソッドがあるため、もはやコントローラクラスがPropertyChangeListenerを実装しなくてもよくなります。リスナーをこれを匿名クラスとしてフィールドへ移します。
class FrameTitleController { private final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { processPropertyChange((TableRowSelectionHandler) e.getSource(), e.getPropertyName()); } }; protected void processPropertyChange(TableRowSelectionHandler source, String propertyName) { // 処理... } public void listenTo(TableRowSelectionHandler target) { target.addPropertyChangeListener(propertyChangeListener); } // その他の実装... }
コード量は増えてしまいますが、メソッドのおかげでこのクラスの利用方法がよりはっきりし、またリスナー登録される型が定まるので、キャスト例外の心配がなくなります。
コントローラの値変化の監視先は通常一つになりますので、完全に一つに限定させると、強い設計になります。
class FrameTitleController { // ...その他の実装のあと private TableRowSelectionHandler target; public void setListeningTarget(TableRowSelectionHandler newTarget) { if (target != null) { deafTo(target); } target = newTarget; if (newTarget != null) { listenTo(newTarget); } } void listenTo(TableRowSelectionHandler target) { target.addPropertyChangeListener(propertyChangeListener); } void deafTo(TableRowSelectionHandler target) { target.removePropertyChangeListener(propertyChangeListener); } // さらにその他の実装... }
実行
アプリケーションの実行は、
モデルの構築 -> コントローラの構築 -> ビューの構築 -> 結合
という流れになります。
public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { start(); } }); } void start() { MainViewModel model = new MainViewModel(); MainViewController controller = new MainViewController(model); MainView mainView = new MainView(); controller.bindMainView(mainView); JFrame frame = new JFrame("Application"); frame.setContentPane(mainView.getContainer()); frame.setContentPane(mainView.getContentPane()); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); }