ClassクラスのClassオブジェクト

具体についてのクラスはインスタンスと同じもの? - 超ウィザード級ハッカーのたのしみ

クラスって何なのだろうと疑問に思い始めた。

さて、Javaにはjava.lang.Classクラスという変なクラスがある。Classクラスのオブジェクトはクラスそのものではないみたい。

面白いのは、ClassクラスのClassオブジェクトもあることだ。

Class<?> class2 = Class.class;

<?>でなしに、<Class>と明記することもできる。

Class<Class> class3 = Class.class;
      ^^^^^

そうすると<Class>がWarningとなる。何のクラスのClassクラスか分からないからだ。

しかし、

Class<Class<?>> class4 = Class.class;
Class<Class<?>> class5 = Class<?>.class;
Class<Class<?>> class6 = Class<Class>.class;

これらはコンパイルエラーになってしまう。

だから何という話だけれども、自己言及はパラドックスを伴うこともあるので、Javaの言語仕様はパラドックスでもなんでもないが、表現しきれないという事実がちょっと面白いなと思った。

具体についてのクラスはインスタンスと同じもの?

特定の具体を示しているクラスがあったとして、それはインスタンスと同じものと言えるのか?

ぼんやりとした疑問で答えはまだ出ていない。

f:id:fjkz:20160327182341p:plain

例えば、DogクラスとPetDogクラスがあったとする。PetDogクラスは、Dogクラスを継承していて、Dogクラスの中でも人に飼われているといるものという特殊なオブジェクトを示している。Dogクラスの示す範囲を限定していて、具体化してるといっても良い。

さて、PetDogクラスに属するjhon, hachi, pochiというDogがいたとする。プログラム上はこれらはインスタンスだ。仮にインスタンスでなくて、jhon, hachi, pochiといった特定のPetDogの特徴だけを備えたJhon, Hachi, Pochiというクラスを作ったら、これらのクラスが示すものはjhon, hachi, pochiと同じものなのだろうか?

もちろんJavaなりC++の上では別ものになる。クラスはインスタンス化しないと使えないので、インスタンスとクラスは別ものという風にできている。インスタンス化というものがなくて、具体的なクラスを作るという操作が、インスタンス化することと等価になるようなプログラミング言語は存在しうるのかということである。

無理な気がしている。Jhonクラスに属するものがjhonだけであっても、Jhonクラスとjhonは別のものに思える。でも、どうしてかがわからない。具体についてのクラスと具体は何が違うのだろう?

単に私がクラスとか型とか集合という概念を分かっていないだけ……?

オブジェクト間の関係

UMLについて調べています。思考の整理ツールとしての表記法(diagramming)には興味があります。

UMLは書きにくいし、曖昧さも強いし、目的が良くわからない絵が多いので、あまり好きにはなれないです。規格としては不完全なように思うので、独自に改変したり拡張したり制限しないと業務には使いにくそうです(Microsoft Officeで書けないし)。ちゃんと使ったことないし、世間を知らないので分からないけれども、世の中ではこれがそのまま使われているでしょうか?効率が悪そう……

さて、UMLではクラス図が最も基本的な図です。クラス図はクラスの関係を記述します。クラス図って、IDEで見れるアウトライン以上の情報を含んでいないので、こんなものは無意味だと昔は思っていました。しかしながら、クラス図に矢印で示されるオブジェクトの関係は、曖昧さを含むけれども、コードに明に現れなくて、かつ重要な概念を含んでいると思ったのでまとめようかと思います。*1

クラスの関係には以下の5種があります。

  • uses-a
  • is-a
  • has-a
  • owns-a
  • is-composed-of-a

名前は私が付けました。is-a関係とそれ以下の順序は微妙ですが、イメージとしては上から下にいくほどオブジェクト間の関係が強くなります。それぞれについて記します。

uses-a 関係 (Dependency)

f:id:fjkz:20160326204547p:plain

