開発チームにアーキテクトがいないなと感じてしまうような、残念なコードスメルの例

まったく個人的なモチベーションの問題から、前回の最終更新から2年以上が経過してしまい、多くの読者のみなさんにはご心配をおかけいたしました。「プログラミングに関して調べたことや日々感じたことをメモとして残していきたいと思います。」というもともとの原点に立ち返って、あまり気負わずに、また今後も時々更新していけたらと思います。今までこのブログの主なテーマとして、JavaEEやSpringといったような、いわゆる業務開発で使われるような技術を中心としてきたわけですが、最近Springを使ったJavaの開発に(アーキテクトではなく)プログラマーとしてちょっと参加する機会があったので、その時気づいたこと、感じたことを書いてみたいと思います。

さて、皆さんはアーキテクチャやアーキテクトという言葉に対してはどのようなものをイメージするでしょうか。システムのセキュリティを確保するための方式であったり、大量のデータを短時間に処理する並列化の仕組みであったり、また、効率的な業務フローの構築だったり、ソフトウェアアーキテクチャという言葉はコンテキストによって実に様々な対象を意味します。実際には、ソフトウェアアーキテクトといってもさまざまな仕事があり、また、それぞれの分野において膨大な知識が必要なため、最近ではお医者さんのように専門の得意分野に特化せざるを得ないということもあるかもしれません。基本的には、アーキテクトはプロジェクトの成功を左右するような基本的な設計方針、技術の選択といったことに関わってきます。

ここでは特に、クラスやパッケージ、設定ファイルの分割といった開発作業に深くかかわってくる、プログラミングの基本指針のことを単にアーキテクチャと呼ぶことにします。自分の経験からも、このプログラミングに関わるアーキテクチャはプログラマーの開発生産性や、後々のコードの再利用性、保守性といった点に深くかかわってくる開発の最重要ポイントの一つであり、他のアーキテクチャ指針とともにプロジェクト成功のためには軽視できないものであると考えています。この意味において有効なアーキテクチャを構築するにあたっては、アーキテクトがアプリケーション開発チームのメンバーとして仕事をするか、少なくとも、深く関与するということが大切だと思います。アーキテクト自身もアプリケーション開発者のコードを少なくともレビューできる体制になっているべきだし、「現場のプログラマーの気持ちがわかる」状態が望ましいと思います。さらに、こういったプログラミングアーキテクチャは事前にあらかじめ決められるものではなく、繰り返しのリファクタリングを通して、繰り返し洗練していくというアジャイルなプロセスが必須であると思います。以前から私が「下流アーキテクト」という言葉を何回か使ってきましたが、少なくとも有効なプログラミングモデルを構築するにあたって、アーキテクトは重役椅子に座ってパワーポイントの資料を編集しているだけではなくて、実際に開発チームを率いるプログラマーでもあるべきなのです。(アーキテクトもプログラミングするべきか? - 達人プログラマーを目指して)

残念ながら、多くのプロジェクトにおいては、開発チームの中にこうしたアーキテクトがいないのでないかと感じることがあります。実際に、そういった事実は実際にソースコードを読んでいるといくつかの特徴から明らかになります。ここでは、アーキテクト不在を感じるソースコードの不吉な臭い(スメル)の例について、いくつか書いてみたいと思います。

間違った過剰なコンポーネント(プロジェクト)分割

初心者のソフトウェアアーキテクトでも、最初に考えるであろうことは、何とかして全体のコードを分割しなくてはならないということでしょう。モノリシック(一枚岩)なアーキテクチャがダメという考えがあるためか、とにかく、コードをいくつのコンポーネントに分割したくなります。全体のソースコードが多くのコンポーネントに分割されていれば、それだけでいい仕事をした気分になってしまうというところもあるかもしれません。*1
しかしながら、コンポーネントの分割には一定のコストが伴うということを忘れていはいけません。単にチェックアウトしてIDEの中で扱わなくてはいけないプロジェクトが多ければそれだけ面倒が増えますし、全体的なビルド時間も分割が増えるにしたがって急速に長くなる傾向があるため、CIの効率的な実行に支障が出ます。XP的な要件を満たすもっともシンプルな設計を目指すのであれば、どうして分割が必要なのかというメリットを考慮したうえで、分割は必要最低限にするべきだと思います。適切なコンポーネントの分割は簡単そうでいて、実は非常に難しいところがあると思いますし、実際、要件に合った最適な分割は何回かのイテレーションを経てようやく到達できるということも珍しくありません。一方、チームにアーキテクトが不在だと、以下のような形式的な分割の基準をあらかじめルールとして設定してしまい、結果として開発生産性を大きく下げるということになりがちです。

  • 画面ごとに分割する
  • 機能ごとに分割する
  • インターフェース、アノテーション、クラス、設定といったソースの種類ごとに分割する*2

