2015年12月13日日曜日

SQuBOKとSwift

イチョウの紅葉がきれいな季節になりました。東京は例年だと11月下旬が見頃ですが、今年はおそいですね。先日、丸の内に観に行ったのですが、まだ全盛ではないものの、とてもきれいでした。

さて、SQuBOKアドベントカレンダーということで、今回はSQuBOKとSwiftについて書いてみたいと思います。かなりこじ付け感がありますが、うまくつながるように書こうと思います。

SQuBOKでは、「3.6.2.3 設計原則」として、「理解しやすく、変更や拡張さらにや再利用しやすいものとなっていることは、良い設計の要素である。」としています。そしてその方法がいくつか紹介されています。そこで、紹介された設計原則を用いて、Swiftで作ったソースコードをリファクタリングしてみます。

たとえば、こんなコードがあったとします。

//配列に含まれる大貧民で禁止上がりのカードを表示する
var array = [2,3,8]                                      // 配列に適当な数字を入れる
for var i = 0 ; i < array.count ; i++ {             // 配列のサイズ分ループする
    if array[i++] <= 2 {                                  // 1,2だったら禁止上がり
        print(array[i])                                      // 表示する
    }
}

危険な感じがしますね。実際にこのコードは落ちます。i++が2箇所にあり、範囲外の要素を参照するためです。

同じ変数への代入は1ヶ所にまとめたいものです。コード内に何ヶ所にも散りばめられていると、あとでとても読みづらくなります。また、ここで意図しているのは「禁止上がりのカードを表示する」ことですが、禁止上がりの判定方法が「<= 2」で表現されており、読んで理解しないと意図がわかりません。また、コードに配列のインデックスの処理が混在していることも読みづらくしている原因です。そこで今回は、設計原則の「インターフェースと実現の分離」でこのコードを直してみたいと思います。

なお、リファクタリングにあたっての参考としてBertrand Meyerの「オブジェクト志向入門第2版 原則・コンセンプト」の「4.6.5 共通する振る舞いのふるい出し」にあるコードも参考にしました。この本はSQuBOKの「3.7.1.3 契約による設計」でも引用されています。

※ここから先は擬似コードなので、実際には動作しません。また、事前にContainerクラスやCardクラスを用意しておく必要があります

//コンテナに含まれる大貧民で禁止上がりのカードを表示する
var container = Container(Card(2),Card(3),Card(8)) // データをセットする
n = container.setFirst()                                           // 最初の要素を取り出す
while( !container.isEnd() )                                      // 要素が終わるまで繰り返す
    if n.isNgCard()  {                                                // 禁止カードかどうかを判定する
        print(n)                                                           // 表示する
    }
n = container.next()                                              // 次の要素を取り出す
}

このように改善することで、配列や禁止カードの判定など下位レベルの問題が分離され、インターフェースを通して呼び出しているため、変更や再利用がしやすくなります。

例えば、改善前のコードでは配列にデータを入れていましたが、setFirest(),isEnd(),next()というインターフェースを持っていれば、データの格納先は配列でもファイルでもデータベースでもよくなります。next()のことをイテレータと呼びますが、これは「次を取り出すこと」を抽象化しているわけです。

また、禁止カードのルールが変わったとしてもisNgCard()を直せばよく、このコード自体は変更する必要がなくなります。そして、下位レベルの問題の実現手段がなくなり、意図がそのままコードに書かれているので読みやすくなります。

Meyerはこの意図にあたるものを「仕様」「何を(What)」、実現にあたるものを「実装」「どのように(How)」として区別しています。契約によるプログラミングでは、仕様と実装をそれぞれコード中に記載します。そして、実装したコードの結果を、仕様を記述したコードが検査することで信頼性の高いソフトウェアを作ることを目指しています。

ところで、改善版のコードも少し危険な箇所があります。
container.next()
というメソッド(イテレータ)は、呼ばれた時の状態によって返却値が変わります。つまり副作用を持つのです。このため、呼ぶタイミングを間違えるとバグの原因になります。

関数型プログラミング言語のHaskellでは、副作用のないプログラムが作れます。このため、関数は同じ入力なら同じ出力を返します。というのも、Haskellは変数の再代入はできず、ループ制御文もありません。ループ的な処理をしたい時には、再帰呼び出しを使います。
例えば、10の階乗を出すプログラムなら、

bar x = if x > 1 then x * bar ( x - 1 ) else 1     -- 関数の宣言
main = do                                                    -- ここから開始
 print $ bar 10

となります。ここで、関数barは内部状態を持たない固定的な式ですが、再起呼び出しをすることで与えられる入力値が変化していくので、それに応じて返す値も変化し、結果として10*9*8..*1という処理を実現しています。まさに関数といった感じですね。もちろん他の言語も関数という機能はありますし、このような書き方は他の言語でもできますが、Haskellの場合は最初から「本当の関数」を目指した書き方をしているところが「関数型」たるゆえんなのでしょう。

話を戻しましょう。先ほどのコードは与えられたすべてのデータから、大貧民における禁止上がりのカードを表示したいだけなのですから、first()やらnext()といったことも、下位の問題といえます。さらに意図に近いコードを書くため、Swiftの高階関数(関数を引数として渡せる)を用いて直してみます。

//コンテナに含まれる大貧民で禁止上がりのカードを表示する
var container = Container(Card(2),Card(3),Card(8))     // データをセットする
print( container.filter { n in n.isNgCard() } )                  // 禁止カードを表示する

とても簡単になりました。{}の中はクロージャと呼ばれるもので、処理そのものをfilterメソッドの引数として渡します。普通の引数っぽくありませんが、最後の引数だけ外に書けます。filterはオブジェクト(プログラム上ではcontainter)のうち、クロージャの結果が真になる要素を配列にして返します。もちろんfilterメソッドを作る必要がありますが、Swiftの配列なら標準で実装されています。処理を1つ1つ説明すると以下のようになります。
・filterはcontainerの要素を1つずつクロージャに渡す
・クロージャはその要素をオブジェクト(プログラム上ではn)として受け取り、isNgCard()を実行する。その戻り値はfilterに渡る
・filterはクロージャの結果が真の場合だけその要素を配列にコレクションする
・すべての要素が検査し終わると、filterは配列を戻り値としてprintに返す
・printはその配列の値をすべて表示する

このようにすると、イテレータの状態にも左右されないし、first()やnext()といった詳細も下位に隠蔽することができました。また、オブジェクト間のインターフェースを減らす、つまりオブジェクト間の通信を減らすことで、オブジェクト間の結合度も下げることができました。これによって、データの格納先は、「最初から順番に取り出すこと」という制約から解放され、ランダムに取り出そうが、数式に置き換えようが、filter()を通してすべての要素にアクセスさえできればよくなります。

終わりに

ということで、SwiftからSQuBOK、Haskellと書いてみました。後から気付きましたが、リファクタリングについてはSQuBOKの「3.7.1.2 リファクタリング」扱っていました。様々な技術がちょっとずつつながって、やっぱりSQuBOKに戻れるあたりが面白いですね。Meyerの本は読破会で話題になったこともあって購入してみましたが、辞書に匹敵する厚さの本でした。それでもひるまなくなったのは、SQuBOKを読み続けているおかげなのかもしれません。

さて、Advent Carenderも中盤にさしかかってきました。
次回はKazuCocoaさんです!

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。