AクラスがBクラスに依存しているという関係 (Dependency)です。AクラスのコードにBクラスが現れているとき、AクラスはBクラスに依存していることになります。Javaで他のパッケージであれば、importと記述しなければならないクラスです。コンパイル時・実行時にCLASSPATHが通っていなければなりません。

継承している場合やフィールド値に持っている場合は以下のもっと強い関係となるので、ここには含みません。オブジェクトへの参照がスタック上にのみある場合がこの関係になります。

UMLでは点線の矢印で表します。依存している方からされている方に矢印が向きます。例の図は、Dogクラスがgrowlメソッドの返り値としてGrowlクラスを返す場合を想定しています。

A dog uses a growl.

なので(言葉としては違いますが)、uses-aとつけています。当然、growlメソッドを呼ぶオブジェクトが返り値を使用する場合はそのオブジェクトもGrowlクラスに依存することになります。

この関係を全て図にしたら収拾つかなくなりそうだし、依存関係を図示したところで何がうれしいのかは不明です。

is-a 関係 (Generalization)

f:id:fjkz:20160326211134p:plain

継承の関係です。特に言うまでもないです。Javaだと代入できる関係、もしくはinstanceoftrueになる関係です。

UMLだと矢印の先が白抜きになります。<<interface>>とわざわざ書いているし、区別する意義が不明なのだけれども、実装の場合は点線を使います。

has-a 関係 (Association)

has-a, owns-a, is-composed-of-aは全て基本的にはhas-a関係です。コードを見ても明には現れません。異なる実装になるかもしれないし、結果として同じ実装になることもあるかと思います。いずれの関係かは設計するときの気持ちで決まります。*2

f:id:fjkz:20160326215759p:plain

has-aというはフィールド値にオブジェクトの参照ポインタを持っているという意味です。

参照ですので、同じオブジェクトを複数のオブジェクトがhasしていても構いません。したがって、所有しているのとは意味が異なります。AクラスからBクラスを覚えている、AクラスからBクラスをたどることができるといった意味なので、knows-aとかcan-seeとかcan-touchとかの方が意味的には適当な気がします。

UMLだと開いた矢印で表します。逆向きに辿れないことを明に示すために矢印の根本に×をつけることもある。両方辿れる場合は矢尻がないこともあります。

例だとDogクラスは帰る家としてHouseクラスを持っています。矢印の上に"go back ▶"とか文脈を入れたりすると親切です。Houseオブジェクトは別のHouseオブジェクトに変わるかもしれないし、Houseオブジェクトは別のDogと共有されるかもしれないし、別のクラス(例えばOwner)と関連しているかもしれません。

owns-a 関係 (Aggregation)

f:id:fjkz:20160326233256p:plain

has-aより強い関連として、Aggregation (集約) という関係がある。集約の意味するところは不明です。オブジェクトが別のオブジェクトを所有しているという関係です。

オブジェクトを階層構造に並べた時に、Aggregation関係にあるオブジェクトは配下にあって、ただ関連してるだけのオブジェクトは横にならんでいて参照を持っているだけというイメージになるでしょうか?

UMLでは矢印の根本を◇にします。

例はちょっとイマイチというか、文脈の中でしか意味をなさないので、適当かどうかこれだけでは分からない。DogがCollarを所有しているという意味です。

is-composed-of-a 関係 (Composition)

f:id:fjkz:20160327000241p:plain

さらに強い関連として、Composition関係があります。全体と部分の関係です。切り分けていくと現れるということです。

part-ofのが自然ですが、向きが逆になってしまうので、is-composed-of-aとしています。

部分は常に1つのオブジェクトをComposeしています。つまり、2つ以上のオブジェクトが同じオブジェクトをComposition関係で配下に共有していることはありません。

全体と部分のオブジェクトは寿命はほとんど同じです。したがって、全体のクラスの、コンストラクタか初期化ルーチンで部分を生成して、途中で付け替えたりとかはしません。

Aggregation (集約) との区別は微妙です。設計者の気持ち的に、別に一個のクラスでもいいけど分けたよという意図が感じられればいいのかと思う。それぐらい関連が強いということです。

