記事を書くのが時間がかかって大変である
はい、タイトル通りです。
そもそもですが、転職活動を機にせっかくなのではてなブログを始めました。
技術以外にもゲームをやってて、チラシの裏的な個人の想いを載せたいなぁ、と思う事は今まで多々あったので細々と色んな雑記を書き続けていきたいなと思います。
あとは今までどこかに書いた記事たちの移植したい!
くらいの気持ちでブログを始めた訳です。
さて、「記事を書くのが時間がかかって大変」についてです。
まあ自分でも分かりきっているのですが、要するに文章が冗長なんですよね。
文脈から得られること、暗黙の了解なこと、他にも例であったりが多くて冗長だなと書きながら思ってます。
そして1日で書ききれなく、日をまたぐと筆が止まります。まあ筆ではなくキーパンチですが・・・w
で、ブログ書くのめんどくさい!大変!となる訳ですね。
今は転職活動という「とても!!!強い動機」があるので上手く稼働していますが、この活動が終わった際に今までと同じように途中で投げないで続けられるかについて考えてみました。
この冗長さは必要なんでしょうか。
よく技術系のブログは見るのですが、特に使い方を説明しているサイトでは説明が足りない!と思いませんか?
つまりチュートリアル的なところで終わっている点です。
これは例えば「ruby言語」のチュートリアル的な意味ではなく、「ruby言語の機能の1つであるmoduleをincludeする機能」のチュートリアル的な意味です。
frameworkだと特にありがちですが、公式のリファレンスを読んでいたり使った事がある程度なら「うん、知ってる」で終わってしまう内容ですよね。
知りたいのはその機能を使うべき状況であったり詳細であったり、前後の文脈であったりするわけです。
そう考えると、冗長さも含めて詳細な記事を書きたいのか、チュートリアル記事を書きたいか、に影響を受けそうです。
次に、冗長さを必要としている人・していない人ですかね。
つまり冗長さを必要としているのは詳細なことを知りたい技術者であり、必要ない人はチュートリアルを知りたい人が大半です。
どちらにもメリット・デメリットがありますよね。
例えば技術ブログに入門的な記事が多いのはPVを稼ぎやすいというメリット、詳細なテクノロジについてのブログは有識者で意見交換できる等
ですかね。ブログがどちらの方向を目指すか、という訳ですね。
まあここまで見ると短い記事たくさんあるブログにしとけば?って思いますが別にPV等は不要なので、もっと詳細な内容を書きたいと思ってしまう訳です。まあそうなると途中で飽きて元の木阿弥になる訳なんですがどうしたらいいんでしょう。
腰は結構重いので、LINEやメールなどでブログ早く書いて!!と言われても書かない気がしますね・・・。
うーん。skypeでみんなでブログ書こう通話でもあれば書くかな?これはいいアイディアな気がしてた! ※
この関係で何かいいネタにならないか考えてみる事にしましょう。
何かいいネタがあったら続きます、きっと。
※ちなみに元ネタはみんなでskypeしながらMMORPGのFF14しよう通話。もちろんFF14で一緒のコンテンツで遊んでいない状態。
転職活動日記(1)
はい、今回は転職活動についての雑記です。
転職するに当たり、自己のプロファイリングは必要な訳ですが、記事にしながら自分の考えを整理してみようと思った次第です。
なんか厨二的な記事ですが色々と探求していきましょう。
さて、一番最初ですが・・・昔幼い頃の夢は音楽家になることだったのです。
なぜ音楽家かというと、多分昔は音楽を通じて色んな人に認めて欲しかったし、社会にコンテンツを提供することで貢献し、音楽を通じて既に廃れつつあった円盤ビジネスへの救世主になりたかったのかな、と今思うと感じる訳です。
つまるところ、身近や遠い他者からの自己承認欲求と、社会からの承認欲求、あとは金銭的な欲求を満たすために、そういう仕事の1つである音楽家になりたい、と思ったということですね。
子供の頃の話なので単純に「かっこいいから」とかそういう理由ですけど。そして他にそれらの欲求を満たせる選択肢が思いつかなかったからですね。
まあ、そんなこんなでギターとかベースとか色々やりましたがさっぱり才能もなく、普段練習するのも難しく(防音もない普通のマンションなので)、ある時諦めてしまった訳です。
夢は夢として、現実ではどうか、でとりあえず理系に進み電気電子の世界へ向かいました。
就職に困らないから、で選んだ訳ですが当然そんなモチベーションでは続くはずもなく、紆余曲折あり最終的にプログラミングを学び、プログラマになった訳ですね。
なぜプログラマ?となる訳ですが、まず大切なのは幼い頃にあった欲求は形を変えて別の「創造的欲求」に変わったということです。
簡単にいうとプログラミングして何か作るのが楽しかった訳です。
さて、勿論人間ですから、他にも色々欲求があります。
コード書くより女の子と遊んでた方が楽しいですからね、女の子と遊んでるだけでお金たくさん貰える仕事ってなんでしょう。ホストですかね?(まあ色々問題はありそうですがw)
勿論、たくさんの女の子が貢いでくれるような傾国の美女ならぬ傾国の美男子な訳ありませんから、実現不可能な欲求というか希望な訳です。
ということで、なぜ創造的欲求が一番になったかというと達成手段が一番難易度が低かったからです。
元々MMORPGをやっていて、そこでBOTを作ったりで事前の知識があったのと、その上でプログラムをそこそこ学び、同級生よりはほんの少しプログラミングの才能があったからですね。
そしてプログラマになり、1度転職し現職に至る訳です。
ちなみに1回目の転職ではお給料の問題で生活が苦しくて転職に至りました。まあ欲求だ何だと言ってますが命や生活に係わる事ならそちらの方が重要ということですね!!
ということで、現職につき時が経つにつれて欲求が変化してきました。
現職ではBtoCのサービスに携わっていることがあり、実際はandroid/iPhoneアプリなんですが、これを作ってるんだよーと見せると「すごーい!!」って言われますし、自分の作ったものが役立っていて嬉しい訳です。単純ですねw
しかしこれは承認欲求的な意味合いで嬉しい訳でないのですが、長くなるのであとにしましょう。
そしてもう1つの創造的欲求も少し内容が変わったな、と思います。
昔は何か作っていれば楽しかったですが(今もそこは変わりないですが)、より高度なものを作りたいと思うようになりました。
現在で言えば、メンテナンスや拡張開発の際に容易に変更が可能で、他システムやモジュールへの影響が最小で、より新しい概念(今で言えば機械学習など)を取り入れた、といったものです。
そんな中、現職は自社サービスではなく、あるサービス業者からの請負業務でシステムを構築している事もあり、この欲求を満たせなくなってきた、ということです。
多くのユーザは「いいサービス」を「最初に決めた期間までに」作りたいのであって、「いいサービスをより良く」ではないんですよね。
この、動く洗練されていないシステムと、洗練された素晴らしいシステムの差といいますか、そこに支払うコストと期間については中々理解されない話だと思います。
マーティン・ファウラーがとても素晴らしい言葉で定義しています、そう「技術的な負債」と言いますが、拡張の度に巨大化し、あるニーズに追従する時間がかかるようになり、最終的には拡張不可能な状態やバグだらけ、処理が遅い使えないシステムに至るわけですね。
勿論、この技術的な負債に対する知識の啓蒙であり、問題解決のためのframeworkであり、プロダクトであったりをいつか作り上げたいという目的もあります。
この話はちょっと置いておきましょう。
さて、欲求の話から変わりますが、将来の展望というものがあります。
30歳までに結婚して2年くらいで子供が生まれて、35歳までに郊外に家かマンションを買って・・・ってやつですね。
これを同じようにプログラマとしての60歳までの将来展望について考えましょう。
夢だけで言ってしまえば、ジョン・フォン・ノイマンみたいに計算が早く、ケント・ベックのように様々な手法を発見し伝道でき、マーティン・ファウラーのように色々な示唆に富んだ知恵を持ち、まつもとゆきひろのように楽しくコードを書ける言語を開発し・・・っていうスーパーマンです。
まあ無理ですよね、現実的に到達可能な夢でないと将来の展望とは言えません。
そうです、60歳になったときどんなプログラマになりたい上で一番実現性が高いか、という話になるわけです。
ところで、どうしてその人物になりたいと思うのでしょうか。
つまるところ、自分の中にある最大の目標を解決できる人物であり、スキルセットであり、を持ちたいという事です。
この内容で言えば、問題解決のframeworkなり、プロダクトなり、知識を体系付けを行える実力が欲しいとなります。
その知識で何を作り、何の欲求を満たすのか、というのは次で考えましょう。
長々ときましたが更に続きます。
まあ結局どういう欲求を満たすために何を学び何を作り・・・という話ではありますがちょっと似たような遠いような話です。
そう、私があらゆる問題解決能力を欲しているのは、問題解決システムを作り上げてその作成者として承認して欲しいという欲求ではないんですよね。
勿論、承認して貰って嬉しいです、ただそれが一番の目的ではない、ということです。
簡単にいうと社会に対しての帰属意識から、という事だと思います。つまり社会が良い形で進化して欲しいし、堅牢になって欲しい、という願いがあるという事ですね。
しかし、帰属集団への小さな貢献ではなく、できれば構造の欠陥や社会正義が作用しないか機能不全を起こしている問題に対してコミットしたいです。
例にしてみましょう。
ある繊維系大企業(帝人等)の社内システムを作ったとしましょう。
次に衆議院の社内システムを作ったとしましょう。
この2つのシステムが、同じアーキテクチャで、同じ規模で、同じメンバーで、同じ想定ユーザ数で、同じシステム構成で等の同条件でシステム開発した際、どちらを誇らしく思うでしょうか?どちらをやりたいか、に置き換えても良いでしょう。
多分衆議院の方が多いのではないでしょうか。つまりより社会に近しいものと、身近ではないものか、という事ですね。
まあ、衆議院の社内システムと、例えば東京水道局の社内システムではどちらに軍配が上がるのか、等々ありますがニュアンスは伝わりますかね?
これは一体どういう意識下から発生するものなんでしょうか?
そもそも「集団」がないと「個人」もないわけですから、社会が健全であれば、自己保存が達成されるし達成して欲しいという欲求が目的になるのでしょうか。
あと付け加えるなら、食べ物が素晴らしく、古き良き文化や遺産もあり、協調性や社会性のある人々、素晴らしきコンテンツ、治安も良く、この文化が進化し続けて欲しいという民族意識なんでしょうか。
この民族意識は、ワールドカップで日本代表が勝つと嬉しい、誇らしいと思うことと一緒だと思いますし。
哲学っぽく言うと、何にも根ざしていない己のレゾン・デートルを獲得していないためですかね。
ということで自分でも分かっていないし上手く説明できないこれを例にするのは難しいですね、NPOやNGOの活動みたいなものでしょうか。
会社と創業者と社会に対する関係もそうかもしれません。
まあ、この文脈を掘り下げていっても、最終的に転職活動に必要なのはどういうスキルが必要で、それはどういう欲求や帰属意識という概念で良いですしこれまでとしておき、あとは集団社会学や社会学、心理学をより深く知っている方にお任せしましょう。
さて、次にいきましょう。次は企業という集団に帰属するという事です。
会社に所属するということは、会社に帰属することであり、つまり会社という村が持つ文化圏を自己啓発する必要があるということです。
日本という文化圏に帰属するということは、日本語という文化を理解し使い、日本の生活習慣に合わせて生活し、文化(刑法や民放)を元に集団内の他者と協調するということですかね。
ということで、集団が持つ文化に自己啓発できるかが問題です。
ある属性A~Eがあり、自身はA~Cはマッチし、Dはマッチせず、Eはミスマッチしたときに、マッチしなかった・ミスマッチ属性の変化を許容できるか、変移できるかですかね。
とは言っても比較は難しいですよね。人間関係の話を合わせて考えてみましょう。
- 自分は設計とframeworkと言語に特化したプログラマ
- 同僚Aさんはアルゴリズムを追い求め続けるストイックなプログラマ
- 同僚Bさんは広く浅くだけど様々な知識を持っている歩くグーグル先生
- 上司Cさんは縁の下の力持ちで影で経理や勤怠まで面倒を見てくれるお母さん
- 上司Dさんは朝から新聞読んで何もしないけど謎のコネクションで仕事を取ってくる人
- 部下Eさんは5年くらい前の自分と同じくらいのプログラマ
なお、予め言及しますが、社会正義や社会のルール(法律等)に基づいた行動している上で、です。
例えば上司Dさんがリベートや犯罪組織からの利益供与で仕事を取ってきても尊敬はできませんよね。
さて、新しい価値を創造する文化に追従するにはフロンティア精神を持つべきですし、ここらへんは分かりやすいですよね。
協調することも分かりやすいですよね。Aさん~Dさんはそれぞれ自分にない問題解決能力であったり、マネジメント力であったり、様々な知識であったり、人脈形成スキルを持っている訳ですから、お互い補完し合い仕事ができたら素晴らしいと思います。
Eさんはちょっと難しいですかね、個人的には失敗談や成功例のノウハウを上手く吸収して上手く成長して貰いたいし、成長してそのEさん独自の考え方を見せて欲しい、という形ですかね。
しかし実際はこんなに単純ではありませんね?ということでもう少し踏み込んで考えてみましょう。
この条件で自分は年収500万で、Bさんは年収2000万だとしましょう。(実際にはそこまで差は出ないと思いますが)
この条件下で同じ程度の難易度・仕事量のプログラミング(開発)をした際に、果たしてBさんと上手くコラボレーションできるでしょうか?
難しいですよね。なぜ難しいのでしょうか?
単純に考えると、Bさんはお金たくさん貰ってるしもうちょっと頑張って欲しいな、と感じる人が多いと思います。
ここでもしBさんが自分の4倍の仕事量や単純に4倍の労働時間、非常に専門的な知識が必要で4倍価値のあるコードを書いていれば、Bさんは凄いエンジニアとして凄いし、ぜひ一緒に仕事をして技を学びたい、と感じると思います。
これは、「自分から見て」Bさんが不当に報酬を受け取っていることと、自分が正当に報酬を受け取っていないことに対して不満を感じる訳です。
自分から見て、なんですよね。営業が評価され、モノを作ったエンジニアが評価されない、といった記事が多々あると思います。これは同じ問題に根差しているはずです。
この場合は、営業のXさんに対しての不満ではなく、会社はもう少しエンジニアを評価して欲しい、と思うはずです。
つまり、社会主義的な話になりますが、集団は帰属している個人から労働力を集め、その労働力を使った結果を富として再分配しているのですが、この労働力 / 再分配された富 の比率が自分の想定したものと集団が決定したものとの差が異なるために起り得て、集団が決定したものが許容できないために不満に思う訳です。
要するに、自分の中・集団で定義されている、この労働の単位が近しく、再分配される富の単位も近しく、更にその2点の比率も近しいのであれば単純な報酬という意味では不満はないのかな、と思います。
この不満というかギャップが容認できるようであれば、きっと素直に同じくらいのレベルの人であればお互いコラボレーションし合って頑張ろう、となり、凄いエンジニアであれば知識であったりノウハウであったりを学びたいですし、数年前の自分と同じくらいの人にはこうすれば良かった等のノウハウを伝えて上手く生かして欲しいと思えるはずです。
最後になりましたが纏めましょう。
ある人生の目的があり
- それは、有名になりたい・社会の役に立ちたい・ものを作るのが楽しい等の欲求があり
- その目的を達成するには
- スキルA,B,C...
- 知識A,B,C...
更には、会社に所属するに当たって、会社の価値観と自分の価値観のうち
- 労働の単位が近しく
- 報酬の単位が近しく
- 労働と報酬の比率が近しい
さて、私のパターンで見てみましょう。
- 社会構造による欠陥や個人の生活に根差した問題、最適化できる領域において、解決に向けて貢献したい
- 何を作る楽しさを求めたいし、より良く洗練させていきたい
- 程々に技術力や作ったものに対して賞賛されたい
- システムを作り上げる際に柔軟に期間やコストに対応でき
- その際に昔ながらの手法を使いつつ、新たな概念を取り入れて問題解決に取り組む姿勢があり
- 様々な価値観を重宝し、そのノウハウや考え方を学ぶことができ
- まったく違う事業も新規で起こしていくような
価値観については
- メンテナンス性・保守性・重複コードの少なさ
- 幅広い知識
- 深い知識
- 人脈・営業力
- コミュニケーション能力
- 管理能力
例えば深い知識でも「サーバの知識」と「COBOLの知識」と「rubyの知識」と「tomcatの知識」では価値は違いますよね?そしてそれらも重要といえば重要ですし、全部に100点中の100点を付けてよいなら100点になる訳です。
でもそんなことはなく、基本は100ポイントの割り振りです。
それも、目指しているもの、置かれた環境、人生で得られた価値観、世の中の状況や将来等々に左右され、100ポイントは目指している場所が同じでも振り方は変わってくる訳です。
ということで、ここは会社側も自分自身も書ききれないですし、この価値観の比率が合いそう、という事にしておきましょう。
長くなりましたがこんな感じでしょうか。
転職活動日記(2)はまた機会があれば書きたいなと思います。
ドメイン駆動 - コンテンツ管理 (3)
前回の記事はこちら(リンク先)からどうぞ。
さて、変更に強いとは言ってもどうすればいいのでしょうか?
今流行りつつあるドメイン駆動設計でしょうか?
ちょっと古いSOA(service oriented architecture)でしょうか?
railsや組み込まれたMerbの様々な概念を使うことでしょうか?
まあたくさんありますよね。人それぞれ、プロジェクトそれぞれ、会社それぞれに解決方法はあると思いますが・・・
今回はあまり知られていない概念モデルを使っていこうと思います。
使って行こうといいますか、今回の問題を概念モデル化して、ドメイン駆動におけるモデルを洗練させていく作業に近しいでしょうか。
概念モデルやそれのパターン(アナリシスパターン)をもっと詳しく知りたい!という方はこちらの書籍(リンク)が素晴らしいです。ドメイン駆動設計(青の方)と合わせてぜひ一読して欲しい、とオススメできる一冊です。
さて、最初は「なぜ概念モデルなのか」というところです。
ちょっとだけITコンサルタントの業務になってしまいますが、あるサービスを新規リリースする際やリプレースする際に
- こういうシステム構成で
- こういうアプリケーションを作って
- こういう運用体制で
あるシステムのリプレースが特に分かりやすいですが、この提案をする際にコンサルタントが問題を解決する手法には幾つかあり、その1つが
- 今どんな(システム・運用の)問題を抱えているのか
- 問題だらけの現状に対しての、理想的な状態
- 理想に向けて解決すべき問題と解決方法
さて、実際に前回の記事で問題となった点を挙げてみましょう。
- テーブル構造が肥大化している
- テーブル構造が複雑化している
- 変更の度にテーブルが変更されるため再起動(メンテナンス)が必要
- 数限りなくある端末やOSの組み合わせの登録が運用コストを上げている
今回の主題ではないので置いておきますが、ちょっと言及すると、アプリケーション(ファイル)の肥大化によるストレージ容量の逼迫、N/W通信量の肥大化、等でしょうか。
話を戻しましょう。
「テーブル構造が肥大化・複雑化する」のは理想の状態ではないからですよね。
そもそも理想ってなんだ?という話になりますが、1つの解決方法として「実際に存在するモデル」から「概念的に考え得るモデル」にして、モデル間の構造を考えてしまえば、概念的には実際に存在するモデルを全て包括する訳ですから、理想と言えますね。
よく分からない話になってきましたね。
簡単な例を挙げましょう。
「記事・アプリケーション」があります。この記事のタイトルにもなっていますが、どちらも「コンテンツ」ですよね。
業務モデルの「記事・アプリケーション」を「コンテンツ」としておけば、ニュースも広告も、静止画像も、動画もみんな「コンテンツ」ですから、どんな変更がきても包括されるという訳ですね。
ということで、今回の概念モデルは「テーブル構造が肥大化・複雑化」する問題に対する理想を作るというものであって、
- 変更の度にテーブルが変更されるため再起動(メンテナンス)が必要
- 数限りなくある端末やOSの組み合わせの登録が運用コストを上げている
前置きが長くなりましたが、スタート地点はよく出てくる業務モデルです。1回目の記事で使った業務モデルを見てみましょう。

