アーキテクトとステークホルダ
アーキテクトの仕事とは何かというと、「ステークホルダの観点を集約し適切なデザインをする」こと。
これはinformation architectだろうが、application architectだろうが同じ事で、デザインの対象が情報設計なのかアプリケーションの設計なのかの違いだけ。
まあ、同一のシステムを対象としてればほとんど同じなんだろうけど。
で、ある事業がコンシューマ向けのWebアプリケーションをローンチしたとして、そのアプリケーションのアーキテクトは何をしなきゃいけないのかということをちょっと整理する。
まず、「第一にアークテクチャとは何の為にあるのかということ。」を明確にすること。
それはほとんどの場合「ステークホルダの利益のため」。
時々アーキテクトの趣味をそのままアーキテクチャに反映させるだけの新規技術オタクがいるけど、それはちょっといただけない。
で、このステークホルダの洗い出しとヒアリングを繰り返して適切なデザインを提案していくのがアーキテクトの仕事になる。
ヒアリングというのは、ある程度の裁量権をもって、方針を決定する事ができる側が行う事であるから「相談」や「お伺い」ではない。
弁護士に法廷戦略について、提案されることはあっても相談される事は無いのと同じで、それだけの信頼関係が要求される。
逆にアーキテクトは、このステークホルダのなかで業務領域をよく知る人に対して十分なリスペクトをもって信頼関係を築いている必要がある。
たとえば、ステークホルダには以下のような部門が考えられる。
- 運用部門
- 顧客サポート部門
- 営業部門
- 開発部門
- 製品企画部門
- PR/宣伝部門
- 経理/法務部門
- 情報セキュリティ部門
などなど。
それぞれのニーズや願望、展望などをヒアリングし設計を行うのだが、残念ながらそれらすべてかなえられる訳ではないし、要求していることが本当に達成したい事とも限らない。
ここで重要なのは、願望や展望から機能要求と非機能要求を見いだし、それをある程度明示的にアーキテクチャ要素に切り出していく事だ。
特に非機能要求は、明示的に要求されなかったり、横断的関心として浮かび上がってきたりする為、意識的/反復的にアーキテクチャ上の非機能要求を精査していく必要がある。
代表的な非機能要求には次のようなものがある:
- セキュリティ
- 可用性
- 耐障害性/障害抑止性
- 運用性
- 保守性/解析性/変更性
- 使用性
- テスタビリティ
- パフォーマンス
- スケーラビリティ
などなど。
これらのことを一人で出来るのはまあ、理想的なのかもしれないが、こういった要求についても各分野のスペシャリストに委譲していくことも組織としては大事。
本当のハイテク企業になるために
以前、ポールグレアムが、「Yahooに起きてしまった事」として、テクノロジー企業としてスタートしたものが徐々にメディア企業としてハッカー文化が摩耗していくといった話をしていた。
Webテクノロジが本当にハイテクだったかはさておき、コミュニケーションのためのWebサービスを開発するという事は、単純なWebサイトを構築したりどこかから拾ってきたCMSをいじったりするのとは本質的に違う。
雑誌を編纂するようにエディトリアルに価値の本質があるわけではないし、コンテンツプロバイダーのようにクリエイティブそのものに価値の本質があるわけではない。
Webサービス企業にとっての本質的価値とはサービスしているソフトウェアのドメイン/構造そのものだ。
サービスしているソフトウェアのドメイン/構造そのものに本質的価値をおく企業は、その表面的なプレゼンテーション層に価値のメインストリームをおかない。
たとえばそれは、Googleにとってはsearchであって、Facebook/Twitterにとってはsocial graph と friend feedであったりする。
このアプリケーションソフトウェアのドメイン/構造に本質的価値をおく企業をこれからのWebサービス企業の形だとすれば、プレゼンテーション層やプラットフォームというものは販促や小売、流通、営業であって、商品開発ではない。
それは決してプレゼンテーション層/プラットフォームに価値が無いことをいいたいのではない。戦略としてプレゼンテーション層に力を入れる事も消して間違いではない。
レイヤが違うという事を(エグゼクティブ、スタッフ、組織が)認識しているかどうかだ。Webサービス、WebアプリケーションとWebページの違いを本当にわかっているのかどうかだ。
それは、販売のフィードバックを商品開発部門がうけとることがあっても、販促部が直接商品開発をすることはないようなものだ。
こういった企業にとって、ソフトウェア開発計画とは事業計画/人事計画でもある。
事業計画/人事計画のようなというのは、開発行程とそのリソース配分そのものがビジネスの進捗に直結するからであるし、
インフラコストと人件費がコストの中心となる企業にとって、コスト改善の為の幾多の意思決定がソフトウェア開発には内在しているからだ。
技術的負債という言葉がある。
技術的負債(英: Technical debt)とは、 行き当たりばったりなソフトウェアアーキテクチャと、 余裕のないソフトウェア開発が引き起こす結果のことを指す 新しい比喩である。「設計上の負債(design debt)」とも言う
というものだ。ソフトウェア開発計画の方針が簡単にぶれてしまえば、事業計画の方針がぶれてしまったときにコストが発生するようにソフトウェア開発にとってもコストが発生してしまう。
製造業が、中国に工場を造るという事業計画を立てて進捗していたが、途中で国内工場の増産にシフトするといった場合、その変更コストは試算され、しかるべき意思決定者によってそのコストが受容される事になるが、ソフトウェア開発企業ではなかなかそうはいかない。
ソフトウェアの技術的負債は、定量化しづらく、コストの試算がきわめて難しい。それはソフトウェア開発工数/工期の見積もりが安定しないのと同じで、複雑系の中にあるものをブラックボックスとして評価する類いのものだからだ。
そして、ソフトウェアアーキテクチャとは、ソフトウェア開発計画に基づいて、適切な抽象化構造をソフトウェアに提供してくことなのだが、この抽象化構造を適切な意思決定者に不可視である場合、この負債の事前評価はさらに難しくなる。
ある意思決定を行った事が、つぎの意思決定への多大なコストを生みうるからだ。
ソフトウェア開発では、個人的な予知に基づいた設計はタブーとされる。不当に複雑な抽象を与えてしまう可能性があるからだ。予知ではなく、現在と計画において適切な構造を提供することが重要だ。
その構造のインクリメンタルな発展であれば、修正コストは最小限に保てる。その構造そのものを別モノに変換するのであればそれが中核的な抽象であればあるほど修正コストは肥大化する。
それは他の業界であれば、
いままで半導体工場を提供していた企業が、その工場でようかんを製造すると方針転換するといえば想像がつくコスト増なのだが、アーキテクト不在のソフトウェア企業であれば、このコストですら秘匿されたものとなってしまう。
これらの可視化と評価はきわめて難しいことだが、
ソフトウェア企業がさらに発展していく為の重要なファクターとなる。
本日発売のWEB+DB PRESSのPerl Hackers Hubにて、技術的負債の定量的評価とアーキテクチャパターンを簡単ながら扱っている。
ぐだぐだと書いたが結局のところただの宣伝なのだ。
position:fixedのサポートを検査する
android端末のwebkitや普通のブラウザはposition:fixedをサポートしているけどiOSのwebkitは何故かサポートしていないので、うざい。うざいけど判別しなきゃ行けないのでsnippet
var isSupportedPositionFixed = (function (body){ var element = document.createElement('div'); element.innerHTML = 'x'; element.style.position = 'fixed'; element.style.top = '0px'; body.appendChild(element); var bodyScroll = body.scrollTop; body.scrollTop = 100; var elementTop = element.getBoundingClientRect().top; var isSupported = (elementTop === 0) ? true :false; body.removeChild(element); body.scrollTop = bodyScroll; return isSupported; })(document.body); console.log(isSupportedPositionFixed);
解説としては、fixedのエレメントを使って、少しbodyをscrollしてgetBoundingClientRectで実際の位置を取得して、動いていないかどうか確認するという感じ。
memo: monad in javascript
(function(){ //unit: a -> M[b] var unit = function(value){ return { value : function(){return value} }; }; //bind: M[a] -> (a->M[b]) -> M[b] var bind = function(m,proc){ return proc( m.value() ); }; // func a->M[b] var x2 = function(x){return unit(x*x)}; var inc = function(x){return unit(x+1)}; //return a >>= f ≡ f a print( bind( unit(10) , x2 ).value(), x2(10).value()); //m >>= return ≡ m print( bind( unit(10) , unit ).value(),unit(10).value()); //(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g) print( bind( bind( unit(10) , x2 ) ,inc ).value(), bind( unit(10) , function(x){ return bind( x2(x) ,inc)}).value() ); })();
評価とか型とかないからあれだけど、
一番単純な形だとこんな雰囲気になるはず。
これだとなんの役にも立たないので、構造を覚えていて
valueが呼ばれるまで評価しないmonadを作ってみる。
(function(){ //unit: a -> M[b] var unit = function(value){ var ret = (value.call) ? value : (value.value) ? value.value : function(){return value} return { value : ret }; }; //bind: M[a] -> (a->M[b]) -> M[b] var bind = function(m,proc){ return unit(function(){ return proc( m.value() ).value() }); }; // bindが逐次実行じゃなくて評価前の状態を保存する // ように作る。 // func a->M[b] var x2 = function(x){return unit(x*x)}; var inc = function(x){return unit(x+1)}; print( unit(10).value()); print( unit(unit(10)).value()) print( bind(unit(10),x2).value()); print( bind( bind(unit(10),x2),inc).value()); //return a >>= f 竕。 f a print( bind( unit(10) , x2 ).value(), x2(10).value()); //m >>= return 竕。 m print( bind( unit(10) , unit ).value(),unit(10).value()); //(m >>= f) >>= g 竕。 m >>= (\x -> f x >>= g) print( bind( bind( unit(10) , x2 ) ,inc ).value(), bind( unit(10) , function(x){ return bind( x2(x) ,inc)}).value() ); var process = [inc,inc,x2,x2,inc]; var m = unit(1); process.forEach(function(e){ m = bind( m , e ); }); print("before evaluate"); print(m.value()); print("afater evaluate") })();
Array.prototype.flatMapを実装するとArrayもmonadに。
Array.prototype.flatMap = function(f){ var ret = []; this.forEach(function(e){ ret = ret.concat( f(e) ); }); return ret; }; //unit: a -> M[b] var unit = function(value){ return [value]; }; //bind: M[a] -> (a->M[b]) -> M[b] var bind = function(m,proc){ return m.flatMap(proc); }; // func a->M[b] var x2 = function(e){return unit(e*e)}; var inc = function(e){return unit(e+1)}; //return a >>= f == f a print( bind( unit(10) , x2 ), x2(10)); print( [10].flatMap(x2),x2(10)); //m >>= return == m print( bind( unit(10) , unit ),unit(10)); print( [10].flatMap(function(e){return [e]}), [10]); //(m >>= f) >>= g == m >>= (\x -> f x >>= g) print( bind( bind( unit(10,1) , x2 ) ,inc ), bind( unit(10,1) , function(x){ return bind( x2(x) ,inc)}) ); print( [10].flatMap(x2).flatMap(inc), [10].flatMap(function(x){ return x2(x).flatMap(inc)}) );
世代間認知不協和
コミュニケーションを媒介するサービスにとって、そのコミュニケーションがどのように育まれているかという点について深い知見を得るというのはとても重要なことだ。コミュニケーションのスタイルというのは日々変化するもので、何が正しくて何が正しくないという知見は簡単に変化してしまう。この変化をとらえていくということは非常に難しい。
最近、Willcomが誰とでも定額、連続通話時間が10分以内の電話はすべて定額の範囲に収まるというプランを発表した。Willcom内通話の定額は以前からあって、2時間45分とかそのあたりで定額がいったん打ち切られるといったようなものだったと記憶している。
ソフトバンクやウィルコムのケータイの無料電話が流行るにつれて、一部の中学生たちが面白い使い方をしているという話を聞いたことがある。学校から帰るとすぐに中のよい友人と通話状態にして、とくに話すことがなくても常につなぎっ放しにしているというようなものだった。
思えば、自分の世代は小中のころは、FAXがそういった役割を担っていた。中のよい友人とFAXでいろいろなやり取りをしていた。当然通話料がかかるが、電話をつなぎっぱなしよりも継続的なコミュニケーションがとれた。FAXを欲しいとこのとき以上に思ったことはなかった。高校のときは、携帯メールで、いつのまにか知らない話題が周囲で共有されていることに気づき、またも携帯がほしくなったものだった。大学時代のコミュニケーションの中心はmixiとWindows Messengerだった。Skypeをつなぎっぱなしで、家にいながら友人と飲み会をしたのは大学院の頃。
それぞれのメディアで、「あるあるネタ」が生まれては消えていった。それはその世代そのときにしか感じられない経験であった。そういったコミュニケーションを介在するサービスに関わりたいと思ったのは、そういったさまざまな原体験に基づいているのかもしれない。
先の中学生のコミュニケーションスタイルを自分は追体験できていない。NintendoDSですれ違い通信をしたり、公園でバトルしている子供たちに自分はなることはできない。モンハンを同僚と楽しむのとはまた別の何かがあるはずだと思う。
同じように自分の体験を前の世代が体験することができていない。そこに認知不協和を呼ぶことがあるのだろう。実際年を取るにつれて、今現在の若いユーザのコミュニケーションのあり方を認識するのが難しくなるという感覚はある。そこに恐怖がある。自分の感覚の貯金を切り崩していく感覚だ。
30代、40代で今現在をとらえている人はとても少ない。そういった希有な人に決まって聞くのはどうやって、今現在をとらえているんですか?という質問だ。答えも要約すれば一つに決まっている。若い人と話すことだ。と。
これには少しばかりの反駁も合った。もう少し論理的なアプローチがあるのではないか。分析的、定量的に今現在を知るというアプローチもあるのではないかと。大筋は今でもそう思っている。だが、実際の所どうだろうか。統計は因果関係を表すものではないのだから、これにも限界があることは少し考えればわかることだ。エビデンスを積むことはできても、そもそもその契機となる仮説構築プロセスは感覚や体験によるものだ。
気がつけば、高校生から10年経っている。貯金がつきるのもカウントダウンが始まっている。このカウントダウンが終わった後、自分の手に残っているものを考えて、日々電車に揺られている。
問題解決と眼
近頃は後輩から、「よいプログラムをかけるようになりたいです」という話を聞く。よいプログラムを書きたいという意識それ自体、とてもすばらしいことだと思った。そのときにふと、なにをもって「よい」とするのかといったことについて普段意識することは少ないのではないかと思った。自分自身も仕事としてプログラミングを行う前までは、何を持って「よい」とするのかといった基準のようなものは持ち合わせていなかった。説明のしにくいもやもやであって、それは美とは何か真とは何かといったような文化人類学的なあるいは審美的なものだというような意識すらあった。しかし、これは間違いだ。
極論すれば、あらゆる前提から逃れて「よい」プログラムなどないんだよ。と俺は後輩に言った。拡張性があるとか、読みやすいだとか、ドキュメンティッドであるとか、そういうものをそのままに「よい」というものじゃない。それがある前提に基づいた問題解決策だから、その前提のもとに「よい」という判断ができるだけだ。解決するべき問題の設定、そのモデル化、開発時の状況などによって、問題系決策は束縛されていて、前提とのマッチングによって、それが「よい」だとか悪いだとか導ける。
前提なしの拡張性なんて、入力文字列をevalするプログラムがもっとも拡張性高いことになってしまうしね。それはモデル化にしても同じで、どういう課題に対するどういうモデリングなのかによって、様相が全然違ってしまう。よくあるリンゴやみかんをインスタンシエートするオブジェクト指向のサンプルの空虚なところは、問題設定なしのモデル化だからだ。
とはいえ、よいプログラマ・よいエンジニアというのは確かに存在していて、それは「よい」プログラムを造り出す。彼らは何が「よくない」プログラマと違うのだろうか。知識や経験といえばそれまでなのだが、同じような経験を積んでいてもその成長は大きく違う。地頭のよさだろうか。それも関係はあるのかもしれないが、抽象的すぎる。僕が考えるところの決定的な違い:それは「よい」プログラマには問題解決のための眼があり、よくないプログラマはそれがない。ということだ。眼が経験を有意義にも無為にもする。眼が状況を判断し、自分がおかれている前提を正しく把握した設計を生む。この問題解決のための眼を養うことが、よいプログラマのための条件なのではないかと思っている。
問題解決のための眼とはなにか。それは「視野」「視座」「視点」の3つに分類できるんじゃないだろうか。この3つは混同されがちだが、明確に意味が違う。
「視野」とは、あるポイントからその問題を眺めたときに同時に把握できる領域の広さのことだ。ある問題はある大きな問題に包含されていて、さらに大きな問題構造に含まれているといったことを把握できる眼。広い/狭いで評価するもの。
「視座」とは、どこから眺めるか。高い/低いでとらえるもの。視野がいかに広くても、視座が低ければさらに次元の高い問題を認識できないし、視座が高すぎても抽象論に終始しミクロな解決策が浮かばないなどある。社長が現場感覚を理解しようとしたり、平社員が部長の立場からものを見てみるといったようなこと。組織の階層だけじゃなく、問題をどのように受け止めるかといった姿勢でもある。
「視点」とは、どの角度から見るか。鋭さ/多様さでとらえるもの。問題の構造を把握して、解決策の筋を刺すときに問題の捉え方によってはシンプルになることがある。普段は見えない角度から本質をえぐり出す力。あるいは男性が女性の立場から見てみたり、女性が男性の立場から見てみたりするようなこと。
視座が拡大縮小なら、視点はどの角度にライトを照らすのかといったこと。そのライトのてらす広さが視野。
ここまで読んで、それってどんなビジネスでもいっしょじゃね?と思ったんではないだろうか。そうプログラマだろうが何だろうが、現代において、知的労働をするというのは、問題解決のための方策の構築であって、そのための意思決定の積み重ねだ。
ところで、とりあつかう問題領域によっては、自分自身で意思決定がしづらい場面があるかもしれない。あるいは問題構造の把握が困難なケースもあるだろう。特に全く知らないビジネス領域に関してのシステム作りだとなおさらだ。そういった現場の場合には、優れた「耳」もよいエンジニアの条件となるのだけど、それはまた別の機会に。
めも:Sub::Future
継続を使わない場合のget_multiのまとめ方を検討している。
これから、〜〜するぞ!という状態のオブジェクト。つまり継続もどき。
sub hoge { my $self = shift my $cache = $self->get_cache;# この手前で処理を停止させたい if( defined $cache ){ return $cache; } my $db = $self->get_from_rpc;# この手前で処理を停止させたい $self->set_cache($db); return $db; }
こんなかんじのロジックを次のように組み替え。
sub hoge_future { my $self = shift; return future( $self,q<get_cache>,sub{ my $cache = shift; if( defined $cache ){ return $cache; } return future($self,q<get_from_rpc>,sub{ my $db = shift; set_cache($db); return $db; }); }); }
そうすると、
::is $self->hoge,$self->hoge_future->next->next;
二つが等価になる。
止めたいところでとめて、何をしようとしているのかわかるので、
それをあつめてget_multiしたりbulk_loadしたりできる。
my $memfuture = $self->hoge_future->next; my ($mem,$key) = @{ $memfuture->binded }; my $r = Memcached->get($key); $memfuture->next_with($r);
package Sub::Future; use strict; use warnings; use Scalar::Util qw/blessed/; use Data::Util qw/is_instance/; use base qw/Class::Accessor::Fast Exporter/; __PACKAGE__->mk_accessors(qw/function binded callback class_name method_name/); our @EXPORT = qw//; our @EXPORT_OK = qw/future/; sub future { my $self = shift; my $method = shift; my $callback = pop; my @args = @_; return Sub::Future->new( { binded => [ $self, @args ], class_name => blessed $self ? ref $self : $self, method_name => $method, function => $self->can($method), callback => $callback } ); } sub next { my ($self) = @_; my @binded = @{ $self->binded }; my $func = $self->function; return $self->next_with($func->(@binded)); } sub next_with{ my ($self,$result) = @_; return $self->callback->($result); } sub run{ my ($class,$future) = @_; my $value = $future; while( is_instance( $value,$class) ){ $value = $value->next; } return $value; } 1;