クラスが巨大になりすぎるから小さいクラスの集合に分けるときとかに使うのだと思う。しかし、あまりやり過ぎるとfan-out、つまり依存元の数が少なく依存先の数が多い設計となってしまい、これは凝集度が低い良くない設計と言われています。

UMLでは矢印の根本を◆にします。

参考

入門 UML 2.0

入門 UML 2.0

*1:私の理解を書いているので、UMLの説明としては正しくないと思います。UML自体の詳細は例えば参考に挙げた文献を当たってください。

*2:個人的にはこれらを区別できないことがオブジェクト指向という手法の問題だと思っているのですが、それについてはまだ考えがまとまっていません。

アーキテクチャは疎↔密を繰り返す

歴史は繰り返しでアナロジーがあるから面白い。ソフトウェアのアーキテクチャの流行りを調べてみると同じようなことを言葉を変えて繰り返している。切り口はいろいろあると思うが、今日はアーキテクチャという切り口から見てみる。

ソフトウェアのアーキテクチャコンポーネントとかモジュールが疎結合になったり密結合になったりを繰り返している。

一般にソフトウェアの部品は疎結合な方が保守性が高い。理解しやすいし、テストもしやすいから品質も高い。再利用性も高いので、生産性も高まる。それに、美しい。人間の頭に合わせて行くと、ソフトウェアは疎な方向に向かっていく。小さく分割されて整理されていなければ人間は理解できない。ソフトウェアは保守にとにかくコストがかかるということは経験として知られている。ソフトウェアを人間が理解できなければ保守できないが、人間の理解というものは非常にコストがかかることだ。コストを抑えたいという考えで、ソフトウェアは疎結合となっていく。また、これは私の考えだが、頭の良い人というのは難しいことを理解できるのではなくて、分割して整理して理解できる形にするのが上手いのである。だから、頭の良い人ほど疎結合にしたがるように思う。

しかしながら、部品同士の結合度が弱くしていくと性能が悪くなっていく傾向がある。部品同士の通信に計算コストがかかるからである。部品に綺麗に分かれていなくて、ひとつの塊になっているようなモノリシックといわれる形体の方が性能は良い。巨大な塊で触れないけど性能は良いコードってないですか。また、最初は綺麗に分かれていたけれども、チューニングのための最適化をしていくと部品がくっついて密結合になっていくということもある。

最初は小さい塊だったのが、どんどん巨大な塊になっていき、人間の限界を超える。そのため、分割したいという考えが出てきて、部品が小さくなる。そのうち、部品が巨大化していったり、部品同士がくっついていったりして、再び限界を超える。そして、また分割する。ソフトウェアのアーキテクチャはこの繰り返しだ。


OSがないころはプログラムは完全にモノリシックだった。ハードウェアを動かす部分とやりたい計算の部分は分けたほうが便利だということでOSができる。

やりたい計算も部品に分かれていないと整理しきれないということで、構造化プログラミングが発案される。それでも、足りないとオブジェクト指向プログラミングが発明される。

OSの方も部品化されてた方が良いとマイクロカーネルという考えが出てきたが、思ったほど部品化の効果がなかったようで、結局モノリシックなLinuxがもっとも普及している。しかし、Linuxカーネルモジュールという部品に分かれていたりする。

UNIXの哲学は疎結合を良しとする考え方の代表だろう。小さいプログラムを作って、シェルスクリプトとプレーンテキストでゆるく結合させようという考えだ。しかし、一部のPOSIXシェルはechoといったよく使コマンドは組み込みコマンドにしてして効率化をはかったりしている。また、シェルスクリプト自体が巨大化して手に終えなくなったりというのもよくある話だ。

マルチプロセスの方がコンポーネント疎結合だけれども、同期が大変だとかプロセスの生成コストが大きいとかで、メモリ空間を共通にしているという意味で密なマルチスレッドになって、でもスレッド同士は疎な方が良いよねってことでGoみたいになったりとよう分からん。

あるいは、最近ではシステムが巨大してしまったので、マイクロサービスなどといって小さく切り分けるのが流行っている。


