コードクローンを対象とした リファクタリングの有効性に関する調査 肥後 芳樹,楠本 真二,井上 克郎 大阪大学 大学院情報科学研究科 {y-higo, kusumoto, inoue}@ist.osaka-u.ac.jp
コードクローン コードクローンとは ソフトウェアの保守を困難にする ソースコード中に存在する一致または類似したコード片 コピーアンドペーストなどのさまざまな理由により生成される ソフトウェアの保守を困難にする あるコード片にバグがあると,そのコードクローン全てについて修正の検討を行う必要がある 機能を追加する場合も同様のことがいえる コピーアンドペースト コピーアンドペースト
リファクタリング ソフトウェアの外部的な振る舞いを保ったままで,内部構造を改善していくこと リファクタリングを行うことでソフトウェアの保守性が増し,後の保守作業効率が向上する これまでに多くのリファクタリングすべきコードのパターン(Bad Smell)とその対処法(Refactoring Pattern)が提案されている[1][2] コードクローンは最も優先してリファクタリングを行うべきものの1つとされている しかし,具体的な評価事例はほとんど報告されていない [1] M. Fowler: Refactoring: Improving the Design of Existing Code, Addison-Wesley, 1999. [2] Refactoring Home Page: http://www.refactoring.com/
研究の目的と概要 コードクローンに対するリファクタリングのコストと効果を調査し,その有効性を評価する 実験対象は複数バージョンのソースコードが公開されているオープンソースソフトウェア 最初のバージョンに対してコードクローン検出 & リファクタリング リファクタリングのコストとして以下のものを用いた ソースコード修正と回帰テストに要した時間 リファクタリングの効果として以下のものを用いた CKメトリクス値の変化 総行数の増減 集約箇所の後のバージョンでの修正回数
CKメトリクス Chidamber と Kemerer が提案したオブジェクト指向ソフトウェアの複雑度を測定するメトリクス[3]であり,広く用いられている 各メトリクスは,クラス単位で計測される WMC(Weighted Methods per Class): そのクラスに定義されているメソッドに重み付けをして足し合わせた値 DIT(Depth of Inheritance Tree): クラス階層における継承の深さ NOC(Number of Children): サブクラスの数 CBO(Coupling Between Objects): 関係しているクラスの数 RFC(Response For a Class): そのクラスのオブジェクトにメッセージが送られた結果,実行されうるすべてのメソッドの数を表す LCOM(Lack of Cohesion Of Methods): そのクラスのメソッドがお互いに関連している程度を表す [3] Chidamber S. R. and Kemerer C. F.: “A Metrics Suite for Object Oriented Design”, IEEE Transaction on Software Engineering, Vol. 20, No. 6, pp. 476-493 (1994).
コードクローンに対するリファクタリング コードクローンは最も優先してリファクタリングを行うべきコード(のパターン)の1つとされている どのようにリファクタリングを行うかは,コードクローン間の関係によって異なる[1] 同一クラス内の複数個所が重複している場合 兄弟クラス間で重複コードが見られる場合 まったく関係のないクラス間で重複コードが見られる場合 [1] M. Fowler: Refactoring: Improving the Design of Existing Code, Addison-Wesley, 1999.
コードクローンに対するリファクタリング: 同一クラス内の複数箇所が重複している場合 Extract Method を適用して,各メソッドの内部からコードクローンを抽出し,新たなメソッドとして定義する. 抽出箇所は,そのメソッドの呼び出し文に変更する.
コードクローンに対するリファクタリング 兄弟クラス間で重複コードを共有している場合 各クラスに対して Extract Method を行い,次に Pull Up Field や Pull Up Method を適用すれば解決する 場合によっては,Form Template Method などを適用する場合もありうる
コードクローンに対するリファクタリング まったく関係のないクラス間で重複コードが見られる場合 Extract Class を適用し,古いクラスから新しいクラスへと処理を委譲するように変更を行う
本研究で扱うコードクローン コードクローンの一般的な定義はない リファクタリング可能なコードクローンを検出したい これまでにいくつかのコードクローン検出手法が提案されているが,それらはどれも異なった定義を持つ リファクタリング可能なコードクローンを検出したい コードクローン検出に CCFinder[3] と CCShaper[4] を用いる [3] T. Kamiya, S. Kusumoto, and K. Inoue, CCFinder: A multi-linguistic token-based code clone detection system for large scale source code, IEEE Transactions on Software Engineering, vol. 28, no. 7, pp. 654-670, Jul. 2002. [4] 肥後 芳樹,植田 泰士,神谷 年洋,楠本 真二,井上 克郎:コードクローン解析に基づくリファクタリングの試み,情報処理学会論文誌, Vol.45, No.5, pp1357-1366, 2004-5
クローンペアとクローンセット クローンペア クローンセット 互いに一致または類似しているコード片の対(ペア) 互いに一致または類似しているコード片の集合(セット) C1 C2 C3 C4 C5 クローンペア クローンセット (C1, C2) {C1, C2, C4} (C1, C4) {C3, C5} (C2, C4) (C3, C5)
コードクローン検出ツール:CCFinder 概要 ソースコードをトークン単位で直接比較することにより,コードクローンを検出する より実用的なコードクローンを見つけることができるように設計されている ユーザ定義名の置き換え テーブル初期化部分の取り除き モジュールの区切りの認識 検出結果はテキスト形式で出力 数百万行単位のソフトウェアからでも数十分で検出可能
コードクローン抽出ツール:CCShaper 概要 CCFinderが検出したコードクローンに含まれるプログラミング言語の構造的なまとまりの部分を抽出 コードクローン情報をリファクタリングに用いる場合に便利 CCFinderが検出するコードクローンはトークンの並びであり,必ずしもリファクタリングの適用が容易なまとまりにはなっていない 現在はJava言語のみに対応 抽出するコードクローンの単位は以下の12個 宣言:クラス,インターフェース 関数:メソッド,コンストラクタ,スタティックイニシャライザ 文:do, for, if, switch, synchronized, try, while,
コード片1 CCFinderが検出するクローン コード片2 CCShaperが抽出するクローン 次に提案手法での抽出の例を示します. 609: reset(); 610: grammar = g; 611: // Lookup make-switch threshold in the grammar generic options 612: if (grammar.hasOption("codeGenMakeSwitchThreshold")) { 613: try { 614: makeSwitchThreshold = grammar.getIntegerOption("codeGenMakeSwitchThreshold"); 615: //System.out.println("setting codeGenMakeSwitchThreshold to " + makeSwitchThreshold); 616: } catch (NumberFormatException e) { 617: tool.error( 618: "option 'codeGenMakeSwitchThreshold' must be an integer", 619: grammar.getClassName(), 620: grammar.getOption("codeGenMakeSwitchThreshold").getLine() 621: ); 622: } 623: } 624: 625: // Lookup bitset-test threshold in the grammar generic options 626: if (grammar.hasOption("codeGenBitsetTestThreshold")) { 627: try { 628: bitsetTestThreshold = grammar.getIntegerOption("codeGenBitsetTestThreshold"); CCShaperが抽出するクローン CCFinderが検出するクローン コード片2 623: } 624: 625: // Lookup bitset-test threshold in the grammar generic options 626: if (grammar.hasOption("codeGenBitsetTestThreshold")) { 627: try { 628: bitsetTestThreshold = grammar.getIntegerOption("codeGenBitsetTestThreshold"); 629: //System.out.println("setting codeGenBitsetTestThreshold to " + bitsetTestThreshold); 630: } catch (NumberFormatException e) { 631: tool.error( 632: "option 'codeGenBitsetTestThreshold' must be an integer", 633: grammar.getClassName(), 634: grammar.getOption("codeGenBitsetTestThreshold").getLine() 635: ); 636: } 637: } 638: 639: // Lookup debug code-gen in the grammar generic options 640: if (grammar.hasOption("codeGenDebug")) { 641: Token t = grammar.getOption("codeGenDebug"); 642: if (t.getText().equals("true")) { 次に提案手法での抽出の例を示します. このようなソースコードが与えられた場合,CCFinderはこのハイライトがかかった部分をクローンとして検出します. 提案手法ではこのような場合,このクローン内に存在する最大の構造的なまとまりであるif文の部分を抽出します.
コード片3 コード片4 CCFinderが検出するクローンペア もう1つ例を示します. 1527: if ( inputState.guessing==0 ) { 1528: t=a.getText(); 1529: } 1530: { 1531: _loop84: 1532: do { 1533: if ((LA(1)==COMMA)) { 1534: match(COMMA); 1535: id(); 1536: if ( inputState.guessing==0 ) { 1537: t+=","+b.getText(); 1538: } 1539: } 1007: if ( inputState.guessing==0 ) { 1008: buf.append(a.getText()); 1009: } 1010: { 1011: _loop144: 1012: do { 1013: if ((LA(1)==WILDCARD)) { 1014: match(WILDCARD); 1015: a=id(); 1016: if ( inputState.guessing==0 ) { 1017: buf.append('.'); buf.append(a.getText()); 1018: } 1019: } コード片3 コード片4 CCFinderが検出するクローンペア もう1つ例を示します. このようなソースコードが与えられた場合CCFinderはこの部分をクローンをして検出します. しかし,このクローン内には特に構造的なまとまりは存在しません. 提案手法ではこの様な場合を識別し,何も抽出しません.
実験: 概要 実験対象はオープンソースソフトウェア Ant 記述言語: Java 15バージョンのソースコードを用いた バージョン番号:1.1, 1.2, 1.3, 1.4, 1.5, 1.5.1, 1.5.2, 1.5.3, 1.5.4, 1.6.0, 1.6.1, 1.6.2, 1.6.3, 1.6.4, 1.6.5 最も古いバージョン(1.1)からコードクローン検出を行い,検出された全てのコードクローンに対してリファクタリングを試みた リファクタリングのコストとして以下のものを用いた ソースコード修正と回帰テストに要した時間 リファクタリングの効果として以下のものを用いた CKメトリクス値の変化 総行数の増減 集約箇所の後のバージョンでの修正回数
実験: バージョン1.1に対するリファクタリング コードクローンのリファクタリングを行うために CCFinderとCCShaperを用いて50トークン以上のコードクローンを検出 バージョン1.1の規模 ソースファイル数:83 総行数:19,986 検出時間は約1分 7個のクローンセットを検出 そのうちの6つが実際にリファクタリングすることができた ソースコード修正後,付属のテストケースを用いて回帰テストを行った その後の修正回数を調査するために バージョン1.2~1.6.5の該当箇所を調査 前のバージョンから修正を加わっているかどうかを diff で調査 クローンセットになっているかを CCFinder と CCShaper を用いて調査
実験: 検出されたコードクローン 以下は検出されたコードクローンの概要である 実際はもっと回帰テストに時間がかかる ID コード片数 単位 付属のユニットテストしか行っていない,結合テストなども行うべき ID コード片数 単位 分類 時間 S0 2 メソッド 単一クラス内 7分 S1 6 19分 S2 - S3 15分 S4 If文 兄弟クラス間 14分 S5 5分 S6 関係のないクラス間 17分
実験: CKメトリクス値の変化(1/3) S0のCKメトリクス値の変化 S1のCKメトリクス値の変化 S3のCKメトリクス値の変化 クラス名 WMC DIT NOC CBO RFC LCOM DirectoryScanner 21(+1) 1 47(+1) 158(+20) クラス名 WMC DIT NOC CBO RFC LCOM DirectoryScanner 21(+1) 1 47(+1) 158(+20) リファクタリング前に比べてメトリクス値が悪化 リファクタリング前に比べてメトリクス値が改善 クラス名 WMC DIT NOC CBO RFC LCOM Task 17(+1) 1 23 4 24(+6) 98(+16) $NestedElementHandler 2 14(-3) $TaskHandler 7(-2) 21(-7)
実験: CKメトリクス値の変化(2/3) S4のCKメトリクス値の変化 S5のCKメトリクス値の変化 S6のCKメトリクス値の変化 クラス名 WMC DIT NOC CBO RFC LCOM ProjectHelper 15(+2) 1 7(+1) 66(+7) 79(+27) Expand 4 2 5 35(-3) Untar 8 36(-3) クラス名 WMC DIT NOC CBO RFC LCOM Javac 23(-1) 3 1 11 93 128(-105) MatchingTask 16(+1) 2 5(+1) 56(+4) 94(+79) Rmic 17(-1) 8 66(-2) 98(-17) クラス名 WMC DIT NOC CBO RFC LCOM NewClass 2 1 7 JavacOutputStream 4 2(+1) 10(-1) TaskOutputStream 3 8(-1)
実験: CKメトリクス値の変化(3/3) コードクローンのリファクタリングは,必ずしもCKメトリクス値を改善するとは限らない 中には,クローンセット S0 や S1 のようにメトリクス値を悪化させているだけのリファクタリングも存在した CKメトリクスを測定するために ckjm を用いた このツールはメトリクス WMC として,単純にそのクラスで定義されたメソッドの数を用いている WMCは Weigthed Method per Class の略 本来は,各メソッドに対して重み付けを行った後,値を足し合わせる Extract Method のようにメソッド数が増えるリファクタリングの場合は,必ず値が悪化してしまう サイクロマチック数などを用いて適切に重み付けをすれば,メソッド数を増やすリファクタリングであっても WMC の改善があるかもしれない
実験: 総行数の変化 下図は各クローンセットのリファクタリング後の総行数の変化を表す 5つのリファクタリングで総行数が減っている 多くても20行程度しか減っておらず,サイズ面で効果があるとはいい難い “Extract Class”パターンを適用したリファクタリングは,新しいクラスを作成したために,リファクタリング前に比べて行数が増えてしまっている リファクタリング S0後 S1後 S3後 S4後 S5後 S6後 行数 19,982 (-4) 19,964 (-22) 19,981 (-5) 19981 19,962 (-24) 19,990 (+4)
実験: 修正の流れ(1/5)(クローンセット S0,S2) 同一クローンセットに含まれるコード片は長方形で囲まれる コード片は Cn で表され,修正が加わるたびに Cn’, Cn’’ と変化していく
実験: 修正の流れ(2/5)(クローンセット S1)
実験: 修正の流れ(3/5)(クローンセット S3,S4)
実験: 修正の流れ(4/5)(クローンセット S5,S6)
実験: 修正の流れ(5/5) リファクタリング箇所の後のバージョンでの修正回数はクローンセットによってさまざまであった 全てのクローンセットがバージョンをまたいで存在しているわけではなく,各コード片に対して異なる修正が加わった結果,コードクローンでなくなる場合もあった. 本実験では7つクローンセットのうちの4つ これらのクローンセットには一度も共通の修正がなかった 一度でも共通の修正が加わった場合は,その後も修正される傾向がある 実際に全てのコード片に同様の修正が加わった場合にリファクタリングを行うことで,その後の修正回数を減らすことができるのでは?
考察 リファクタリングが有効であるかどうかは,その効果がコストを上回っているかどうかで決まる 例:クローンセット S0 コスト:ソースコード修正と回帰テストに7分を要した 効果(CKメトリクス):WMC,RFC,LCOM が悪化 効果(修正回数):バージョン 1.5 と 1.6.3 での修正箇所が2つから1つに減った 効果とコストの大小関係は一意に決めることができない 本実験で行ったリファクタリングは,基本的かつ汎用的であり,ソースコード修正を行う者のスキルや対象ソフトウェアのドメインを限定するものではない リミテーション リファクタリングの規模が小さい メトリクス値で調査したのは複雑度の変化のみ 他の面からの評価も必要である
まとめ コードクローンに対するリファクタリングのコストと効果を調査した 実験対象は15バージョンのソースコードが公開されているオープンソースソフトウェア その最初のバージョンに対してリファクタリングを行った コストの尺度 ソースコード修正と回帰テストに要した時間 効果の尺度 CKメトリクス値の変化 総行数の増減 集約箇所の後のバージョンでの修正回数 こらからの予定 コスト・効果項目を増やす 企業ソフトウェアでも実験を行う(+ 開発者へのインタビュー)
コードクローン検出ツール: CCFinder 検出プロセス Source files 字句解析 変換処理 トークン列 検出処理 変換後トークン列 クローン情報 出力整形処理 クローンペア位置情報 1. static void foo() throws RESyntaxException { 2. String a[] = new String [] { "123,400", "abc", "orange 100" }; 3. org.apache.regexp.RE pat = new org.apache.regexp.RE("[0-9,]+"); 4. int sum = 0; 5. for (int i = 0; i < a.length; ++i) 6. if (pat.match(a[i])) 7. sum += Sample.parseNumber(pat.getParen(0)); 8. System.out.println("sum = " + sum); 9. } 10. static void goo(String [] a) throws RESyntaxException { 11. RE exp = new RE("[0-9,]+"); 12. int sum = 0; 13. for (int i = 0; i < a.length; ++i) 14. if (exp.match(a[i])) 15. sum += parseNumber(exp.getParen(0)); 16. System.out.println("sum = " + sum); 17. } 字句解析 変換処理 トークン列 検出処理 変換後トークン列 クローン情報 出力整形処理 字句解析 変換処理 トークン列 検出処理 変換後トークン列 クローン情報 出力整形処理 字句解析 変換処理 トークン列 検出処理 変換後トークン列 クローン情報 出力整形処理 1. static void foo() throws RESyntaxException { 2. String a[] = new String [] { "123,400", "abc", "orange 100" }; 3. org.apache.regexp.RE pat = new org.apache.regexp.RE("[0-9,]+"); 4. int sum = 0; 5. for (int i = 0; i < a.length; ++i) 6. if (pat.match(a[i])) 7. sum += Sample.parseNumber(pat.getParen(0)); 8. System.out.println("sum = " + sum); 9. } 10. static void goo(String [] a) throws RESyntaxException { 11. RE exp = new RE("[0-9,]+"); 12. int sum = 0; 13. for (int i = 0; i < a.length; ++i) 14. if (exp.match(a[i])) 15. sum += parseNumber(exp.getParen(0)); 16. System.out.println("sum = " + sum); 17. }
リファクタリングできなかったクローンセット S2 コード片 C0 コード片 C1 protected void fireTargetStarted(Target target) { BuildEvent event = new BuildEvent(target); for (int i = 0; i < listeners.size(); i++) { BuildListener listener = (BuildListener) listeners.elementAt(i); listener.targetStarted(event); } protected void fireTaskStarted(Task task) { BuildEvent event = new BuildEvent(task); for (int i = 0; i < listeners.size(); i++) { BuildListener listener = (BuildListener) listeners.elementAt(i); listener.taskStarted(event); }