はい、これについて抽象的な概念にまで持っていければよい訳ですね。
しかし、1つの実際にする存在するモデルは、概念をそぎ落として実装されたものですから、1つだけを見ると概念モデルが不完全である可能性があります。
実例で考えましょう。

コンテンツの実例としてアプリケーションや動画、記事などがある訳ですが、ここで既に差があります。アプリケーションには長さという概念がなく、動画、記事には価格という概念がありません。
まあ動画は無料動画とか有料ストリーミング動画とかありますから、価格という概念は出てきやすいかな、とは思いますけど一旦置いておきましょう。
ここでもしアプリケーションというコンテンツを考慮せずに動画・記事という業務モデルのみで概念モデルを形成した場合は、価格という概念が抜け落ちてしまう訳です。
実際は、当然概念が抜け落ちることがありますし、アナリシスパターン化するには、3回くらいはリリースしたシステムのドメインに対してではないと知見が足りなく上手くいかないと言われる所以ですね。
さて、一気に進めましょう。コンテンツと言われるものを集めて、属性を集約してみましょう。

こんな感じでしょうか。コンテンツに含まれる概念は不足していますが、今回の主題にしたいところではないため省略としています。
また、今回は完全な概念モデルを作るのではなく、あくまでも導入ですのでこれくらいにしておきましょう。
次はコンテンツの属性について見ていきましょう。
前回の記事では「表示する端末」は厳密に記述すると「コンテンツを表示する端末・OS組合せの一覧」ですね。要はブラックリストです。
更に掘り下げると「コンテンツ」の「表示を許可」する「端末・OS組合せ」の「一覧」ですね?
ということで、ブラックリスト・ホワイトリストを図にしてみましょう。

