2011年7月3日日曜日

ソフトウェア見積り―人月の暗黙知を解き明かす を読んでの感想

見積りってどうやるのが業界標準なの?と長年疑問に思っていたので「ソフトウェア見積り―人月の暗黙知を解き明かす」を手にとって見た。



プロジェクトの工数見積りって要件もざっくりとしか決まっていない時点で6ヶ月で納品とかよく算出されるけれど、以前から「これってみんなは何を根拠に出しているのだろう」と前々から疑問に思っていた。自分の場合は、ほんとにざっくりとこんな機能がいるよね~、で、この機能は大体5日~、10日~、のような感じで算出するのだけれど、これってほんとあいまいだなぁ、と常々考えていた。

で、本書。そんな不安な思いを打ち消してくれる役立つ内容てんこ盛りだった。中でも自分的になるほど~と感心した点を以下にまとめておく。

不確実性のコーン
プロジェクトの初期では要求さえ明確ではないので正確な見積りを行うことがほぼ不可能で、その振れ幅は4倍~0.25倍にもなる。ただ、詳細設計が完了する段階までくるとその振れ幅は1.25倍~0.80倍になり、それ以降もプロジェクトの収束に向かって見積りの正確性は向上していく。

判断するな。数えろ。
恣意的な判断で、これは5日ぐらい、という判断をするのではなく、判断の基準となる過去のデータを調べて予測すること。その参考とする過去のデータってなんなのよ?という詳しい説明は本書でなされている。

範囲での見積り
シングルポイントで5ヶ月です、というような見積りをするのではなく、7ヶ月から+-2ヶ月です。というような範囲で見積りを出すようにし、プロジェクトの進行とともにその範囲が狭まっていくようにする。またそれぞれの見積りごとに正確性の割合を出すと良い。5ヶ月でリリースできる確立は15%ですが9ヶ月でリリースできる確立は90%です、のような感じ。

上記のほかにも、ステークホルダーにどのように見積りを認めさせるのか、など交渉のヒントにも言及されている。

本書を読むといくつかのすぐに使える技法を学ぶことは可能だが、だからと言って翌日からすぐに正確な見積りができるようになるわけではない。というのも、基本的に正確な見積りを行うようになるには組織全体として取り組む必要があるので、中長期的な視野にたってそのことを煮詰めていく必要がある(過去のデータを収集するあたりなどとくに)。ただ、正確な見積りのためには開発者個々人の見積りに対する高い意識も必要とされるので、そういうのを根付かせるための材料として本書を使うこともできるなと感じた。


--------------------
以下、本書を読んでいないと意味不明だと思うけれど、自分的メモ用に気になった公式をいくつか抜粋。
最良ケースと最悪ケースの見積りを考えるようにする。何も言わないと開発者は大体最良ケースに近い一点見積りを行う。
期待ケース1=[最良ケース + (4*最有力ケース) +最悪ケース]/6
期待ケース2=[最良ケース + (3*最有力ケース) +(2*最悪ケース)]/6
MRE(Magnitude of relative error)=[(実際の結果-見積り結果)/実際の結果]の絶対値
標準偏差=(最悪ケースの見積りの和 - 最良ケースの見積りの和)/6

2011年7月2日土曜日

LambdaExpressionで動的な検索条件を作る

最近の仕事で固定の検索条件ではなく動的に検索条件を作りたいという仕様があり、それもあってExpression周りをいじくりまわしていて楽しかったので解説する。

今回のサンプルコードはこちらからダウンロードできる。


Linqを使用する場合に動的に検索条件を作るためにはExpression Treeと戯れる必要がある。Linq To SqlであればDynamic Linqという文字列を解析してExpression Treeに変換してくれる拡張関数群が公開されているので、文字列をいじくるのを苦にしなければかなり簡単に動的な検索条件を作成することが可能だ。しかし、Linq To Entityだとそうはいかないのでちょっと込み入ったことをしなくてはならない。

動的検索条件と言っても、すべての検索条件がAnd条件であるならば、単純にWhereメソッドを下記のように順々に呼び出していけばよいだろう。

if ( 条件1 )
  query = query.Where(c => c.Name == "四郎" );

if ( 条件2)
  query = query.Where(c => c.Age > 15 );

※今回の解説の仕様として、等号などのオペレーターや対象プロパティは事前に決まっていてどの条件を組み合わせるかのみ動的に設定できる、という設定とする。オペレーターやプロパティもすべてを動的に作る場合にはParameterExpressionなどを使ってさらに深くExpression Treeをいじる必要があるのでその解説は次回以降行う、かもしれない。

しかし、Or検索が間に入ってくると話が変わってくる。前述の方法ではWhereメソッド間はAnd条件になるので動的なOr検索を行うことはできない。というわけで、LambdaExpressionの登場だ。こいつを上手に使ってやると下記のような条件を簡単に動的に組み立てることができる。

query.Where(c => (c.Name.Contains("白") || c.Bushous.Any(b => b.Age > 36)) && c.Name.EndsWith("城"));