こうした形式的な基準に従って分割した場合、一つの開発チームに割り当てられたプロジェクトの数が何百個になることさえあります。実際問題として、プロジェクトの数以上に問題なのは、こうした形式的な分割方法によって得られるのは多くの場合、密結合で凝集性の低いアーキテクチャであるということです。要するに、一見きれいにコンポーネントが分割されているようにみえるが、実際は関連した機能があちこちに分散し、実質はモノリシックなシステムと変わらない、結局は何も分割したことになってはいないという状態です。例として、Springのbean定義をxmlで記述しているとして*3、xmlのbean定義ファイルと実際のbeanの実装クラスを別々のプロジェクトに分割して管理するというルールを考えてみてください。当然beanの定義と実際のクラスは密接に依存し合っているので、通常は同時に更新する必要がありますし、別々のプロジェクトとしてビルドすることで常に不整合が発生することを避けられません。プログラマーの気持ちがわかっているアーキテクトであれば、最初からこんなルールにはしないし、むしろアノテーションを使ったりJavaソースを使ってbeanの設定とクラス定義を近くに配置するということを考えると思いますが、そうでないと、設定ファイルは別々のプロジェクトで管理するものという常識で考えてしまいます。

私としてはeclipseプロジェクトのようなビルドの単位となるコンポーネントの分割は最低限にすることをお勧めします。もちろん、分割するメリットとして、

  • 別々にビルド・デプロイできる(SOA的にコンポーネントごとに別々のチームで運用する場合など)*4
  • 依存関係を強制できる(データ層からプレゼン層のクラスを間違って参照できないなど)
  • ビューなど頻繁に変更される領域とドメインロジックなど安定した領域を分ける

といったようなことがありますが、比較的小規模なプロジェクトであれば、

くらいの分割からスタートするので十分なのではないかと思いますし、むしろ最初から過剰に分割された構造で進めるより、実際上ははるかに生産性が高いと思います。

フレームワークによって強制されたパラレル継承

これは、ずっと以前に侵略的なフレームワーク - 達人プログラマーを目指してで書いたことの繰り返しになりますが、フレームワークの規約によって一つの機能ごとに様々なクラスや設定を作成しなくてはならないルールになっている場合がよくあります。たとえば、

  • MusicTrackDao
  • MusicTrackModel
  • MusicTrackService
  • MusicTrackView
  • MusicTrackViewModel
  • musicTrack.jsp
  • JSPのタグ
  • 個々のbean定義
  • 個々の単体テストクラス

といったように、同じ名前で始まる大量のソースが存在していれば、パラレル継承のスメルを発しているといって間違いありません。*5

なお、関連してこのようなケースでは、レイヤーごとに値の入れ物として別々のクラスを定義することが強制されている場合も多いのですが、実際に多くの場合

musicTrackViewModel.setTitle(musicTrackModel.getTitle());
musicTrackViewModel.setTrackData(musicTrackModel.getTrackData());
...

のように、記述するコードの大半が単なる値の転記となることもあります。
これも、実際にプロジェクトにアーキテクトが参加していれば、そんなナンセンスな設計ルールにはしないはずなのですが、事前にアプリケーション開発チームとは別の標準フレームワークチームがルールを決めた場合など、このアンチパターンによく出会います。コンポーネント分割の場合と同様にたくさんのクラスを作成させることで、メンテナンス可能なアーキテクチャを作っているんだという気分になるという人もいるかもしれませんが、実際のところは、これも密結合・低凝集なアーキテクチャの原因となり、生産性を大きく低下させる元となります。大量の無駄なクラスを作成しなくてはならないために、初期の開発に時間がかかるという点に加えて、さらなる問題は「変更の分散」といったコードスメルの原因となることにありますね。つまり、後々のメンテナンスであちこちのファイルを同時に編集しなくてはならないということです。

