はじめに
ソフトウェア開発では、コードの変更が避けられない一方で、無秩序や技術的負債が増加し、開発スピードの低下や保守コストの増大を引き起こします。特に、リファクタリングを怠ると、コードが複雑化し、新たなバグの温床となります。
この記事では、リファクタリングの重要性に加え、対象機能の選定方法や、具体的なリファクタリング手法とそのテスト戦略について解説します。
なぜリファクタリングが今必要なのか?
課題
- 技術的負債の増加:変更を重ねるほど、コードの複雑性や無秩序が増大
- テスト不足:回帰テストが不十分だとリファクタリング後の動作保証が困難
- 開発速度の低下:技術的負債が増えることで見積もり工数に誤差が生じ、開発効率が悪化
仮説
- 技術的負債が予測不可能なほど高くなる前にリファクタリングを開始すれば、開発スピードを維持できます
リファクタリングの対象機能をどう選ぶか?
リファクタリングの成功には、優先度の高い機能を選定することが鍵です。
以下に、選定基準を示します。
なお、選定基準は、高橋寿一著『ソフトウェア品質を高める開発者テスト 改訂版 アジャイル時代の実践的・効率的でスムーズなテストのやり方』を参考にしています。
基準:バグの発生率が高いコード
バグが多発する箇所は、保守性と品質向上の観点から改善が求められます。
「バグの発生率」は、HotSpot値で評価します。
方法: Bugspotsを使用
Bugspotsは、Googleが提唱したバグ予測アルゴリズムをオープンソースとして開発したツールです。
直近の変更がある場合ほど、Hotspot値が大きく算出されます。
基準:複雑なコード
コードの複雑性が高いほど、バグのリスクも増加します。
循環的複雑度を測定し、優先度を判断しましょう。
方法: Checkstyleを使用
Javaであれば、Checkstyleで循環的複雑度を計測できます。
Checkstyle設定例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="severity" value="warning"/>
<module name="TreeWalker">
<module name="CyclomaticComplexity">
<property name="max" value="1"/>
<property name="switchBlockAsSingleDecisionPoint" value="true"/>
</module>
</module>
</module>
各メソッドの循環的複雑度が出力されますので、機能(クラス)全体の総循環的複雑度を集計します。
総循環的複雑度の高い機能が、リファクタリング候補となります。
リファクタリング対象
以下の両方を満たす機能を優先的にリファクタリングします。
- HotSpot値が高い
- 循環的複雑度が高い
テスト戦略
リファクタリングは正しく行う必要があります。
リファクタリングの「正しさ」とは、プログラムの外部からみた「振る舞い」が変更なく、ソースが理解し易くなっている(複雑さが減少している)ことです。
リファクタリングでは、リファクタリングでソースを改修するたびに、システムの「振る舞い」が以前と同じかどうかを検証します。
これを実現するテスト戦略を説明します。
「振る舞い」テストの整備
「振る舞い」とは、業務的に意味のあるシステムの動作を指します
同じ入力に対して、リファクタリング後も、システムの「振る舞い」によるアウトプットが、同じであることを検証します
以下を検証するテストを用意します。
- 戻り値が同じであること
- データベースの更新結果が同じであること
- 別サービスに渡す引数が同じあること
ブロックの選定
前述のようにリファクタリングが必要な機能を選定した後は、振る舞いテストの範囲を設定します。
選定した機能の処理は一般的に複雑です。
クラスやメソッド全体を網羅するテストを一気に整備してリファクタリングするのは、簡単ではありません。
そこで、処理の中から業務的に意味のある小さい塊を見つけます。
これを「ブロック」と呼称します。
既に内部メソッドとして定義されているものも「ブロック」になります。
複雑な処理を業務的に意味のある小さな「ブロック」に分割することで、テスト範囲を限定し、リファクタリングを効率的に実施できるようにします。
リファクタリング手法
リファクタリングは、「ブロック」の単位に実施します。
ブロック単位に、「アプリケーションのリファクタリング」、「テストスクリプトのリファクタリング」を実施します。
「アプリケーションのリファクタリング」では、「1.クラスの分割」、「2.内部構造の整理」のステップを実施します。
1.クラスの分割
循環的複雑度が高いクラスは、さまざまな責任を一つのクラスで抱え込みがちです。
このようなクラスには、以下のような処理が混在していることがあります。
- データの計算や補完
- データベースへのアクセス
- 他サービスの呼び出し
まず、この問題を解消するために、処理を以下の2つのクラスに分割します。
-
ドメイン・クラス
データの補完や計算といった、ドメイン(ビジネス領域)に特化した処理を実装します -
コントローラ・クラス
データベースアクセス、ドメイン・クラスや外部サービスの呼び出しを行い、処理フローを管理します
「ドメイン・クラス」と「コントローラ・クラス」が呼び出しできるクラスは次のとおりになります。
2.内部構造の整理
クラスを分割した後、内部構造を整理することで、コードの可読性と保守性を向上させます。
以下のような実装が整理の対象となります。
-
1つのメソッド内で複数の振る舞いを実装している場合
例:データの検証と更新を同じメソッドで処理している -
1つのメソッド内で複数項目の発番処理を実装している場合
例:異なるデータタイプの項目に対して発番処理をまとめて行っている -
1つのメソッド内でヘッダと明細の項目補完を実装している場合
例:明細データの計算処理とヘッダ情報の更新処理を同時に行っている
ドメイン・クラスの1つのメソッドは、1つの「振る舞い」 のみに責任を持つように整理します。
以下の方法を活用して改善を図ります。
- メソッドの分割、移動
- インライン化と抽出
- 条件記述の統合、分解
- クラスの分割、統合
- デットコードの削除
テストスクリプトのリファクタリング
アプリケーションのリファクタリングが完了したら、変更内容に合わせてテストスクリプトもリファクタリングを行います。
特に、ドメイン・クラスとコントローラ・クラスの責務に応じて、テストスコープを調整します。
-
ドメイン・クラスのテスト
振る舞いテストを活用し、ビジネスロジックや計算処理に特化したテストケースへリファクタリングします -
コントローラ・クラスのテスト
振る舞いテストのうち、処理フローや外部システム、ドメイン・クラスとの連携を検証するテストケースのみを残します
まとめ
リファクタリングは、技術的負債を解消し、コード品質を向上させる上で欠かせないプロセスです。一度に行うのではなく、段階的かつ継続的に進めることで、無理なく効果的な改善が可能になります。
そして、テストとリファクタリングを繰り返し実施することで、効率的で安定した開発プロセスを構築できるはずです。
本記事では、自社の製品開発における実践的なリファクタリング活動を踏まえ、具体的な手法とテスト戦略について解説しました。
この記事が、皆様のリファクタリングの参考になれば幸いです。