今回使用するモデルは下記。DBの作成はEF Code First4.1に全部任せている。
public class Bushou
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public int CastleId { get; set; }
    public Castle Castle { get; set; }
}

public class Castle
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual IList<Bushou> Bushous { get; set; }
}
    
public class Ikusa : DbContext
{
    public DbSet<Bushou> Bushous { get; set; }
    public DbSet<Castle> Castles { get; set; }
}

前述の各条件を表すLambdaExpressionは下記になる。

Expression<Func<Castle, bool>> condition1 = c => c.Name.Contains("白");
Expression<Func<Castle, bool>> condition2 = c => c.Bushous.Any(b => b.Age > 36);
Expression<Func<Castle, bool>> condition3 = c => c.Name.EndsWith("城");

このままでは各LambdaExpressionはばらばらなので一つにまとめる必要がある。その際に注意が必要なのだが、LambdaExpressionを統合するには同一の引数を使用するように変更してやらなければならない。上記の定義を見てもらえば分かると思うけれど、各LambdaExpressionにはcという引数が定義されている。Expression Tree的にはcondition1のcとcondition2、condition3のcはまったくの別物なので、こいつらを一つにまとめる必要がある。

で、そんな面倒なことを行う際に便利なクラスがC#4.0から提供されている。それがExpressionVisitorクラスだ。このクラスの使いどころは次のような場合だ。Expressionには多種多様なタイプが用意されている。go toステートメントを表すGoToExpression、ループを表すLoopExpression、メンバーアクセスを表すMemberExpressionなどなど数十種類に及ぶ。で、今回は引数をいじくる必要があるのでParameterExpressionが対象になる。しかしExpression Treeを順々にたどっていって任意のExpressionを探すのは骨が折れる。なのでそんなときはExpressionVisitorクラスを使用する。

使い方はExpressionVisitorクラスを継承し、自分が気になるタイプのExpressionを引数としているメソッドをオーバーライドするだけだ。今回はParameterExpressionをいじくるのでVisitParameterがオーバーライドするメソッドになる。

class ParameterVisitor : ExpressionVisitor
{
    private ReadOnlyCollection<ParameterExpression> _fromParams, _toParams;
    public ParameterVisitor(ReadOnlyCollection<ParameterExpression> from, ReadOnlyCollection<ParameterExpression> to)
    {
        _fromParams = from;
        _toParams = to;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        for (var i = 0; i < _fromParams.Count; i++)
        {
            if (node == _fromParams[i])
                return _toParams[i];
        }
        return node;
    }
}

ParameterVisitorクラスは変更元の引数コレクションと変更先の引数コレクションを保持し、ParameterExpressionが見つかるたびに、その見つかった引数(上記で言うところのnode)が変更元の引数と同一かをチェックし、同一であれば、変更先の引数を返却している。変更先の引数を返却することで変更元のExpression Treeは差し替えられている。つまり、変更元の引数の数と、変更先の引数の数が異なる場合はアボンするということなので注意が必要だ。ParameterVisitorクラスのコンストラクタにそれようのチェックコードを入れるのも良いだろう。

実際にParameterVisitorクラスの使用方法は下記になる。

class Helper
{
    public static Expression<Func<T, bool>> AndAlso<T>(Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
    {
        var newExp = new ParameterVisitor(right.Parameters, left.Parameters).Visit(right.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left.Body, newExp), left.Parameters);
    }

    public static Expression<Func<T, bool>> OrElse<T>(Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
    {
        var newExp = new ParameterVisitor(right.Parameters, left.Parameters).Visit(right.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.OrElse(left.Body, newExp), left.Parameters);
    }
}

各メソッドともに、まずはVisitメソッドを呼び出し、二個目の引数のLambdaExpressionの引数を一個目の引数のLambdaExpressionの引数で差し替えている。その後にAndAlso、またはOrElseを呼び出しExpressionの統合を行っている。

Helperクラスの使い方は下記になる。

var condition = Helper.AndAlso(Helper.OrElse(condition1, condition2), condition3);

そうしてできたLambdaExpressionをWhereメソッドに渡してやれば下記と同等の記述になる。

query.Where(c => (c.Name.Contains("白") || c.Bushous.Any(b => b.Age > 36)) && c.Name.EndsWith("城"));

query = query.Where(condition);

かなり冗長になるけれど、上記の方法以外にも下記のようにWhereメソッドを呼び出すこともできる。

var resultExp = Expression.Call(typeof(Queryable),
                                "Where",
                                new Type[] { typeof(Castle) },
                                query.Expression,
                                Expression.Quote(condition));
query = query.Provider.CreateQuery<Castle>(resultExp);

今回の例では下のほうの呼び出し方をする必要はまったくないけれど、オペレーターやプロパティも含めてすべてを動的で行う場合などで、Anyやら何やらを動的に呼び出す必要がある場合は、最後に解説したような呼び出し方をしなければならない。