さて、「表示許可」「表示不許可」について掘り下げていきましょう。
あるユーザが特定の端末OSでアクセスした際に表示「できる・できない」条件ですね。
つまり、一般的によくあるログインユーザの所持権限によって、機能を実行「できる・できない」条件と似た構造な訳です。
この権限をヒントに文脈を掘り下げましょう。つまり、
あるユーザが特定の端末OSでアクセスした際に、権限が付与されていた場合に表示「できる・できない」条件ですね。
この概念を元にもう少し概念モデルに近づけましょう。

つまりこういう事ですね。あるユーザが特定条件(指定端末)でアクセスした際にはコンテンツを表示する権限を有するため、コンテンツの表示が可能と。
さて、コンテンツの属性について、この表示権限と同じ概念のものを探していきましょう。
ぱっと見た限り「公開開始日」「公開終了日」「プレミアム対象」「年齢レーティング」が合致しそうですね?細かく見ていきましょう。
- 公開開始日~終了日は表示権限の可否は可で、演算としては以上・以下・一致、権限タイプ・・・としてはつまりコンテンツドメインに含まれる要素「公開日」を「基準値」にしている
- プレミアム対象権限の可否は可で、演算は一致(もしくは以上)、基準値はユーザドメインのユーザ種別(フリー・通常課金・プレミアム課金・カスタマーサポート・システム管理者等)
- 年齢レーティングは表示権限の可否は可で、演算は以上・以下、基準値はアクセス者の生年月日もしくは年齢、年齢別アクセスチェック(確認)の可
これらを踏まえてもう少しこの概念を成長させてみましょう。

大まかにいくとこんな感じでしょうか。この構造によって表示権限が増えたとき、例えばコンテンツを表示するための条件としてシンジケーション(収益対象化のプラットホーム)からのアクセスのみ表示を行う、という要件が加わったときに、コンテンツ自体やその関連に変更はなく、表示権限の実装の1つとして、ユーザ・端末OS組合せと同じ階層に「ユーザのアクセス元」が基準値として加わるわけです。
このシンジケーション要件例は、記事のPVを元にユーザに何かの価値(お金、ポイント等)を提供する際に、PVにはRSSで参照されたPVを表示したくないという事ですね。
RSSには広告を任意に含めるのは難しいですし、ブログとしての広告媒体という意味で見られていない訳でありRSSにシンジケーションは含めない、という要件例であればこの概念ドメインにマッチすると思います。
さて、もう少し表示権限のみを掘り下げましょう。
この表示権限は複数の責務を担っています。それは
- 表示の可否
- 入力値の算出
- 基準値の算出
- 演算方法
- 演算(判定)
もう少し概念的にいきましょう。
結局のところ、ある2点間「アクセスユーザのプレミアム課金」「記事のプレミアム対象」における関係がどうなのかを演算する責務と、2点間を割り当てる責務がある訳ですね。
つまり、2点間を割り当てする責務はどうなるべきでしょう。
2点間を割り当てるということは、その2点が概念的に等価、等級が同一といいますか、を知っている必要がある訳ですし、同一にする責務を負っているはずです。つまり算出は、単なるプロパティの選択ではなく、値の変換を含めた導出です。
よって、「入力値の算出」「基準値の算出」はある値の導出を行うものであり、「表示権限」ではないため、導出責務として独立し、表示権限は導出責務を知るという責務だけあればよい訳ですね。
例を挙げてみましょう。
年齢レーティングというものがあります。R15やR18ですね。
ある記事が18歳以上でなければ参照する権限がない、ということは2点は「記事に設定された年齢レーティング(から算出される実際の年齢)」と「アクセスユーザの生年月日(から算出される年齢)」になりますが、この概念的には異なる2点を同一概念に変換する必要があります。
さて、立ち戻って結局のところこの概念は「表示権限」ですから、異なる2つの概念を同一にする訳ではないですよね。
さて、まだ3つ残っていますね?
ということでそちらも探求してみましょう。
カンの良い方はもう理解してそうですが、つまるところ表示の可否と演算方法はどちらも演算であって一緒ですよね。
そして、導出責務と同じく、表示権限が演算の内容を知る必要はなく、パッケージされた演算手法のみを知る責務があるわけです。
結局のところ、表示権限は「導出責務」と「演算責務」の関係を作動させる責務がある、という事です。
ここまでを図にしてみましょう。

はい、随分簡単になりましたね。残りはもう少しです、頑張りましょう。
ということで次です。
結局表示権限となりましたが、この検索する際の条件は1つではなく複数になるはずです。
「記事がプレミアム対象の際は、ユーザがプレミアム課金者」「記事が公開済である」「現在時刻が記事の公開日より未来である」等です。
これについて考えてみましょう。
導出責務(入力値の算出)が「記事がプレミアム対象の際は、ユーザがプレミアム課金者」の「表示権限」の導出責務・演算責務を関係した結果になりそうですね?
そして、導出責務(基準値の算出)が「記事が公開済である」になり、演算責務は「論理和」になる訳です。
つまり、表示権限は子要素となる表示権限を持つ場合がある、ということですね。
ということでこうなりました。

さて、次は違う例にいってみましょう。
経験豊富な方ならお気づきかも知れませんが、ここで扱うのはそう、「長さ」についてです。
コンテンツについて様々な実体ドメイン、つまり動画・記事・アプリケーション・ニュース・音楽等々があるわけですがから、長さと偏に言っても「文字数」「再生時間」等ですね。
このままドメイン設計の観点から言ってしまうと、コンテンツというドメインの概念には長さを表すValueObjectが1つあり、長さを表す。実ドメインとしての動画ドメインであれば長さというValueObjectは再生にかかる長さを表すものである。となってしまいますね?
しかし待ってください、我々が本当に知りたいのは、「長さ」ではないでしょう。YouTubeで動画の長さ(再生時間)を見て「再生時間はコンテンツを消化する時間」と認識しているはずです。
消化というのはつまり、参照したコンテンツからユーザが期待した結果を得られるまでの時間、ということです。
例を挙げてみましょう。
ブログ(記事)の長さの基準に文字数というものがあります。この記事の文字数だけ見ても参照する人にとっては何の参考にもなりませんね?そう、記事の中に動画やスライドが含まれていればその記事を読んでいる時間が延びるはずです。
つまり、長さとは平均再生時間であり、平均読破時間であり、ユーザがコンテンツから期待した結果を得られるまでの時間
、「平均的に参照するまでの参考値」という事ですね。
その1つに文字数であったり、動画の長さがあるという事です。
更にいうなら、単純な再生時間、つまりコンテンツを参照する最大時間も持つべきです。
つまりコンテンツは複数の長さを持つ、ということですね。
ここでコンテンツは複数の長さを持つ、としてドメインの概念を理解することによって、最終的に実装される「コンテンツ」テーブルに長さを持たず、「コンテンツ長さ」テーブルという関係テーブルに長さを持つことによって、容易にコンテンツの参照の参考値となる長さを追加できる訳です。
さて、今回はここまでです。
例に挙げたコンテンツと表示権限と長さの概念が全てに当てはまるかというときっと知見が足りません。
世には色々なデジタルコンテンツのサービスがありますから、もっと素晴らしい概念が含まれているかも知れません。
毎回このドメインの概念を探求していくべきですし、ドメインが進化していく以上完成はないわけです。
しかし、ここで用いたアナリシスパターンでブログシステムを構築した後に、動画配信サービスを構築する際はドメインモデルのスタートとしてきっと役に立つと思いますし、真っ新からスタートした状態よりは堅牢なドメインモデルを作りやすいはずです。
そういうことで、このドメインへの探求(の入り口)への手助けになれば、と思います。
なお、時間に余裕ができたら実装編ということでコンテンツ管理(4)に続きます。
ドメイン駆動 - コンテンツ管理 (2)
前回の記事はこちら(リンク先)からどうぞ。
さて、参照しやすいように前回のデータベース設計を置いておきましょう。