疎結合・密結合は、どちらが良いとも言えず、いずれを選択されるかは設計思想の問題で、その選択はソフトウェアの性質やその時々の計算機の性能や他のソフトウェアの状況や流行りで決まってくる。

それでも、アーキテクトが必要になるような大きな設計は除いて、ちょっとした設計ぐらいの小さい話なら極力疎結合にしようとした方がよいと思う。関数呼び出しを一回削って節約できるユーザーの時間と、それによって発生するテストや保守の時間を比べたら、きっとほとんどの場合後者の方が大きい。それに、放っておいたらすぐにモジュールの結合度は高くなっていく。疎結合にしようと踏ん張っても、塊になるのを少しゆるやかにするのがいいところだ。疎なものを密にするのは簡単だが、密なものを疎にするのは不可能だ。密結合とスパゲッティは紙一重である。ソフトウェアを延命させたかったら、疎にした方がよい。ソフトウェア危機は計算機が発明されて以来ずっと続いているのだから。

Spockを試す

JavaでBehavior Driven Developmentをするためのフレームワーク、つまり、機械に読める仕様書を書くためのフレームワークの1つであるSpockをeclipse上で試す。ついでにGradleも試す。

github.com

Eclipseの設定

SpockはGroovyを使うので、EclipseでGroovyを使えるように設定します。新しめのEclipse用のプラグインは公式のマーケットプレイスにないので、

Home · groovy/groovy-eclipse Wiki · GitHub

