はじめに
C#のプロジェクトを触っていると、特に長年運用されているシステムでは、少し古いコードや現在では非推奨な書き方に出会うことがあります。
これらの「レガシーコード」は、当時の技術では標準的な書き方だったかもしれませんが、現在の視点では以下のような課題を抱えることが多いです。
- メンテナンスが難しい
- パフォーマンスが低下する可能性がある
- 安全性が確保されていない
- 新しい技術と互換性が低い
今回は、C#でよく見かけるレガシーコードの具体例と、それらを現代的な書き方に改善する方法を紹介します。
とはいえ、ここで紹介するコードが既存システムに含まれていても、必ずしもすべてを改善すべきというわけではありません。既存の環境や制約、リソースの都合で変更が難しい場合もあります。大切なのは、これらのコードを理解し、学び、それを通じて自身のスキルやプロジェクトを進化させていくことです。
1. データベース接続
データベース接続の実装方法は、.NETの進化とともに大きく変わってきた領域です。特にデータアクセス層の実装は、パフォーマンスやセキュリティに直結するため、現代的な手法への移行が重要視されています。
レガシーコード
public DataTable GetUserData()
{
string connectionString = ConfigurationManager.ConnectionStrings["MyConnection"].ConnectionString;
SqlConnection connection = new SqlConnection(connectionString);
DataTable dt = new DataTable();
try
{
connection.Open();
string sql = "SELECT * FROM Users WHERE Active = 1";
SqlCommand command = new SqlCommand(sql, connection);
SqlDataAdapter adapter = new SqlDataAdapter(command);
adapter.Fill(dt);
}
finally
{
if (connection?.State == ConnectionState.Open)
{
connection.Close();
}
}
return dt;
}
問題点
- ADO.NETを直接操作しているため、コード量が多くなりがち
-
using
を使わずに手動でリソースを解放しており、リソースリークのリスクがある -
DataTable
は型安全性が低い - 非同期処理に対応していない
現代的な改善案
Entity Frameworkを使用した実装
public class UserContext : DbContext
{
public DbSet<User> Users { get; set; }
public UserContext(DbContextOptions<UserContext> options)
: base(options)
{
}
}
public async Task<IEnumerable<User>> GetActiveUsersAsync()
{
using var context = new UserContext(_options);
return await context.Users
.Where(u => u.Active)
.ToListAsync();
}
Dapperを使用した実装
public async Task<IEnumerable<User>> GetActiveUsersAsync()
{
using var connection = new SqlConnection(_connectionString);
var query = "SELECT * FROM Users WHERE Active = 1";
return await connection.QueryAsync<User>(query);
}
改善点
-
using
ステートメントによる確実なリソース解放 - 非同期処理による効率的な実行
- ORマッパーによる型安全性の確保
- コード量の削減
- テストが容易
2. シングルトンパターン
シングルトンパターンは、グローバルな状態管理のために広く使用されてきましたが、その実装方法は.NETの機能強化とともに進化してきました。特に、スレッドセーフなシングルトンの実装は、システムの信頼性に大きく影響します。
レガシーコード
public class DatabaseManager
{
private static DatabaseManager instance;
private static readonly object padlock = new object();
private DatabaseManager() { }
public static DatabaseManager Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new DatabaseManager();
}
}
}
return instance;
}
}
}
問題点
- 二重チェックロック方式が複雑
- テストが困難
- 依存関係が固定的
現代的な改善案
Lazyを使用した実装
public class DatabaseManager
{
private static readonly Lazy<DatabaseManager> _instance =
new Lazy<DatabaseManager>(() => new DatabaseManager());
private DatabaseManager() { }
public static DatabaseManager Instance => _instance.Value;
}
依存性注入を使用した実装
public class DatabaseManager : IDatabaseManager
{
public DatabaseManager(IConfiguration configuration)
{
// 初期化処理
}
}
// Startup.csまたはProgram.cs
services.AddSingleton<IDatabaseManager, DatabaseManager>();
改善点
- スレッドセーフな実装がより簡潔に
- 遅延初期化が組み込み済み
- テスト容易性の向上
- 依存関係の柔軟な管理
3. イベントハンドラー
イベントハンドラーは、特にGUIアプリケーションで頻繁に使用される機能です。C#の言語機能の進化により、より簡潔で安全な実装が可能になりました。
レガシーコード
public class UserControl
{
public delegate void UserEventHandler(object sender, EventArgs e);
public event UserEventHandler OnUserAction;
protected virtual void RaiseUserAction()
{
if (OnUserAction != null)
{
OnUserAction(this, EventArgs.Empty);
}
}
}
問題点
- 冗長な delegate 定義
- null チェックが必要
- 型安全性が低い
現代的な改善案
public class UserControl
{
public event EventHandler<UserEventArgs> UserAction;
protected virtual void OnUserAction(UserEventArgs e)
{
UserAction?.Invoke(this, e);
}
}
// 使用例
control.UserAction += (sender, e) =>
{
Console.WriteLine($"User action occurred: {e.ActionType}");
};
改善点
- null条件演算子による簡潔な記述
- 型付きイベント引数の活用
- ラムダ式による簡潔なハンドラー定義
4. 例外処理
適切な例外処理は、システムの信頼性とデバッグ容易性を左右する重要な要素です。特に大規模システムでは、詳細なエラー情報の取得と適切な処理が運用面で大きな差を生みます。
レガシーコード
public void ProcessData()
{
try
{
// 処理
}
catch (Exception ex)
{
EventLog.WriteEntry("MyApplication", ex.Message, EventLogEntryType.Error);
throw new Exception("データ処理中にエラーが発生しました。", ex);
}
}
問題点
- 再スロー時にスタックトレースが失われる可能性
- ログ記録が原始的
- 例外の区別がない
現代的な改善案
public async Task ProcessDataAsync()
{
try
{
await DoWorkAsync();
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "データ処理中にエラーが発生しました。");
throw; // スタックトレースを保持
}
}
改善点
- 構造化ログ記録の使用
- 例外フィルターの活用
- スタックトレースの保持
- キャンセレーション対応
5. 設定管理
設定管理は、アプリケーションの柔軟性と保守性に直接影響を与える重要な部分です。特に複数環境での運用を考慮すると、型安全な設定管理の重要性は高くなります。
レガシーコード
public string GetAppSetting(string key)
{
return ConfigurationManager.AppSettings[key] ?? string.Empty;
}
問題点
- 型安全性がない
- 環境による設定変更が困難
- テストが困難
現代的な改善案
public class AppSettings
{
public string ConnectionString { get; set; }
public int Timeout { get; set; }
}
public class UserService
{
private readonly AppSettings _settings;
public UserService(IOptions<AppSettings> settings)
{
_settings = settings.Value;
}
}
// Program.cs
services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
改善点
- 型安全な設定管理
- 依存性注入との統合
- テスト容易性の向上
- 環境別設定の容易な切り替え
ボーナス:最新のC#機能活用例
C# 9.0以降で導入された新しい言語機能は、より簡潔で安全なコードを書くための強力なツールを提供しています。これらの機能は、特にデータの不変性保証やnull安全性の向上に大きく貢献し、多くのレガシーコードの課題を解決する手段となっています。
以前は複数行のコードで表現していた処理が、より宣言的で理解しやすい形で書けるようになりました。
レコード型
不変のデータ構造を簡潔に表現できる新しい型です。DTOやドメインオブジェクトの実装に特に有用です。
public record UserDto(int Id, string Name, bool Active);
// with式による不変オブジェクトの更新
var updatedUser = user with { Active = false };
パターンマッチング
条件分岐をより宣言的に書けるようになった機能です。特に複雑な条件分岐を持つビジネスロジックが読みやすくなります。
public string GetUserStatus(User user) => user switch
{
{ Active: true, LastLoginDate: var date } when date >= DateTime.Today.AddDays(-7)
=> "アクティブ",
{ Active: true }
=> "非アクティブ",
_
=> "無効"
};
null許容参照型
nullによるバグを開発時に検出できる型システムの機能です。多くのNullReferenceExceptionを未然に防ぐことができます。
public class User
{
public string Name { get; set; } = null!;
public string? NickName { get; set; }
}
まとめ
レガシーコードを現代的な書き方に改善することで、以下のような利点が得られます。
- コードの可読性と保守性の向上
- パフォーマンスの改善
- バグの発生リスク低下
- テスト容易性の向上
- 新しい機能やパターンの活用
重要なのは、単に新しい書き方に置き換えるだけではなく、既存のコードがどのようにしてシステムを支えてきたのかを理解し、その改善がもたらす具体的なメリットを考えることだと思うのです。
レガシーコードは、時代遅れの「負の資産」ではなく、過去の経験や知見が詰まった「資産」です!
それを最大限活用しつつ、段階的なリファクタリングを行うことで、リスクを最小限に抑えつつ、システムをより堅牢でモダンな形へ進化させることが可能ではないでしょうか。