このうち、記事検索APIをコードにしてみましょう。
public class ArticleList extends DomainObject{ private List<Article> articles; private ArticleListCondition condition; public ArticleList(List<Article> articles, ArticleListCondition condition){ articles = articles; condition = condition; condition.setAll(articles.size()); } public String getBlogId(){ if(articles.size() > 0) return articles.get(0).getBlogId(); return null; } } public class PagingObject{ private Integer start; private Integer end; private Integer all; //getter,setterは省略 } public class ArticleListCondition extends PagingObject{ private Integer tagId; private Integer blogId; private String term; //getter,setterは省略 } public class ArticleAPIServiceImpl implements ArticleAPIService{ @DependencyInjection private ArticleRepository articleRepository; @Override public ArticleList find(ArticleListCondition message){ List<Article> articles = articleRepository.find(message); ArticleList result = new ArticleList(articles,message); return result; } } public class ArticleListAPIController extends JSONController<ArticleList>{ @DependencyInjection private Converter jsonConverter; @DependencyInjection private ArticleAPIService articleAPIService; @Override public String service(ArticleListCondition condition){ ArticleList result = articleAPIService.find(parameters); return jsonConverter.convert(result); } } public class ArticleRepositoryImpl extends JDBCRepositoryImpl implements ArticleRepository{ @Override public ArticleList find(ArticleList parameter){ StringBuilder query = new StringBuilder("select * from article where "); List<Object> param = new ArrayList<>(); if(parameter.getTagId() != null){ param.add(parameter.getTagId()); query.append("tag_id = ?"); } //他のパラメータは省略 return update(query.toString(), (Object[])param.toArray()); } }
こんな感じでしょうか。DIしているところはアノテーションを付けてます。ここではDI自体どのように行うかには言及しませんのでご了承ください。あとSQLの「select * 」は見やすいように省略しているだけなので、実際は必要な項目のみを記述してくださいね。
さて、ぱっと見た感じであれば問題ないですし、実際このコードに変更が入らなければまぁ問題ないかな、と思います。
とは言え、変更がないなんて事は滅多にありません。
有り得そうな変更が起こった時にどう変化していくか見ていきましょう。
まずは・・・ユーザがログインしていて、かつ、プレミアムユーザであれば、プレミアム対象の記事を表示し、それ以外はプレミアム対象の記事を表示しない、という要件が発生したとしましょう。
コードで変更すると以下のようになりますよね。
public class ArticleListCondition extends PagingObject{ private Integer tagId; private Integer blogId; private String term; //追加された箇所 private String token; //getter,setterは省略 } public class ArticleRepositoryImpl extends JDBCRepositoryImpl implements ArticleRepository{ //追加された箇所 @DependencyInjection private UserRepository userRepository; @Override public ArticleList find(ArticleListCondition parameter){ StringBuilder query = new StringBuilder("select * from article where "); List<Object> param = new ArrayList<>(); if(parameter.getTagId() != null){ param.add(parameter.getTagId()); query.append("tag_id = ?"); } //追加された箇所 User user = userRepository.getByToken(parameter.getToken(); Function<User,Boolean> isPremium = (u) -> { if(user == null) return false; //user.userType == 0 is free user if(user.getUserType() == 0) return false; //user.userType == 1 is normal user //user.userType == 2 is premium user //user.userType == 3 is customer support user //user.userType == 4 is administrator return true; }; if(isPremium(user)) query.append(" and is_premium = 1 "); else query.append(" and is_premium = 0 "); //他のパラメータは省略 return update(query.toString(), (Object[])param.toArray()); } }
こんな感じでしょうか。まぁ実際にログインしいてるユーザかを判定するトークン発行機能が既にあれば大した事のない機能ですね。
さて、実際は開発中もしくは2次フェーズといいますか、リリース後に追加フェーズもしくは運用開発として更にたくさんの要件が上がってくると思います。
ということで要件をどんどん増やしていってみましょう。
まずは、記事・アプリケーション以外に、ニュースも配信したいという要件が出てきたとしましょう。
ドメイン的には「article」に合致しそうですが、「news」というテーブルをまったく同じ構造で作って、ほぼ同じコードになっていると想定できますね?
実際に書いてみましょう。
public class NewsAPIServiceImpl implements NewsAPIService{ @DependencyInjection private NewsRepository newsRepository; @Override public NewsList find(NewsListCondition message){ List<News> news = newsRepository.find(message); NewsList result = new NewsList(articles,message); return result; } } public class NewsAPIController extends JSONController<NewsList>{ @DependencyInjection private Converter jsonConverter; @DependencyInjection private NewsAPIService newsAPIService; @Override public String service(NewsListCondition condition){ NewsList result = newsAPIService.find(condition); return jsonConverter.convert(result); } } public class NewsRepositoryImpl extends JDBCRepositoryImpl implements NewsRepository{ @Override public NewsList find(NewsListCondition parameter){ StringBuilder query = new StringBuilder("select * from news where "); List<Object> param = new ArrayList<>(); if(parameter.getTagId() != null){ param.add(parameter.getTagId()); query.append("tag_id = ?"); } if(isPremium(user)) query.append(" and is_premium = 1 "); else query.append(" and is_premium = 0 "); //他のパラメータは省略 return update(query.toString(), (Object[])param.toArray()); } }
はい、こんな感じですね。「ArticleRepositoryImpl::find」の「isPremium」クロージャはJDBCRepositoryImplへと実装を委譲することができそうなので、そのまま委譲しました。
どんどんいきましょう。更に要件を増やします。今回は配信対象となる機種条件をアプリケーションだけではなく、記事とニュースにも適用できるようにしましょう。
以下のコードでは省略しますが・・・
「device」テーブルに「user_agent」なるカラムを追加して、送信されてきたユーザエージェントを見て機種を判別するものとしましょう。実施はiccidやら何やらでもっと複雑な機種判定になるとは思いますけどね。
実際に記事やニュースを表示できる機種・OSの組み合わせ一覧はアプリケーションの「application_device_os」のような中間テーブルを作って判定するものとしましょう。
public class NewsListCondition extends PagingObject{ private Integer tagId; private Integer blogId; private String term; private String userAgent; //getter,setterは省略 } public class NewsAPIServiceImpl implements NewsAPIService{ @DependencyInjection private NewsRepository newsRepository; @Override public NewsList find(NewsListCondition message){ List<News> news = newsRepository.find(message); NewsList result = new NewsList(articles,message); return result; } } public class NewsAPIController extends JSONController<NewsList>{ @DependencyInjection private Converter jsonConverter; @DependencyInjection private NewsAPIService newsAPIService; @Override public String service(NewsListCondition condition){ NewsList result = newsAPIService.find(condition); return jsonConverter.convert(result); } } public class NewsRepositoryImpl extends JDBCRepositoryImpl implements NewsRepository{ @Override public NewsList find(NewsListCondition parameter){ StringBuilder query = new StringBuilder( "select distinct * from news ns "+ "left join user_device_os udo on udo.news_id = ns.news_id "+ "left join device_os_version ddo on ddo.device_id = udo.device_id "+ "left join device dc on dc.device_id = ddo.device_id "+ "where "); List<Object> param = new ArrayList<>(); if(parameter.getTagId() != null){ param.add(parameter.getTagId()); query.append("tag_id = ?"); } //他のパラメータは省略 Device userDevice = getDeviceByUserAgent(parameter.getUserAgent()); query.append("device_id = ? "); param.add(userDevice.getDeviceId()); if(isPremium(user)) query.append(" and is_premium = 1 "); else query.append(" and is_premium = 0 "); //他のパラメータは省略 return update(query.toString(), (Object[])param.toArray()); } }
こんな感じでしょうか。ユーザエージェントから端末のユニークIDを取得するところは親クラスに処理を委譲して共通化を図ってみました。それと、コード量が多くなるので記事以外は省略していますし、記事も大まかなところだけ想定して書いています。
段々と複雑になってきて、更に同じようなコードが重複してきましたね?
このくらいであれば色々な箇所を共通化できるようリファクタリングしていけば、まあまあ変更に耐えうるとは思います。
しかし、時にはもっと激しくドメインの構造を覆すような変更もあるので、もう少し掘り下げていきましょう。
次のお題は、「指定した端末で閲覧できる」以外に「指定した端末以外で閲覧できる」機能を追加したとしましょう。
今のままでは合致するドメインの構造はありませんから新しく追加しなければなりません。
一番簡単でぱっと思いつくのは、「application_device_os、news_device_os、article_device_os」を「whilelist_application_device_os、whilelist_news_device_os、whilelist_article_device_os」に変更し、「blacklist_application_device_os、blacklist_news_device_os、blacklist_article_device_os」を追加する、ですかね。
しかし、よくよく考えると「iOS 4.1」以外で表示する、と「iOS 8.1~4」で表示する、は共存できませんから、「news、application、article」に「is_blacklist_devices」みたいなカラムを追加して、中の値によって関連している中間テーブルはホワイトリストとして扱うか、ブラックリストとして扱うか、切り替えしてもいいわけですね。
これをコードにしてみましょう。
public class NewsRepositoryImpl extends JDBCRepositoryImpl implements NewsRepository{ @Override public NewsList find(NewsListCondition parameter){ StringBuilder query = new StringBuilder( "select distinct * from news ns "+ "left join user_device_os udo on udo.news_id = ns.news_id "+ "left join device_os_version ddo on ddo.device_id = udo.device_id "+ "left join device dc on dc.device_id = ddo.device_id "+ "where "); List<Object> param = new ArrayList<>(); if(parameter.getTagId() != null){ param.add(parameter.getTagId()); query.append("tag_id = ?"); } //他のパラメータは省略 Device userDevice = getDeviceByUserAgent(parameter.getUserAgent()); query.append( "when ns.is_blacklist_device = 1 "+ "then device_id != ? "+ "when ns.is_blacklist_device = 0 "+ "then device_id = ? "+ "end "+ ); param.add(userDevice.getDeviceId()); if(isPremium(user)) query.append(" and is_premium = 1 "); else query.append(" and is_premium = 0 "); //他のパラメータは省略 return update(query.toString(), (Object[])param.toArray()); } }
まあコードに起こすと簡単でしたね?しかし忘れてはならないのは、配信するサービスの数だけこの修正がいるということです。
どんどん進みましょう、と言いたいところですが要件は無限にありますしもうちょっと言及して終わりにしましょう。
今のブラックリスト・ホワイトリストの実装後に、
- やっぱり条件なしで記事見せたい
- PCのみ見せたいけどデバイスの登録が多すぎて大変なので、OSやユーザエージェントはあいまい指定したい
そういった要望を実装していくと
等々が起こり、最終的に「リプレースしよう!!」ってなりますよね。そう、保守開発している方なら経験があると思います。
何が悪かったのでしょうか?
例え最初の要件定義~リリースまでもれなく実装したとしても起こり得る問題ですよね。
最初の要件定義で未来の変更というかビジネスビジョンについて深く探っていけばよかったのでしょうか?
まあそんな事やったところで実際とは違うかも知れませんよね。
当初は5年後までに記事・アプリケーション・広告・着うた・待ち受け壁紙を配信する、という要望で、実際に運用してみてニュースやブラックリスト・ホワイトリストが出てくるかも知れませんし。
ということで、次回は変更に強いドメインモデルを以下に作るかについて考えていきましょう。
ドメイン駆動 - コンテンツ管理 (1)
ドメイン駆動やドメイン駆動設計が世に広まって久しいですね。
個人的には、最新技術、例えばインフラはAWS(elastic beanstalk + aurora DB)の、アプリケーションはscala + play2 + aurelia + bootstrapみたいな形で実装してく等の技術的な要素は勿論重要ですが、ドメイン設計と言いますか、分析モデルを作り上げていきドメイン設計として実装まで至る方もとても重要だと思います。
分析モデルからドメイン設計に至るまでは、業務ロジックと言いますか、ビジネスのコアな部分であることが多いため世に記事として出ない事が多いせいでしょうか、あまり注力というか注目というかされてない気がします。
まあ、パフォーマンスを始めとする非機能要件も勿論重要ですけどね。
ということで今回のお題は「コンテンツ管理・配信」です。
初回の当記事は開発フローの知識というか前提条件を統一するために、一般的な開発を例に挙げましょう。
それでは張り切っていってみましょう。
さて、スタートは自作アプリケーションや写真・動画・記事等の様々なコンテンツをアップロードおよび配信するコンテンツ管理・配信システムを作りたい、という要求から始めることにしましょう。
まずは業務分析や要求定義で使うユースケースやアクティビティ図がありますね?
ということで、ユースケース図を用意しました。
今回はさくっと書いてますが、もっとたくさんのユースケースがありますし、図中のアクターやユースケースももっとたくさん出てくるはずです。

はい、次はアクティビティ図です。

はい、記事投稿機能についてのアクティビティ図です。もう少し注釈があったりしますが、まぁ置いておきましょう。
これらの業務分析や要求定義の結果、以下のような要求仕様ができあがるわけですね。
- ブログ機能
- 記事の登録・編集
- htmlにて編集・配信できる機能
- 太文字・斜体・打消し・アンダーライン等文字を整形する機能
- 見出し・タイトル・引用符・リンク・表示の開閉・箇条書き・注釈等の記事を整形する機能
- 色編集機能
- 編集中のhtmlをプレビューする機能
- 配信URL編集機能
- 配信日時指定機能
- タグ設定機能
- タグ一覧表示・登録・編集・削除機能
- 下書き保存機能
- プレミアムユーザ向け記事設定機能
- 年齢によるレーティング制限機能
- 記事一覧機能
- 記事削除機能
- 記事評価・コメント機能
- ブログ概要登録・表示機能
- 説明文
- オーナー情報
- オーナー情報(外部URL)
- 投稿数
- 継続日数
- カテゴリ
- レイアウト編集機能
- タグ一覧表示機能
- 配信月一覧表示機能
- テンプレートによるレイアウト編集
- 手動によるレイアウト編集
- ブログ内検索機能
- 記事の登録・編集
- ユーザ管理
- ユーザ一覧表示・登録・編集・削除機能
- ログイン機能
- ユーザランク(プレミアム登録)機能
- アプリケーション配信・管理
- アプリケーション一覧表示・登録・編集・削除機能
- アプリケーションの評価・コメント機能
- アプリケーション配信予約機能
- 指定端末のみ配信可能な機能
- 端末一覧表示・登録・編集・削除機能
- タグ一覧表示・登録・編集・削除機能
要求仕様としてはざっくりこんなかんじですかね。実際は各項目にもっと細かい要求の説明があると思いますし、項目自体ももっと多いはずです。
ということで、次は非機能要件に行きましょう。今回はざっくり書いちゃいますね。
- 機能性
- 正確性、合目的性、相互運用性、セキュリティ、機能性標準適合性
- 信頼性
- 成熟性、障害許容性、回復性、信頼性標準適合性
- 効率性
- 時間効率性(システム効率)、時間効率性(業務効率)、資源効率性
はい、かなり略しましたが非機能要件です。これらももっと項目がありますし、各項目についての要求が述べられる訳ですね。
例えば障害許容性は3営業日以内に復旧すればいいよ、みたいな業務システムの場合(あるのかそんなシステム?っていうツッコミは置いておいて)、パフォーマンス的な観点で見て通常のマシンスペック(もしくはスケールアップしたもの)でスループットを達成できればシステムを冗長化する必要はない訳ですね。
今回は関係なさそうに見えますが、効率性という非機能要件においては、分析モデルからドメイン設計する際に影響を受ける可能性があるかも知れません。
なお、上記の非機能要件については日本情報システムユーザー協会(JUAS)が発行したガイドライン(リンク)を使っていますので、興味がある方はリンク先を参照ください。
次は業務フローと運用フローにいきましょう。
基本はアクティビティ図みたいなものが複数出来上がると思いますので、イメージは省略します。
例えばB2Cのシステムであれば、問合せ機能が要求にあり、電話もしくはメールでの問合せが主となると思いますが、そういった運用フローも含まれている訳ですね。実際は存在しないプロジェクトの方が多かったりしますが・・・。
まあ、そういった情報を元にサブシステムとしての管理システム(の要求)にユーザのタイプ(administrator、operator等)を持つ必要があるか?等の検討が進むわけです。
さて、業務モデルが出てくるのは次のステージの方が多いとは思いますが、良いプロジェクトであればステークホルダーやコンサルタントが自前で持ちだしてくる事もありますし、今の段階で描いてみましょう。

はい、こんな感じですね。
実際はもうちょっとドメインがあったりしますが、今回はそういったものを想定できる細かい要求仕様がないので省略しています。
なお、ドメインに対するプロパティの型を今この段階で明示する必要はないんですが、この図を書いているツールのクラス図がそうなってますので、図のサンプルとして入っちゃってます。
どんどん行きましょう。
大まかに情報が出そろったところで、今回はこういう環境構成で、こういうシステム関連図で、っていうのができてきますね?
ということで単純な環境構成で書いてみました。

実際はAWSであればEC2をauto scalingかbeanstalkにしたり、RDSはauroraにする、各サーバを冗長化する等色々ありますし、オンプレの場合もありますし、セキュリティ要件や想定ユーザ数、スループット等に影響されますので今回のはあくまでイメージということで。
システム関連図は本来あった方がいいと思いますが、今回は通りAndroid/iOSであればネイティブアプリからAPI経由で閲覧・登録し、ウェブ版であればWeb→APIで登録とぱっと見て分かると思いますので省略とします。
その次に参りましょう。ここで各機能別に画面遷移図や画面定義書が出来上がり、データベース設計書なんかができてくる訳ですね。
画面遷移図や画面定義書はあまり関係ないので省略するとして、データベース定義(ER図)はこんなかんじでしょうか。

まあ分析モデルをそのままデータベースにすることが多いですし、そのままにしてみました。
良し悪しで言うと当然ダメですがまずは先に進みましょう。
今後、サーバ側のドメインについて言及することが多いと思いますので、ここでAPIのインターフェースを定義してみましょう。
ブログにおける記事を参照するとき、一覧と詳細の表示があると思いますのでこの2つにしましょう。
一般にブログを見る時は全件or月別アーカイブorタグのどれかですので、その3つを検索条件として一覧を返すAPIと、記事自体を一意に識別するユニークキーを渡すと詳細を返すAPIって感じですね。
| API名 | URL | 入力パラメータ | 出力内容(サンプル) |
|---|---|---|---|
| 記事一覧取得API | date:yyyyMMで指定すること tagId:検索するタグのID blogId:検索するブログのID |
https://api.cms.companyname.co.jp/api/1.0.0/blog/article/?date=201601&tagId=123456&blogId=88877766 | ※1 |
| 記事詳細取得API | 省略 | 省略 | 省略 |
※1
{
articles:[
articleId:2222345676,
blogId:123456,
ageRating:12,
title:'ドメイン駆動 - コンテンツ管理 (1)',
detail:'<p>ドメイン駆動やドメイン駆動設計が世に広まって久しいですね。
個人的には、最新技術、例えばインフラはAWS(elastic beanstalk + aurora DB)の、アプリケーションはscala + play2 + aurelia + bootstrapみたいな形で実装してく...(省略)
',
url:'http://www.copanynameblog.co.jp/blog/entry/20160301235959',
isPremium:0,
...(以下省略)
],
blogId:123456,
paging:{
start:0,
end:0,
all:2
}
}
戻りましょう。上記のように今までの検討結果を持ってプログラミングが始まり、テストしてテストして・・・最後にリリースして、また最初のサイクルに戻る訳ですね。
まあご存知のサイクルですね。いや知ってるし別に・・・って方は長々やってしまってすいません。
あとは、設計書を増やしたり減らしたり、最初にテストコード書いたり、もっと(大きい/小さい)機能別にライフサイクルを回したり、それらの複合でやったり・・・と色々なやり方はありますよね。
分析モデルもドメイン駆動もプロジェクト運営手法も、今回は出てきてない継続インテグレーションも、変更を最小にし、変更が容易で柔軟なシステムを作る事が目的の一つで、様々な手法のどちらか一方しか選択できないという訳ではないので、全部いいとこどりすればいいじゃないという事で置いておきましょう。
まあ実際は全部やるのはコストとスケジュールの都合で難しいとは思いますけどね。
さて、前置きがとても長くなりましたね。次回はこのシステムに対する変更を見ていきましょう。
Java Reflection(メソッドを呼び出す)
前回の記事はこちら。
さて、今回はメソッドの呼び出しです。
実はメソッドの呼び出しだけだといまいちぱっとしないので色々盛り込みました。
ということでまずは見てみましょう。
package jp.co.companyname.pjname.domain; import java.util.Date; public class DomainObject implements Serializable{ private static final long serialVersionUID = -1L; protected String createdBy; protected String modifiedBy; protected Date createdDate; protected Date modifiedDate; public String getCreatedBy(){ return this.createdBy; } public void setCreatedBy(String createdBy){ this.createdBy = createdBy; } public String getModifiedBy(){ return this.modifiedBy; } public void setModifiedBy(String modifiedBy){ this.modifiedBy = modifiedBy; } public Date getCreatedDate(){ return this.createdDate; } public void setCreatedDate(Date createdDate){ this.createdDate = createdDate; } public Date getModifiedDate(){ return this.modifiedDate; } public void setModifiedDate(Date modifiedDate){ this.modifiedDate = modifiedDate; } } public class Foo extends DomainObject{ private static final long serialVersionUID = -1L; protected Strgin foo; private List<String> fooHistory; public Foo(){ fooHistory = new ArrayList<String>(); } public String getFoo(){ return this.foo; } public void setFoo(String foo){ this.foo = foo; addFooHistory(foo); } public String name(){ return foo; } protected void setDefault(){ this.foo = "foo"; } protected boolean isDefault(){ return this.foo.equals("foo"); } private void addFooHistory(String his){ fooHistory.add(his); } public static String getDefault(){ return "foo"; } public String getHistories(){ StringBuilder r = new StringBuilder(); for(String each : fooHistory){ if(r.length()>0) r.append(", "); r.append(each); } return r.toString(); } }
はい、単純なドメインオブジェクトがあります。メソッドとしては
- 1:このドメインが持つプロパティへのアクセサ(getter/setter)
- 2:ドメインを操作するpublicメソッド(Foo#name())
- 3:ドメインで共通のstaticメソッド(Foo#getDefault())
- 4:ドメインで共通のprotectedメソッド(Foo#isDefault(), Foo#getDefault())
- 5:ドメイン固有のprivateメソッド(Foo#addFooHistory)
では実際にリフレクション経由でアクセスしてみましょう。
Class<?> fooClazz = Foo.class; Foo foo = new Foo(); Method setFooMethod = fooClazz.getMethod("setFoo",String.class); setFooMethod.invoke(foo,"hogehoge"); System.out.println(foo.name()); foo.setFoo("hoge"); Method nameMethod = fooClazz.getMethod("name"); String name = (String)nameMethod.invoke(foo); System.out.println(name); 実行結果: hogehoge hoge
上段のコードのは1のsetterにアクセスしている訳です。getter/setterは特別なメソッドではありますが、単体で見ると通常のメソッドと同じ訳ですね。
下段のコードは2の公開メソッドにアクセスしている訳ですね。今回は引数なしの戻り値ありです。
それでは次に参りましょう。
Class<?> fooClazz = Foo.class; Method getDefaultMethod = fooClazz.getMethod("getDefault"); String defaultValue = (String)getDefaultMethod.invoke(null); System.out.println(defaultValue); 実行結果: foo
上記のコードは3のstaticな公開メソッドにアクセスしています。staticな場合は、Method#invoke(target,args) を呼ぶ際にtargetがない訳ですから、nullを指定しています。
さて、どんどんいきましょう。
Class<?> fooClazz = Foo.class; Foo foo = new Foo(); Method setDefaultMethod = fooClazz.getDeclaredMethod("setDefault"); setDefaultMethod.setAccessible(true); setDefaultMethod.invoke(foo); System.out.println(foo.name()); Method addFooHistoryMethod = fooClazz.getDeclaredMethod("addFooHistory",String.class); addFooHistoryMethod.setAccessible(true); addFooHistoryMethod.invoke(foo,"hogehoge"); System.out.println(foo.getHistories()); 実行結果: foo foo, hogehoge
上段のコードは4のprotectedなメソッドへのアクセス、下段のコードはprivateなコードへのアクセスをしています。簡単ですね!
Method#setAccessible() については、前回の記事にも書いた通り、セキュリティマネージャの設定次第では動きません。起動オプションやポリシ設定の仕方(このリンク先)を確認しておきましょう。
さて、次はプロパティの操作についてです。java.beansパッケージにはプロパティを包括的に操作できるPropertyDescriptorというクラスがあるので、そちらを使ってみましょう。
Class<?> fooClazz = foo.class; Foo foo = new Foo(); foo.setFoo("bar"); PropertyDescriptor pd = new PropertyDescriptor("foo",fooClazz); String getFooResult = pd.getReadMethod().invoke(foo); System.out.println(getFooResult); pd.getWriteMethod().invoke(foo,"hoge"); System.out.println(foo.getFoo()); 実行結果: bar hoge
こんなかんじですね。最初に指定したプロパティへのアクセサが簡単に取得できる訳です。
もちろんプロパティ「foo」であれば、「getFoo」「isFoo」「setFoo」等、java beansの命名ルールに則ってアクセサが実装されている必要があります。
メソッドがなければNoSuchMethodExceptionがスローされますので、次回のフィールドへのアクセスを組み合わせて疑似的にアクセサがあるように振る舞うこともできます。
さて、基本的なメソッドクラスは押さえましたので、もう少し先に進みましょう。
public class Bar extends Foo{ private static final long serialVersionUID = -1L; private String bar; public String getBar(){ return this.bar; } public void setBar(String bar){ this.bar = bar; } @Override @ThreadUnsafe @Transactional("hoge-transaction"); public String name(){ return super.name() + this.bar; } public static Bar newInstance(){ return new Bar(); } } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ThreadSafe{ } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ThreadUnsafe{ } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Transactional{ public String value(); }
はい、ではn階層の継承構造を持つドメインオブジェクトについて考察してみましょう。
継承構造はBar←Foo←DomainObjectになってますね?FooやDomainObjectのプロパティにアクセスする際は、getter/setterはpublicであるため、どの継承階層に所属しているかは普段あまり気にしないと思います。(特に継承構造が深いと)
勿論、今回のお題はリフレクションですから、リフレクションで操作することもあると思います。例えば
<bean id="barInstance" class="jp.co.companyname.pjname.domain.Bar"> <property name="bar" value="hogehoge" /> <property name="createdBy" value="administrator" /> </bean>
こんなような設定ファイルがあった場合に、Barが持っているプロパティ「bar」と継承階層の誰かが持っている「createdBy」が混在することになります。
この操作が上手くいく方法を考えてみましょう。
Bar barInstance = new Bar(); String fieldName = "createdBy"; String getMethodName = "get" + Character.valueOf(fieldName.charAt(0)).toString().toUpperCase() + fieldName.substring(1,fieldName.length()); Class<?> barClazz = Bar.class; Method getCreatedByMethod = barClazz.getMethod(getMethodName); String createdByValue = getCreatedByMethod.invoke(barInstance);
はい、動きそうな感じがしますが、残念ながらClass#getMethod(getMethodName) でNoSuchMethodExceptionが飛びます。
Barクラスは該当のプロパティを定義していないですし、getter/setterも持っていないためです。
メタプログラミング的には、Barの継承構造を知っている必要はないですし、把握もしてないですし、
ここではどうやってcreatedByを操作するか、のみ解決すればよいわけですね。
ということで書いてみましょう。
Bar barInstance = new Bar(); String fieldName = "createdBy"; String getMethodName = "get" + Character.valueOf(fieldName.charAt(0)) .toString().toUpperCase() + fieldName.substring(1,fieldName.length()); Class<?> barClazz = Bar.class; Method getCreatedByMethod = null; do{ try{ getCreatedByMethod = barClazz.getMethod(getMethodName); } catch(NoSuchMethodException nsme){ try{ getCreatedByMethod = barClazz.getDeclaredMethod(getMethodName); } catch(NoSuchMethodException nsme2){ barClazz = barClazz.getSuperclass(); } } }while(barClazz != null && getCreatedByMethod == null); if(getCreatedByMethod == null) throw new NoSuchMethodException(); getCreatedByMethod.invoke(barInstance);
はい、こんなかんじですかね。Class#getMethod / Class#getDeclaredMethodに存在しないメソッド名・引数の組み合わせを渡すと、NoSuchMethodExceptionが飛ぶことを踏まえて、Exception飛ぶ=見つからない、扱いとしてpublic / private・protectedのどちらにもメソッドがなかったら、継承構造を1つ遡るようにしているわけです。
それと、うーんExceptionがガシガシ飛ぶコードはちょっと・・・って思いません?ということでもう少し綺麗に書き直しましょう。
Bar barInstance = new Bar(); Object[] parameters = new Object[]{}; Class<?>[] parameterTypes = new Class<?>[]{]; String fieldName = "createdBy"; String getMethodName = "get" + Character.valueOf(fieldName.charAt(0)) .toString().toUpperCase() + fieldName.substring(1,fieldName.length()); Class<?> barClazz = Bar.class; Method getCreatedByMethod = null; Comparator<Class<?>[]> comparator = (left, right) -> { if(left.length > right.length) return 1; if(left.length < right.length) return -1; for(int i = 0; i < left.length; i++){ if(!left[i].equals(right[i])) return 1; } return 0; }; Function<Method[],List<Method>> converter = (arg) -> { return Arrays.asList(arg); }; Consumer<Method> consumer = (each) -> { if(getCreatedByMethod != null) return; if( each.getName().equals(getMethodName) && comparator.compare( each.getParameterTypes(), parameterTypes() ) == 0 ){ getCreatedByMethod = each; } }; while(getCreatedByMethod == null){ converter.apply(barClazz.getMethods()).forEach(consumer); if(getCreatedMethod == null) converter.apply(barClazz.getDeclaredMethod()).forEach(consumer); barClazz = barClazz.getSuperclass(); if(barClazz == null) break; } if(getCreatedByMethod == null) throw new NoSuchMethodException(); getCreatedByMethod.invoke(barInstance);
はい、こんなかんじですかね。幾つかあるラムダはユーティリティとしてどこかに纏めると更にシンプルになると思います。
さて、メソッドの呼び出しからはちょっと外れますが・・・メソッドの修飾子が何であるか判定する必要が出てくることがあるかも知れません。特にユーティリティを作成したときのメソッド呼出しで、static / instance の判定なんかは必要そうですね。
ということで、修飾子の判定をするのに必要な機能を見ていきましょう。
なお、Java言語のシンタクスとしては、メソッドに以下の修飾子を適用できます。
public, protected, private, abstract, static, final, synchronized, native, strictfp
Class<?> fooClazz = Foo.class; Foo foo = new Foo(); Method setFooMethod = fooClazz.getMethod("setFoo",String.class); //false Modifier.isAbstract(setFooMethod.getModifiers()); //false Modifier.isFinal(setFooMethod.getModifiers()); //true Modifier.isPublic(setFooMethod.getModifiers()); //false Modifier.isPrivate(setFooMethod.getModifiers()); ...
java.lang.reflect.Modifierには、intを受け取り、修飾子を判定するメソッドがあるのでそちらを利用しましょう。もちろん上記のコードの例以外にもたくさんのメソッドが用意されています。
前回の記事では紹介しませんでしたが、もちろんClass#getModifiers() もありますので、クラスに適用されている修飾子はそれを使って判定することができるわけですね。
さて、最後になりましたがアノテーションについて見ていきましょう。アノテーションについては纏めて記事にしますので、今回はさわりだけ。
Class<?> barClazz = Bar.class; Method method = barClass.getMethod("name"); Transactional[] trans = method.getAnnotationsByType(Transactional.class); System.out.println(trans[0].value()); 実行結果: hoge-transaction
はい、簡単ですね?AnnotatedElement#getAnnotationsByType() を呼ぶとそれに注釈されているアノテーションが取得できるわけです。これを使って各種frameworkは注釈されているアノテーションから設定値を取得しているわけですね。
AnnotatedElementインターフェースの実装は、AccessibleObject, Class, Constructor, Executable, Field, Method, Package, Parameterですので、これらでアノテーションを取得することができます。
ということで、今回はここまでとします。次回はフィールド(プロパティ)への操作です。
Java Reflection(インスタンスを生成する)
「Java」「Reflection」「リフレクション」といったキーワード、自分でframeworkぽいものを作ったり、ある程度共通化したいと思ったときに使った事があると思います。
どのような機能があり、どういった事ができるか、等の入門的な記事はたくさんありますが、もう少し掘り下げて書かれた記事はなかなか見当たらないため、今回は掘り下げた内容の記事を書いてみようと思います。そのため、そもそもリフレクションとは何ぞや?という方は他のサイトの記事を見た方が参考になると思います・・・その点はご了承ください。
ということでまとめると
- ・ある程度frameworkを使ったことがある
- ・Javaにおけるリフレクションの概要を知っている
- ・実際にリフレクションを細かく使っていきたい
- ・エセframeworkつくりたい
方に向けた記事になっています。
という事で、今回はタイトル通りインスタンス生成についてです。まずは定番ですが簡単な生成から見ていきましょう。
まずはクラス定義。
package jp.co.companyname.pjname.domain; public class Foo{ private String bar; static{ System.out.println("static block."); } public Foo(){ System.out.println("constructor."); } public Foo(String bar){ this.bar = bar; } public String getBar(){ return this.bar; } public void setBar(String bar){ this.bar = bar; } }
この「Foo」クラスを生成するやり方は幾つかあります。まずはどの様な手段があるか見ていきましょう。
Class<?> fooClazz = Foo.class;
Foo fooInstance = fooClazz.newInstance();
Foo foo = new Foo();
Class<?> fooClazz = foo.getClass();
Foo fooInstance = fooClazz.newInstance();
Class<?> fooClazz = Class.forName("jp.co.companyname.pjname.domain.Foo");
Foo fooInstance = fooClazz.newInstance();
Class<?> fooClazz =
Thread.currentThread().getContextClassLoader()
.loadClass("jp.co.companyname.pjname.domain.Foo");
Foo fooInstance = fooClazz.newInstance();
どの例も「Foo」クラスの定義情報を取得し、そこから新しいインスタンスを作成しているわけですね。
違いは「Foo」のクラス定義情報をどうやって取得しているか、な訳です。
さて、1つ目と2つ目はほぼ使う事もないので飛ばすとして、3つ目と4つ目の違いを見ていきましょう。
ぱっと見た限り、3つ目と4つ目は、「Class.forName("X")」で「Class<?> fooClazz」を読み込んでいるか、「ClassLoader#loadClass("X")」で「Class<?> fooClazz」を読み込んでいるわけです。
それ以外に違いはないのか?と思いますよね。ということでもう少し掘り下げていきましょう。
動作を確認するために自前のクラスローダを用意します。これを使って違いを見ていきましょう。
FooClassLoader extends ClassLoader{ @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ System.out.println("load class."); return super.loadClass(name, resolve); } }
Class<?> fooClazz =
Class.forName("jp.co.companyname.pjname.domain.Foo");
System.out.println("new instance.");
Foo fooInstance = fooClazz.newInstance();
実行結果: static block. new instance. constructor.
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo"); System.out.println("new instance."); Foo fooInstance = fooClazz.newInstanace();
実行結果: load class. new instance. static block. constructor.
ということで、Class#forName("X")を経由してクラス定義をロードした場合は、staticブロックが即時に初期化され、ClassLoader#loadClass("X")を経由してクラス定義をロードした場合は始めてクラスが利用されたときにstaticブロックが初期化されるわけですね。
実際にstaticブロック満載なコードは少ないと思いますが、気を付けるならClassLoader#loadClass("X")の方が安全でしょうし、自前でClassLoader作るのはちょっと・・・っていう状態であれば「Thread.currentThread().getContextClassLoader()」で取ってきたClassLoaderを使えばまぁ基本問題ないでしょう。
さて、単純な状態のクラスのインスタンスは今までの「Class#newInstance()」で取れますが、FooクラスのStringを1つ引数に受けるコンストラクタを呼び出してインスタンス化することはできません。
勿論、そういったクラスのインスタンスを生成する機能もありますので、次はそちらを見ていきましょう。
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo"); Constructor[] constructors = fooClazz.getConstructors(); Foo fooInstance = null; for(Constructor constructor : constructors){ if(constructor.getParameterTypes().length == 0) fooInstance = constructor.newInstance(); if(constructor.getParameterTypes().length == 1) fooInstance = constructor.newInstance("bar"); }
はい、見ての通りClass型にはコンストラクタを取得する「Class#getConstructors()」というメソッドが用意されていて、そちらを使っていく形になります。
Class#getConstructors()を呼ぶと全てのコンストラクタが取れてしまうので、ifで分岐している訳ですね。もうちょっとスマートに書くと
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo"); Constructor constructorNoArg = fooClazz.getConstructor(); Foo fooInstanceDefaultConstructor = constructorNoArg.newInstance(); Constructor constructorStringArg = fooClazz.getConstructor(String.class); Foo fooInstanceString1Constructor = constructorStringArg.newInstance("bar");
のように書くことができ、引数なしのコンストラクタはClass#newInstance()と同じ挙動になるわけです。
今回はClassLoader#loadClass("X")でクラス定義を読み込んでいるので、staticブロックはConstructor#newInstance()が呼ばれたタイミングで実行されます。
さて、もうちょっと先に進みましょう。
package jp.co.companyname.pjname.domain; public class Foo{ private String bar; static{ System.out.println("static block."); } public Foo(){ System.out.println("constructor."); } private Foo(Integer bar){ this.bar = bar.toString(); } public Foo(String bar){ this.bar = bar; } public String getBar(){ return this.bar; } public void setBar(String bar){ this.bar = bar; } class InnerFoo{ } }
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo"); InnerFoo innerFooInstance = fooClazz.newInstance();
さて、InnerFooのインスタンスが作れそうに思えますが、実際に動かしてみると作れないんですよね。
実はFooクラスをコンパイルする際にInnerFooクラスにFooクラスを1つ受け取るコンストラクタが合成メソッドとして自動で生成されていて、それを使わないとインスタンスを生成できないわけです。
なぜInnerFooにコンストラクタが追加されるかというと、InnerFooはインナークラスですから、Fooの外からは見えないわけですね。
これを解決するのに合成メソッド(のコンストラクタ)が生成されるってことですね。
前置きが長くなりましたが、実際にインスタンス化してみましょう。
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo"); Constructor innerFooConstructor = fooClazz.getDeclaredConstructor(Foo.class); InnerFoo innerFooInstance = innerFooConstructor.newInstance(new Foo());
ということで、これでインスタンス化ができましたね。
さて、実は、Clsss型は誰がエンクロージングなのか(インナークラスの大元のクラスなのか)を知っているので、それを使ってもうちょっと固有のコードを減らしていきましょう。
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo"); Constructor innerFooConstructor = fooClazz.getDeclaredConstructor(fooClazz.getDeclaringClass()); InnerFoo innerFooInstance = innerFooConstructor .newInstance(fooClazz.getDeclaringClass() .newInstance());
綺麗になりましたね。今回はインナークラスでしたが、無名クラスも同様ですので、Class#getEnclosingConstructorやClass#getDeclaredConstructorを使ってインスタンスを生成しましょう。
ちなみに、判定を盛り込んだらこんなかんじになります。
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo"); if(fooClass.isMemberClass()){ Constructor innerFooConstructor = fooClazz.getDeclaredConstructor(fooClazz.getDeclaringClass()); InnerFoo innerFooInstance = innerFooConstructor .newInstance(fooClazz.getDeclaringClass() .newInstance()); } else if(fooClass.cz.isAnonymousClass(){ //今回は省略 } else{ fooClazz.newInstance(); }
ついでにprivateなコンストラクタの呼出しもやってしまいましょう。
FooClassLoader loader = new FooClassLoader(); Class<?> fooClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Foo.Foo"); Constructor fooConstructor = fooClazz.getDeclaredConstructor(Integer.class); fooConstructor.newInstance(1);
Class#getDeclaredConstructorでprivate(protected)なコンストラクタが取れますので、これでいけるか?って思いますがやっぱりいけません。IllegalAccessExceptionが飛ぶわけですね。一般的な実行環境であれば
fooConstructor.setAccessible(true); fooConstructor.newInstance(1);
としてあげると、アクセスが許可され、private(protected)なコンストラクタでもリフレクション経由で呼び出すことができます。
ただし、SecurityManagerに設定がされている場合はその限りではありませんので、javaを起動している実行ファイル(の引数)をチェックしましょう。起動オプションが「java (省略) -Djava.security.manager -Djava.security.policy=ポリシファイル 対象クラス (省略)」の形になっていれば、ポリシファイル次第では上記のコードは動かなくなります。ポリシファイルの構文はここにルールがありますので、リフレクションで何か作ったあと環境違いで動かなくなったとき、起動オプションでセキュリティ設定している場合はリンク先を参照して、コードを直すかポリシファイルを書き換えるかしましょう。
さて、これで設定ファイル(xmlなど)からクラス名と引数を受け取ればインスタンスを生成する機能が作れるかな?と思いますが・・・もうちょっと続きます。そうGenericsを忘れていますね?ちょっと難しいですが、見ていきましょう。
package jp.co.companyname.pjname.domain; public class Bar<T extends Set<V>,V>{ private T uniqueList; private List<V> allList; private String name; public Bar(T uniqueList, List<V> allList){ this.uniqueList = uniqueList; this.allList = allList; } public Bar(V allList){ this.allList = allList; } public Bar(String name, List<V> allList){ this.name = name; allList.forEach(new Consumer<String>() { @Override public void accept(String t) { if(!uniqueList.contains(t)) uniqueList.add(t); } }); } }
はい、まずこのコンストラクタを取るところからして困難なわけですね。コンストラクタが1つしかない場合はClass#getConstructors()[0]ってやればいいのですが、2つ以上になったらお手上げです。
さて、困ったなぁと思う前に当初の目的に戻りましょう。基本的にメタ的なプログラミングをするのであれば、spring frameworkにあるようなapplication-context.xmlみたいな、定義(設定)ファイルがあるわけですよね。実際にメタ的なハコはリフレクションで作る訳ですが、何のクラスで動作するかは設定ファイルに書くわけです。ここのBarクラスでいうと・・・
List<String> allList = new ArrayList<String>(); Set<String> uniqueList = new HashSet<String>(); allList.forEach((t) -> { if(!uniqueList.contains(t)) uniqueList.add(t); }); Bar<Set<String>, String> t = new Bar<Set<String>,String>(uniqueList,allList);
みたいな形ですね。TとVの部分に当たるものは、「T = Set」「V = String」なわけです。これらの情報は事前に知っていて、TとList<V>を受け取るコンストラクタを経由してインスタンスを生成するという前提の元、リフレクションでインスタンスを生成できるかやってみましょう。
Set<String> arg1 = new HashSet<String>(); List<String> arg2 = new ArrayList<String>(); FooClassLoader loader = new FooClassLoader(); Class<?> barClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Bar"); Constructor barConstructor = barClazz.getConstructor(arg1.getClass(), arg2.getClass()); Bar barInstance = barConstructor.newInstance(arg1,arg2);
はい、これだけだと案外簡単ですね。ではもう少しコンストラクタを扱うのが難しいドメインオブジェクトを見てみましょう。
package jp.co.companyname.pjname.domain; public class Hoge<X extends Map<K.V>,Y extends ConcurrentMap<K,V>,K,V>{ public Hoge(X x){ System.out.println("x constructor"); } public Hoge(Y y){ System.out.println("y constructor"); } }
さて、まずは普通にコードを書いたときの状態を見てみましょう。
Hoge<Map<String,Integer>,ConcurrentMap<String,Integer>,String,Integer> hoge =
new Hoge<>(new ConcurrentSkipListMap<String,Integer>());
実行結果:
y constructor
generics引数1を受けるコンストラクタHoge(X x)、generics引数2を受けるコンストラクタHoge(Y y)があります。
継承構造はMap→ConcurrentMap→ConcurrentNavigableMapとなっていて、ConcurrentNavigableMapの実装がConcurrentSkipListMapになります。
よって、ConcurrentSkipListMapはgenerics引数1、2どちらにも当てはまるのでHoge(X x)とHoge(Y y)のうち、直近の実装が近い方であるConcurrentMapの方、Hoge(Y y)が呼ばれている訳ですね。
もちろん、Hoge(X x)の方にもタイプ値としては当てはまるわけですから、コンストラクタさえ取れれば使えます。
ということで、Hoge(X x)の方を呼び出すコードを考えてみましょう。
Integer[] constructorGenericsIndex = {0};
FooClassLoader loader = new FooClassLoader();
Class<?> hogeClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Hoge");
Map<String,String> hogeArg1 = new ConcurrentSkipListMap<>();
TypeVariable<?>[] hogeGenericTypeVariables = hogeClazz.getTypeParameters();
List<TypeVariable<?>> constructorTypeVariables = new ArrayList<>();
for(Integer index : constructorGenericsIndex){
if(index > hogeGenericsTypeVariables)
//指定したHogeのgenerics引数のインデックスが実際の数を超えていたら無視する
continue;
constructorTypeVariables.add(hogeGnericsTypeVariables[index]);
}
Constructor hogeConstructor = null;
for(Constructor each : hogeClazz.getConstructors()){
Type[] genericsTypeParameters = each.getGenericParameterTypes();
if(genericsTypeParameters.length != constructorTypeVariables.length)
continue;
boolean isMatch = true;
for(int i=0;i<genericsTypeParameters.length;i++){
TypeVariable<?> param = (TypeVariable<?>)genericsTypeParameters[i];
TypeVariable<?> constructorTypeVariable = constructorTypeVariables[i];
isMatch &= param.getName().equals(constructorTypeVariable.getName());
}
if(isMatch)
hogeConstructor = each;
}
if(hogeConstructor==null)
throw new InstantiationException();
Hoge hogeInstance hogeConstructor.newInstance(hogeArg1);
実行結果:
x constructor
さくっと書きましたがこんな感じでしょうか。Class<Hoge>からは、Hogeの型引数がClass#getTypeParameters()で取れるので、このコードでは「0番目」のgeneric引数をコンストラクタとして受け取るメソッドを探してそれを使っているわけですね。なお、今回のコンストラクタはgenerics型しか受け取らないため、Constructor#getGenericParameterTypes()の結果「Type[] genericsTypeParameters」は全て「TypeVariable<Class<?>>」でしか返ってきませんが、通常の型が混じっている場合(StringやList<V>など)、ParameterizedType(実際はParameterizedTypeImpl)等も含まれるので、instanceof等を使って上手く分岐しましょう。
さて、genericsも終わったところで案外使うEnum型にいってみましょう。まずはenumを宣言しましょう。
public enum FooEnum{ hoge1(1), hoge2(2), hoge3(3), ; private Integer id; public static final FooEnum defaultValue = hoge1; public FooEnum(Integer id){ this.id = id; } public id(){ return id; } }
次にこのEnumを実体化してみましょう。
String configValue = "hoge2"; Enum<?>[] enums = FooEnum.class.getEnumConstants(); //enumのconstantsのみ取れる。ここではdefaultValueは入ってこない for(Enum<?> each : enums){ if(configValue.equals(each.name()) return each; }
enumの定数のみはClass#getEnumConstants()で取得できるので、これで全て取ってきます。
ここで、Enum#name() で実際の FooEnum.hoge1.name() とした値が取れるので、これを使って判別しましょう。
今回は設定値であるconfigValue = "hoge2" と一致したものを返してる訳です。
さて、これで大まかに使いそうなところは網羅できたと思います。しかし、これらを毎回コードに書くのは非効率ですし手間もかかるのでユーティリティとして1つのクラスに纏めてみましょう。
public class ReflectionUtil{ private ClassLoader _loader; private ClassLoader _defaultLoader = Thread.currentThread().getContextClassLoader(); private Class<?> nullType = Object.class; public void setClassLoader(ClassLoader _loader){ this._loader = _loader; } public ClassLoader getClassLoader(){ return this._loader; } private ClassLoader getLoader(){ if(_loader!=null) return _loader; return _defaultLoader; } private void setAccessible(final AccessibleObject target, boolean flag){ if(!target.isAccessible()) AccessController.doPrivileged(() -> { target.setAccessible(flag); return target; }); } private Class<?> loadClass(String className) throws ClassNotFoundException{ return getLoader().loadClass(className); } private Object[] addParam(Object[] base, Object opt){ Object[] dest = new Object[base.length+1]; System.arraycopy(base, 0, dest, 0, base.length); dest[base.length] = opt; return dest; } public <T> T newInstance(String className, Object...params) throws ClassNotFoundException,InstantiationException,NoSuchMethodException, InvocationTargetException,IllegalAccessException{ return newInstance(loadClass(className),params); } public <T> T newInstance(Class<?> clazz, Object...params) throws ClassNotFoundException,InstantiationException,NoSuchMethodException, InvocationTargetException,IllegalAccessException{ List<Class<?>> typeParameters = new ArrayList<Class<?>>(); boolean isVoidParameter = params == null || params.length == 0; boolean isInnerClass = clazz.isMemberClass() || clazz.isLocalClass() || clazz.isAnonymousClass(); if(!isVoidParameter){ Arrays.asList(params).forEach((each) -> { if(each==null){ typeParameters.add(nullType); return; } typeParameters.add(each.getClass()); }); if(isInnerClass){ boolean isDeclaringClass = clazz.isMemberClass(); boolean isEnclosingClass = clazz.isAnonymousClass() || clazz.isLocalClass(); List<Class<?>> enclosingTypeParameters = new ArrayList<Class<?>>(); if(isDeclaringClass) enclosingTypeParameters.add(clazz.getDeclaringClass()); else if(isEnclosingClass) enclosingTypeParameters.add(clazz.getEnclosingClass()); enclosingTypeParameters.addAll(typeParameters); Constructor<?> constructor = null; if(isDeclaringClass) constructor = clazz.getDeclaredConstructor((Class<?>[])enclosingTypeParameters.toArray()); else if(isEnclosingClass) constructor = clazz.getEnclosingConstructor(); setAccessible(constructor, true); Object enclosingInstance = null; if(isDeclaringClass) enclosingInstance = newInstance(clazz.getDeclaringClass()); else if(isEnclosingClass) enclosingInstance = newInstance(clazz.getEnclosingClass()); Object[] enclosingParams = addParam(params,enclosingInstance); return (T)constructor.newInstance(enclosingParams); } else if(clazz.isPrimitive()){ //short if(Short.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Short.valueOf((short)0); return (T) Short.valueOf(params[0].toString()); } //int if(Integer.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Integer.valueOf(0); return (T) Integer.valueOf(params[0].toString()); } //long if(Long.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Long.valueOf((long)0); return (T) Long.valueOf(params[0].toString()); } //float if(Float.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Float.valueOf(0); return (T) Float.valueOf(params[0].toString()); } //double if(Double.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Double.valueOf(0.0); return (T) Double.valueOf(params[0].toString()); } //byte if(Byte.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Byte.valueOf((byte)0); return (T) Byte.valueOf(params[0].toString()); } //char if(Character.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Character.valueOf((char)0x00); return (T) Character.valueOf(params[0].toString().charAt(0)); } //boolean if(Boolean.TYPE.equals(clazz)){ if(isVoidParameter) return (T) Boolean.valueOf(false); return (T) Boolean.valueOf(params[0].toString()); } throw new InstantiationException(); } else if(clazz.isAnnotation()){ throw new InstantiationException(); } else if(clazz.isInterface()){ throw new InstantiationException(); } else if(clazz.isEnum()){ Enum<?>[] enumerations = (Enum<?>[])clazz.getEnumConstants(); for(Enum<?> each : enumerations){ if(isVoidParameter) return (T)each; for(Object param : params){ if(param != null && param.toString().equals(each.name())) return (T)each; } } throw new InstantiationException(); } else{ Constructor constructor = null; try{ constructor = clazz.getConstructor((Class<?>[])typeParameters.toArray()); } catch(NoSuchMethodException nsme){ constructor = clazz.getDeclaredConstructor((Class<?>[])typeParameters.toArray()); } finally{ if(constructor == null) throw new InstantiationException(); return (T)constructor.newInstance(params); } } } }
こんなかんじですかね。generics引数を受け取るクラスは動かないので別建てでメソッドを用意する必要がありますが、大体の印象は伝わりましたかね?実際はエラー処理やら、次回以降でやるAnnotationやProxyを使ってインスタンスも作れるので、色々変わってくるとは思います。
ついでに、実際にどんな感じになるか書いてみましょう。
<?xml> <bean id="foo1" class="jp.co.companyname.pjname.Foo" /> <bean id="foo2" class="jp.co.companyname.pjname.Foo"> <constructor-args> <constructor-arg class="java.lang.String" value="bar" /> </constructor-args> </bean>
String id = CUstomXmlParser.getId();
String className = CustomXmlParser.getClassName();
Object[] params = CustomXmlParser.getConstructorArguments();
Object bean = ReflectionUtil.newInstance(className, params);
...
こんな感じですね。CustomXmlParserは書くとかなり長くなってしまいますので省略しています。
ということで、長くなりましたがインスタンス生成については以上です。時間があるときに最後のユーティリティのところはちょこちょこ直すかも知れません。(動作確認もしてないですし・・・w)