にあるURL(Eclipse Marsの場合はhttp://dist.springsource.org/snapshot/GRECLIPSE/e4.5/)からプラグインをインストールする。

Help -> Install New Software

でwork with:にgroovy-eclipseリポジトリのURLを追加して、Groovy-Eclipseプラグインをインストールする。

Gradleプロジェクトの作成

Gradleでプロジェクトを作ってみる。

File -> New -> Gradle Project

で言われるがままに設定していけば出来上がる。

build.gradleを以下のように設定する。*1

apply plugin: 'java'
apply plugin: 'groovy'

repositories {
  jcenter()
}

dependencies {
  compile 'org.codehaus.groovy:groovy-all:2.3.11'
  testCompile 'org.spockframework:spock-core:1.0-groovy-2.3'
}

Mavenのpom.xmlより設定ファイルがシンプルで良いですね。

プロジェクトを右クリック -> Gradle -> Refresh Gradle Project

で設定が読み込まれるよう。

テスト対象

テスト対象のJavaのクラスは以下。

package org.example.spocktest;

public class Calculator {
  public int add(int a, int b) {
    return a + b;
  }
}

テストの記述

src/test/groovyというディレクトリを作成して、src/test/groovyをSource Pathに加える。

GroovyでCalculatorクラスの仕様を記述する。例えば以下のような仕様が書ける。

package org.example.spocktest

import spock.lang.Specification
import spock.lang.Unroll

class CalculatorSpec extends Specification {

  @Unroll
  def add() {
    given:
      def calc = new Calculator()

    when:
      def result = calc.add(a, b)

    then:
      result == expected

    where:
      a  | b  | expected
      1  | 1  | 2
      0  | 0  | 0
      1  | 0  | 1
      0  | 1  | 1
  }
}

JUnitと同じようにテスト出来る。whereで指定する組み合わせは、PICTを用いて機械的に生成したい。

PICTを試す - 超ウィザード級ハッカーのたのしみ

感想と今後調べること

Spockは、Eclipse, Gradle, 今回は試していないがMavenといったJavaの標準的なツールとの連携もよく出来ている印象だ。 また、可読性が高い点が非常によい。

JUnitで世の中回っているので、併用する形になると思うが、Spockを使う場面とJUnitを使う場面をどう分けるかが難しいところ。

また、Coverageを算出する方法も調べたい。全てのBehaviorを記述してCoverageが100%にならない場合は、余計なコードが存在するということが分かる。

他に気になる点として、仕様変更に対して柔軟かどうかというのも検討する必要がありそうだ。

参考

魅惑的(Fascinating)なテスティングフレームワーク Spock - A Memorandum

Spockを使ってみる (導入編) - Qiita

Gradle + Spock + Eclipse環境 - Qiita

*1:意味が分かっていないので正しいかは?javaプラグインは要るのか?groovyのバージョンが2.3になってしまったので、2.3用のプラグインを使う。groovyのバージョンはどこで変える?

SQL on HadoopだったらDWHでよくね?

ちょっと前からモヤモヤしていたこと――HiveやPrestoのようなSQLHadoop上のデータを集計できるというようなものを使うのだったら、昔からあるデータウェアハウス(DWH)でよくないか?

データを扱うにはSQLが、なんやかんやで向いているということが再確認されている。ビッグデータは大量の非構造データのことだとしばし前はいわれていたが、非構造データなんてゴミデータなわけで意味のあるデータは何かしらの構造がある。データがどういう構造をもっていたら整理しやすいかというのは、長年――人類が文字をもったときから――研究されてきた。その結果人類は関係モデルというものにたどり着いた。他にもKey-ValueだとかXMLだとかデータの持ち方は色々あるけれど、やっぱり関係モデルが未だ最強のデータモデルだと私は思う。慣れている人が多いという点と扱いやすいように整理されているという点で関係モデルに帰着させるのが、最も便利だ。Hadoopで非構造データが分析できるといわれたが、分析をしたいなら整理されていなければ無理で、整理したければ関係モデルに落ち着くのだ。話題先行でアプリがないと言われていたHadoopで、やっとキラーアプリと有力視されたのは結局SQLでクエリがうてるというものだ。山ほど似たようなのが出てきている、e.g.、Cloudera Impala, Apache Drill, Presto, Hive on Tez, Spark QL。

しかし、SQLで分析って前からできた。DWHとか言われる列指向型のDBだ。例えば、TeraData、HP Vertica, IBM Netezzaなど。Hadoopが目指すものがこれらと同じなら、こっちの方がよくないか。最近のHadoopの動向を見ていると、既存のものを再発明しているようにしか見えないように思えてきた。Clouderaは、HDFS以外にKuduっていう列指向DBを作っちゃったりしていることからも、この仮説が正しいような気がする。しかし、既存のものは歴史も長くかつSQLに特化しているわけで、事例も多く使い勝手も良いと思う。Hadoopを使うというのが目的なら別だけれど、意思決定のためにデータを素早く集計したいというであれば、前からあるDWHを導入するのが正しいのではないかと思い始めた。

DWHってビビるほど高いけどね。Hadoopオープンソースだから、一騎当千のエンジニアに給料弾んで自前で構築も運用も何もかもやらせれば、安くなるのかな?OSSは本当に安いのかという語り尽くされた問題で、前記の一騎当千の将がいなくなったら崩壊するリスクはどうするだとか、有償サポートを頼んだら結局金がかかるじゃんとか、運用まで考えるとどっちとも言えなくなってくる。

今の段階であれば、DHWの方を選択するのが、ほとんどの人にとっては正解じゃないかな。

将来的には微妙だ。SQL on Hadoopも道具として完成されてくるでしょう。そのときは、DWHと同じようなものが出来上がると思う。こうなってくると、プロプライエタリソフトウェアとオープンソースソフトウェアの戦いみたいなる。

ソフトウェア産業は標準規格の奪いあいだから、みんながHadoopを使うようになったら、HortonworksやClouderaが勝つ気がする。つまり、BIツールだとかがHadoopとの親和性を高めていって、Hadoopありきの世の中になれば、Hadoopに頼らないものを作っても売れなくなり、ますますHadoopへの依存度が高まるという現象が起きる。量子力学とのアナロジーでボーズアインシュタイン凝縮と呼ばれる現象だ。

しかし、そうなるかというと私はならないと思う。Hadoopが並列処理のOSを目指しているのは想像が付くが、本当のOSほどは依存度が強くない。OSSで成功できたのはRedHatだけで、それはOSというソフトウェアの立ち位置が特殊だったからだ。Hadoop = HDFS or YARNだが、HDFSやYARNがなくてもソフトウェアは動く。Cloudera自身が、HDFS以外のデータ置き場としてKudu出しちゃったりしているし、同じClouderaのImpalaもYARNなしに動かせちゃったりする。技術的には興味深いと思うけれども、規格という点で見ると愚策ではないかなと私は思う。他にも、Kafka、Cassandra、SparkみたいなHadoopの周辺にはあるけれど、Hadoopがなくても動くものは多い。そうなると、SQL on HadoopがDWHを駆逐するのは無理そうだ。ちょっとだけDWHのパイが食われるか、パイ自体が広がるなら互いに分け合うようになるのがいいところな気がする。

なので、SQL on Hadoopを頑張って取り組む意味があんまりないような気がしてきた。

Bashでロックを取る方法

前回に続いてBashネタで、Bashで排他ロックを取る方法について。

並行プログラミングをしようとするならば、排他は必要となる。シェルスクリプトでも並行性について考慮しなければならない場面があるが、アトミックにロックを取る手段はシェルスクリプトだけでは無理で、OSの機能に頼らなければならない。

3つのやり方を見つけたので、メモしておく

  1. lockfileコマンド
  2. flockコマンド
  3. 自前のコマンド

lockfileコマンド

lockfileというコマンドがある。ただし、必ずしもどの環境にあるとは限らない。

LOCKFILE="/tmp/example.lock"
lockfile "${LOCKFILE}"

# 排他したい処理

rm -f "${LOCKFILE}"

上のようにLOCKFILEというファイルをlockfileコマンドでつくって、排他したい処理を行い、最後にLOCKFILEを削除する。

lockfileコマンドはLOCKFILEがあるときは削除されるまで待つ。オプションで待ち時間やリトライの回数等を設定できる。

ファイルの作成、削除はアトミックに行われることを利用している。

flockコマンド

これもどの環境にもあるとは限らないが、flockコマンドを使う方法。Linux限定であれば、これが一番いいと思う。このコマンドはファイルのありなしでなくて、アドバイザリロックという機能を利用します。

LOCKFILE="/tmp/example.lock"

(
  flock -w 10 -x 200 || {
    echo "ERROR: lock timeout" 1>&2
    exit 1;
  }

  # 排他したい処理

) 200> "${LOCKFILE}"

上の例は10秒間ロックを取得できなかったらエラーとなります。サブシェルがLOCKFILEを200番のファイルディスクリプタで開いて、flockコマンドが200番のファイルディスクリプタで開かれているファイルのアドバイザリロックを取得します。サブシェルから抜けるとLOCKFILEは閉じられるので、ロックが開放されます。例の場合は、LOCKFILEは最初だけ作成されますが、削除はされません。既存のファイルを開いてロックを取得することも可能です。

自前のlockコマンド

環境を選びたくないなら、自前でlockコマンドを作ってしまうというのも手段だ。

LOCKFILE="/tmp/example.lock"

function my_lock
{
  local start_time cur_time
  start_time=$(date +%s)
  local timeout=10

  while true; do
    ( umask 0777; : > "${LOCKFILE}" ) 2> /dev/null
    if [[ $? -eq 0 ]]; then
      return 0
    fi
    cur_time=$(date +%s)
    if [[ $((cur_time - start_time)) -gt ${timeout} ]]; then
      echo "ERROR: lock timeout" 1>&2
      return 1
    fi
  done
}

function my_unlock
{
  local exit_code=$?
  rm -f "${LOCKFILE}"
  exit "${exit_code}"
 
}

(
  mylock || exit
  trap  "my_unlock" EXIT HUP INT QUIT TERM

  # 排他したい処理
)

多分、lockfileコマンドとしていることは同じで、ファイルの作成に成功したらロックが取得でき、ファイルを削除したらロックを開放する。trapを使うのは処理が失敗したり、途中で止めたときでも、ロックを開放することを保証するためである。