もちろん、本当に継承が適切ならば継承を使えばよいのですが、JUnitやSpringMVCの昔のバージョンと今の最新バージョンを比較してみるとわかるように、最近のフレームワークでは共通のベースクラスを避ける傾向があると思います。*6

共通コードのプルアップがされていない

何らかのMVCのフレームワークでアプリケーションを作っているとして、以下のようなコードがあったとします。いったい、何が問題なのでしょうか。

/**
 * 共通ベースコントローラー
 */
public abstract class AbstractController {
    protected abstract void execute(RequestContext context);
...
}

/**
 * 返品ポリシーのリンクを描画するためのコントローラー
 */
public class ReturnPolicyController extends AbstractController {

    public static final String RETURN_POLICY_MODEL_ID;

    @Inject
    private ModelRegistory modelRegistry;

    protected void execute(RequestContext context) {
        ReturnPolcyViewModel viewModel = modelRegistory.findModel(ReturnPolcyViewModel.class,  RETURN_POLICY_MODEL_ID);
        if (viewModel == null) {
           throw new InvalidConfigrationException("No model found for " + RETURN_POLICY_MODEL_ID);
        }
 
        viewModel.setURL(URLConverter.convertURL(context, model.getURL()));
        .....           
 
        context.setViewModel(viewModel); 
    }
}

/**
 * 音楽のトラック情報を描画するためのコントローラー
 */
public class MusicTracksController extends AbstractController {

    public static final String MUSIC_TRACKS_MODEL_ID;

    @Inject
    private ModelRegistory modelRegistry;

    @Inject
    private MusicTracksDataSource musicTracksDataSource;

    protected void execute(RequestContext context) {
        MusicTracksViewModel viewModel = modelRegistory.findModel(MusicTrakcsViewModel.class,  MUSIC_TRACKS_MODEL_ID);
        if (viewModel == null) {
           throw new InvalidConfigrationException("No model found for " + MUSIC_TRACKS_MODEL_ID);
        }
 
        .....           

        context.setViewModel(viewModel); 
    }
}

...似たようなサブクラスが何百と存在

普通の感覚であれば、各サブクラスのコードの類似性に気づくと思います。共通のロジックを親クラスに移動(プルアップ)するなどして、重複を減らすべきでしょう。しかし、ここではイメージなので実際のロジックを省略していますが、実際に各画面ごとのロジックが記述されている場合、この類似性に気づかずに大量の似たようなクラスを作成してしまうことがあります。こうしたコードの類似性は実際にいくつかのサブクラスを作成してみないと気づかないことが多く、フレームワークチームから与えられたベースクラスは多くの場合重複コードの削減のためにあまり役立ちません。アーキテクトが開発にかかわっているのであれば、こうした重複を発見する都度リファクタリングによって取り除く努力をすべきだと思います。

手続き型のコード

これはこのブログでも何度か取り上げてきたことですが、Javaなどのオブジェクト指向の言語を使っているにも関わらず、ほとんど活用できていないというケースが結構あると思います。実際、そういったプロジェクトで作成されたコードをみると、

  • データとgetter、setterだけのクラス
  • すべてstaticメソッドからなるXXXUtilクラス*7、あるいは巨大なサービスクラス
  • ポリモーフィズム(多態性)が使えておらず、Enumや型によって似たようなswitch文の構造が繰り返し現れる

といったような手続き型の言語で設計したのと大差のない構造になってしまっています。これは、要件やチームのスキルによっては必ずしもアンチパターンとまでは言えないかもしれませんが、やはり、プログラミング言語のあるべき設計を使いこなせるアーキテクトがチームに存在していないのが原因の一つなのではないかと思います。

ところどころに散在する実験コードの痕跡

