ソフトウェア開発は複雑性とシンプルさの戦いです。この2つのバランスを取ることは難しく、長く保守不可能なメソッドと過度な抽象化のトレードオフがあります。どちらかに傾きすぎると、コードの可読性が低下し、欠陥の可能性が高まります。
欠陥は回避可能でしょうか?NASAは試みていますが、彼らは膨大なテストも行っています。彼らのソフトウェアは文字通りミッションクリティカルです。ほとんどの組織ではそうではなく、大量のテストはコストがかかり、実用的ではありません。テストに代わるものはありませんが、テストなしで欠陥耐性のあるコードを書くことは可能です。
20年間のコーディングとアプリケーションアーキテクチャの経験から、欠陥を減らすための4つのプラクティスを特定しました。最初の2つのプラクティスは欠陥の導入を制限し、最後の2つのプラクティスは欠陥を露出させます。各プラクティスはそれ自体が広大なトピックであり、多くの本が書かれています。各プラクティスを数段落に凝縮し、可能な限り追加情報へのリンクを提供しました。
1. シンプルなコードを書く
シンプルであることは簡単であるべきですが、そうではありません。シンプルなコードを書くことは難しいのです。
これを読んで、シンプルな言語機能を使うことだと思う人もいるでしょうが、そうではありません。シンプルなコードは愚かなコードではありません。
客観的に保つために、複雑性の尺度として循環的複雑度を使用しています。複雑性を測定する他の方法や複雑性の他のタイプもあり、これらのトピックについては後の記事で探索したいと思っています。
Microsoftは循環的複雑度を次のように定義しています:
循環的複雑度は、メソッドを通る線形独立パスの数を測定します。これは条件分岐の数と複雑性によって決定されます。低い循環的複雑度は、一般的に理解、テスト、保守が容易なメソッドを示します。
低い循環的複雑度とは何でしょうか?Microsoftは循環的複雑度を25未満に保つことを推奨しています。
正直に言うと、Microsoftの循環的複雑度25という推奨値は高すぎると感じています。保守性と複雑性の観点から、理想的なメソッドサイズは1~10行で、循環的複雑度は1~5です。
Bill WagnerはEffective C#, Second Editionでメソッドサイズについて書いています:
C#コードをマシン実行可能コードに変換することは2段階のプロセスであることを忘れないでください。C#コンパイラはアセンブリで配信されるILを生成します。JITコンパイラは必要に応じて各メソッド(またはインライン化が関係する場合はメソッドのグループ)のマシンコードを生成します。小さな関数はJITコンパイラがそのコストを償却するのをはるかに容易にします。小さな関数はインライン化の候補になる可能性も高くなります。重要なのは小ささだけではなく、制御フローのシンプルさも同じくらい重要です。関数内の制御分岐が少ないほど、JITコンパイラが変数をレジスタに割り当てるのが容易になります。より明確なコードを書くことは単に良い実践ではなく、実行時により効率的なコードを作成する方法です。
循環的複雑度を視点に入れるために、次のメソッドは循環的複雑度が12です。
public string ComplexityOf12(int status)
{
var isTrue = true;
var myString = "Chuck";
if (isTrue)
{
if (isTrue)
{
myString = string.Empty;
isTrue = false;
for (var index = 0; index < 10; index++)
{
isTrue |= Convert.ToBoolean(new Random().Next());
}
if (status == 1 || status == 3)
{
switch (status)
{
case 3:
return "Bye";
case 1:
if (status % 1 == 0)
{
myString = "Super";
}
break;
}
return myString;
}
}
}
if (!isTrue)
{
myString = "New";
}
switch (status)
{
case 300:
myString = "3001";
break;
case 400:
myString = "4003";
break;
}
return myString;
}
一般的に受け入れられている複雑性仮説は、複雑性と欠陥の間に正の相関が存在することを仮定しています。
前の行は少し複雑です。最も単純な言葉で言えば、コードをシンプルに保つことは欠陥率を低下させます。
2. テスト可能なコードを書く
研究によると、実際のテストを書かずにテスト可能なコードを書くことは、欠陥の発生を低下させます。これは重要で深刻なので、繰り返す必要があります:実際のテストを書かずにテスト可能なコードを書くことは、欠陥の発生を低下させます。
これは疑問を生じさせます。テスト可能なコードとは何でしょうか?
テスト可能なコードを、分離してテストできるコードと定義します。これは、すべての依存関係をテストからモックできることを意味します。依存関係の例はデータベースクエリです。テストでは、データはモック(偽造)され、期待される動作のアサーションが行われます。アサーションが真の場合、テストは成功し、そうでない場合は失敗します。
テスト可能なコードを書くことは難しく聞こえるかもしれませんが、実際にはInversion of Control(Dependency Injection)とS.O.L.I.D原則に従うと簡単です。その容易さに驚き、なぜこのようにして書き始めるのに時間がかかったのか疑問に思うでしょう。
3. コードレビュー
開発チームが採用できる最も影響力のあるプラクティスの1つはコードレビューです。
コードレビューは開発者間の知識共有を促進します。経験から言うと、他の開発者とコードについてオープンに議論することは、私のコード作成スキルに最も大きな影響を与えました。
Code Completeという本で、Steve McConnellはコードレビューの利点に関する多くのケーススタディを提供しています:
- 200人以上の従業員を持つAT&Tの組織の研究では、組織がレビューを導入した後、生産性が14%増加し、欠陥が90%減少したと報告されています。
- Aetna Insurance Companyは、検査を使用してプログラムのエラーの82%を発見し、開発リソースを20%削減することができました。
- 同じグループの人々によって開発された11個のプログラムのグループでは、最初の5つはレビューなしで開発されました。残りの6つはレビューで開発されました。すべてのプログラムが本番環境にリリースされた後、最初の5つは平均100行あたり4.5エラーでした。検査されていた6つは平均100行あたり0.82エラーのみでした。レビューはエラーを80%以上削減しました。
これらの数字があなたをコードレビューの採用に説得しない場合、あなたはJohnny Paycheck’s Take This Job and Shove Itを歌いながらブラックホールに漂流する運命にあります。
4. ユニットテスト
正直に言うと、締め切りに追われているとき、テストは最初に削除されるものです。しかし、テストの利点は否定できません。以下の研究がそれを示しています。
Microsoftはユニットテストの有効性に関する研究を実施しました。彼らは、自動テストを使用したコーディングバージョン2(バージョン1はテストなし)が欠陥を即座に20%削減したが、追加の30%のコストがかかったことを発見しました。
別の研究はテスト駆動開発(TDD)を調べました。彼らはTDDを使用していない同様のプロジェクトと比較して、コード品質が2倍以上増加したことを観察しました。TDDプロジェクトは平均して開発に15%長くかかりました。TDDの副作用は、テストがライブラリとAPIのドキュメントとして機能したことです。
最後に、テストカバレッジと検証後の欠陥に関する研究では:
両方のプロジェクトでテストカバレッジの増加は、リリース前の変更の数に調整されたときのフィールド報告の問題の減少に関連していることがわかります。
例
次のコードの循環的複雑度は4です。
public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
{
AgencySettingsRepository agencySettingsRepository = new AgencySettingsRepository();
var agencySettings = agencySettingsRepository.GetById(agency.Id);
if (agencySettings != null)
{
var newAuthAdmin = agencySettings.NewUserAuthorizationContact;
if (newAuthAdmin.IsNotNull())
{
EmailService emailService = new EmailService();
emailService.SendTemplate(new[] { newAuthAdmin.Email }, GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
{
s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
s.Add(new EmailToken
{
Token = "Subject",
Value =
string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
});
s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });
return s;
});
}
}
}
上記のコードのテスト可能性を検討しましょう。
これはシンプルなコードですか?
はい、循環的複雑度は5未満です。
依存関係はありますか?
はい。2つのサービスAgencySettingsRepositoryとEmailServiceがあります。
サービスはモック可能ですか?
いいえ、それらの作成はメソッド内に隠されています。
コードはテスト可能ですか?
いいえ、AgencySettingsRepositoryとEmailServiceをモックできないため、このコードはテスト可能ではありません。
リファクタリングされたコードの例
このコードをテスト可能にするにはどうすればよいでしょうか?
コンストラクタインジェクションを使用して、AgencySettingsRepositoryとEmailServiceを依存関係として注入します。これにより、テストからそれらをモックし、分離してテストできます。
以下はリファクタリングされたバージョンです。
サービスがコンストラクタに注入されていることに注意してください。これにより、SendMailコンストラクタに渡される実装を制御できます。その後、ダミーデータを渡し、サービスメソッド呼び出しをインターセプトするのは簡単です。
public class SendEmail
{
private IAgencySettingsRepository _agencySettingsRepository;
private IEmailService _emailService;
public SendEmail(IAgencySettingsRepository agencySettingsRepository, IEmailService emailService)
{
_agencySettingsRepository = agencySettingsRepository;
_emailService = emailService;
}
public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
{
var agencySettings = _agencySettingsRepository.GetById(agency.Id);
if (agencySettings != null)
{
var newAuthAdmin = agencySettings.NewUserAuthorizationContact;
if (newAuthAdmin.IsNotNull())
{
_emailService.SendTemplate(new[] { newAuthAdmin.Email },
GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
{
s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
s.Add(new EmailToken
{
Token = "Subject",
Value = string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
});
s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });
return s;
});
}
}
}
}
テスト例
以下は分離してテストする例です。モックフレームワークFakeItEasyを使用しています。
[Test]
public void TestEmailService()
{
//Given
//Using FakeItEasy mocking framework
var repository = A<IAgencySettingsRepository>.Fake();
var service = A<IEmailService>.Fake();
var agency = new Agency { Name = "Acme Inc." };
var user = new User { FirstName = "Chuck", LastName = "Conway", Email = "chuck.conway@fakedomain.com" }
//When
var sendEmail = new SendEmail(repository, service);
sendEmail.SendUserHadJoinedEmailToAdministrator(agency, user);
//Then
//An exception is thrown when this is not called.
A.CallTo(() => service.SendTemplate(A<Agency>.Ignore, A<User>.Ignore)).MustHaveHappened();
}
まとめ
欠陥耐性のあるコードを書くことは驚くほど簡単です。誤解しないでください。完全に欠陥のないコードを書くことはできません(方法を見つけたら教えてください!)。しかし、この記事で説明した4つのプラクティスに従うことで、コードで見つかる欠陥の減少が見られます。
要約すると、シンプルなコードを書くことは循環的複雑度を約5に保ち、メソッドサイズを小さくすることです。テスト可能なコードを書くことはInversion of ControlとS.O.L.I.D原則に従うことで簡単に実現できます。コードレビューはあなたとチームがドメインと書いたコードを理解するのに役立ちます。コードを説明する必要があるだけで問題が明らかになります。そして最後に、ユニットテストはコード品質を大幅に改善し、将来の開発者のためのドキュメントを提供できます。
Author: Chuck Conway is an AI Engineer with nearly 30 years of software engineering experience. He builds practical AI systems—content pipelines, infrastructure agents, and tools that solve real problems—and shares what he’s learning along the way. Connect with him on social media: X (@chuckconway) or visit him on YouTube and on SubStack.
著者: Chuck Conwayは、ソフトウェアエンジニアリングの経験が30年近くあるAIエンジニアです。彼は実用的なAIシステム(コンテンツパイプライン、インフラストラクチャエージェント、実際の問題を解決するツール)を構築し、学んだことを共有しています。ソーシャルメディアで彼とつながってください: X (@chuckconway) または YouTube と SubStack で彼を訪問してください。