コードのアーキテクチャのスメルは、なにもスキルの低いプログラマーのみで構成されたチームで起こるとは限りません。非常にモチベーションがあって、やる気のあるプログラマーであれば、自然なこととして、いろいろと自分の新しいアイデアをどんどん試したくなります。それは、技術者としてはすばらしいことなのですが、業務アプリケーション開発のプロジェクトの顧客の要件を最短で満たすという制約された条件の中では、ある程度節度をもって計画的にやらないと問題となることがあります。実際、うまくいかなかったアイデアなどは未練を捨てて早期にクリーンアップするべきだと思いますが、実際上は時間の制約からか、あるいはプログラマーが入れ替わったからか、ところどころに過去の遺産がデッドコードとして残されることがあります。そういったデッドコードが存在していると、障害の解析が困難になったり、後々機能追加が大変になったりメンテナンスの上で苦労することになります。
そういった問題を防ぐためには、やはりアーキテクト的な視点からコードの状態を監視し、常に余分な複雑性が取り残されないようにする努力やプロセスが必要だと思います。

最後に、最近気づいたことを

最後に、最近になって気づいた自分の間違えについて書いておかなくてはなりませんね。以前であればこうした設計上の問題は日本のSI業界の構造が問題なのであるという話をしていたかもしれません(^_^;)が、ここで書いたような話は多少フィクションが入っているとはいえ、実際私がSI業界以外の今の会社で体験したことに基づいていると告白しなくてはなりません。言語の特性から、Javaで開発していると、こういった設計上の問題が起きやすいということがある可能性もありますが、こういった話はSIer以外でも、どこの国の開発チームでもあるのだなということですね。

こうした設計に陥る原因として考えられるのは、SI業界の構造というより、むしろ、アーキテクトがアプリケーション開発チームと一緒に仕事していないという問題にあるのではないかと思います。そして、実際多くの場合、「フレームワークチーム」「共通チーム」というのがアプリケーション開発チームとは切り離されて存在しており、フレームワークを開発プロジェクトが開始する前に作成して提供するというようなモデルの場合に起こりがちなのではと思います。さらに、非常によくあるパターンとして、あるバージョンのフレームワークを使ってアプリケーションの開発が進められている時点では、フレームワークは既にアクティブに開発されておらず、フレームワークの改善に興味がないということがあります。既にフレームワークチームは「次世代フレームワーク」の開発に目が移っていて、古いフレームワークはよほどのバグがない限り手を付けないというようなケースが多いかもしれません。

自分としては、なるべくオープンソースのフレームワークを活用するなどして、自社フレームワークの部分を最小限にし、アーキテクトはアプリケーションのアーキテクチャの改善にもっと目を向けるべきなのではないかと思うのですが、さまざまな政治的、ビジネス的理由からそれができないのが問題でないかと思いますね。

*1:なお、ここではコンポーネントとはJavaパッケージのような論理的な名前空間の分割ではなくて、eclipseやmavenのプロジェクトに相当するようなビルド・デプロイの分割単位のことを指すものとします。

*2:場合によってはインテーフェースやアノテーションを分けるのが有効な場合ももちろんありますが、それにはちゃんとした理由があります。リモートのサービスでインターフェースだけをクライアントから使うとか、昔のSpringがそうしていたように、JDK1.4をサポートするために、アノテーション依存の部分を分けるといったような例があります

*3:レガシーコードだったらともかくも、新しいプロジェクトでxmlに個々のbean定義を書くような規約だったら、これ自体かなり疑問な設計方針なのですが

*4:コンポーネント分割の構造とチームの構造がマッチしているのは基本的によいことだと考えられます。

*5:そして、多くの場合作成すべき各コードにベースとなる親クラスやインターフェースが存在してそれを継承して作成するルールになっているのですが、実際親はマーカーというか、実際になんら本質的な役割を持っていないという場合も多いです。これは親クラスが実際の必要性から作られたのではなく、最初からルールとして存在しているという何よりの証拠といえます。

*6:いわゆるPlain Old Java Object=POJOとして作成可能、継承などはアプリケーション側の裁量で自由に使えるようにする。

*7:StringUtilsやCollectionsのような標準のライブラリーにもみられるように、どこのクラスにも属さないロジックの格納場所として、最低限のUtilクラスは特に問題ではないと思います。ここで問題としているのは、大部分のビジネスロジックがUtilクラスで書かれているというようなケースですね。