C# 継承入門 ― WorkItem / ChangeRequest で学ぶ実践 OOP
対象読者
- C# のクラスとオブジェクトはわかるが、継承はこれから
- 実践的なコードで “派生クラスの作り方” を体感したい
目次
ベースとなる資料
0. ストーリーとコードの目的
あなたは、とある開発チームのタスク管理ツールを自作しています。
- WorkItem は “すべての作業カード” を表す基底クラス。新機能の開発でもバグ修正でも、一枚のカードにして看板(カンバン)に貼ります。
- ChangeRequest は “既存 WorkItem の設計変更を依頼するカード”。「このクラスにプロパティを追加してほしい」「処理時間を短縮してほしい」といったリファクタリング系の要望を追跡します。
目的は 「共通情報を再利用しつつ、種類ごとの追加情報だけをシンプルに拡張できる継承設計」 を体験すること。
- 自動採番 によってタスク ID を一意に保ち、
protected
メンバー で派生クラスにもアクセスを許可し、override
で出力フォーマットを統一しながら多態性を担保します。
1. 何を作るのか
- WorkItem : ID・タイトル・説明・所要時間を管理する “基底クラス”
- ChangeRequest : WorkItem を継承し、元タスクを示す
originalItemID
を追加した “派生クラス”
ポイント
- 再利用 : 共通ロジックをそのまま活用
- 拡張 : ChangeRequest 独自のプロパティを追加
2. 完成コード全体
// ─── 基底クラス ─────────────────────────────────
public class WorkItem
{
private static int currentID;
protected int ID { get; set; }
protected string Title { get; set; }
protected string Description { get; set; }
protected TimeSpan JobLength { get; set; }
static WorkItem() => currentID = 0; // 静的コンストラクタ
public WorkItem() // 既定コンストラクタ
{
ID = 0;
Title = "Default title";
Description = "Default description.";
JobLength = TimeSpan.Zero;
}
public WorkItem(string title, string desc, TimeSpan joblen)
{
ID = GetNextID();
Title = title;
Description = desc;
JobLength = joblen;
}
protected int GetNextID() => ++currentID;
public void Update(string title, TimeSpan joblen)
{
Title = title;
JobLength = joblen;
}
public override string ToString() => $"{ID} - {Title}";
}
// ─── 派生クラス ────────────────────────────────
public class ChangeRequest : WorkItem
{
protected int OriginalItemID { get; set; }
public ChangeRequest() { } // 既定コンストラクタ
public ChangeRequest(string title, string desc,
TimeSpan jobLen, int originalID)
{
ID = GetNextID(); // ← 継承メンバー
Title = title;
Description = desc;
JobLength = jobLen;
OriginalItemID = originalID; // ← 派生専用
}
}
3. コード解剖
観点 | コード例 | ポイント |
---|---|---|
静的フィールド | private static int currentID; | 採番カウンタを全インスタンスで共有 |
静的コンストラクタ | static WorkItem() | 最初のアクセス時に 1 回だけ実行 |
保護レベル protected | protected int ID { get; set; } | 派生クラスから見えるが外部には公開しない |
自動採番 | GetNextID() | インスタンス生成毎に currentID ++ |
メソッドの再利用 | change.Update(...) | 派生クラスでもそのまま使える |
多態性 | override string ToString() | ChangeRequest も同じ出力形式 |
4. 動かしてみる
var item = new WorkItem(
"バグ修正",
"自分のコードブランチのすべてのバグを修正する",
new TimeSpan(3, 4, 0, 0));
var change = new ChangeRequest(
"基底クラスの設計変更",
"クラスにメンバーを追加する",
new TimeSpan(4, 0, 0),
1);
Console.WriteLine(item); // 1 - バグ修正
change.Update(
"基底クラスの設計を変更する",
new TimeSpan(4, 0, 0));
Console.WriteLine(change); // 2 - 基底クラスの設計を変更する
確認ポイント
- ID が連番で発行される
ChangeRequest
がUpdate
とToString
を継承利用
5. 設計のコツ
- “is-a” 判定 ― ChangeRequest is a WorkItem → 継承で OK
- 基底クラスは最小公倍数 ― 共通する“本質”だけ
- 静的 vs. インスタンス責務 ― 採番は静的、内容はインスタンス
override
を忘れずに ― 共通 API を崩さず振る舞い変更
6. よくある質問
Q | A |
---|---|
派生コンストラクタから base(...) を呼ばないのは? | 基底が既定コンストラクタを持つので暗黙呼び出し可。パラメーター付きのみなら base(...) 必須。 |
new でメンバー隠蔽した方が早い? | 多態性が失われるため基本は override 推奨。 |
静的フィールドはスレッド安全? | 単純インクリメントならほぼ問題なし。厳密さが必要なら Interlocked.Increment を使用。 |
7. 練習課題 ✍️
難易度 | 課題 |
---|---|
★☆☆(初級) | ChangeRequest に Priority プロパティ(Low / Medium / High)を追加し、Update で編集できるようにせよ。 |
★★☆(中級) | WorkItem を継承した BugTicket を実装し、Severity 列挙体 (Critical / Major / Minor) を保持。ToString を [Critical] 3 - NullReferenceException の形式にオーバーライドせよ。 |
★★★(上級) | WorkItem を abstract クラス に変更し、抽象メソッド decimal CalculateCost() を追加。ChangeRequest と BugTicket で独自計算ロジックを実装し、List<WorkItem> に混在させて合計コストを求めるテストコードを書け。 |
★☆☆(初級) Priority プロパティ追加
public enum Priority { Low, Medium, High }
public class ChangeRequest : WorkItem
{
public Priority Priority { get; set; } = Priority.Medium;
public ChangeRequest(string title, string desc, TimeSpan jobLen, int originalID, Priority priority = Priority.Medium)
: base(title, desc, jobLen)
{
OriginalItemID = originalID;
Priority = priority;
}
// 基底の Update を拡張
public void Update(string title, TimeSpan jobLen, Priority priority)
{
base.Update(title, jobLen);
Priority = priority;
}
public override string ToString() => $"[{Priority}] {base.ToString()}";
}
★★☆(中級) BugTicket クラス実装
public enum Severity { Critical, Major, Minor }
public class BugTicket : WorkItem
{
public Severity Severity { get; }
public BugTicket(string title, string desc, TimeSpan jobLen, Severity severity)
: base(title, desc, jobLen)
{
Severity = severity;
}
public override string ToString() => $"[{Severity}] {ID} - {Title}";
}
★★★(上級) 抽象メソッド CalculateCost 実装
// 抽象基底クラス化
public abstract class WorkItem
{
// 既存メンバーはそのまま…
public abstract decimal CalculateCost();
}
// ChangeRequest のコスト計算(時給 8,000 円換算)
public override decimal CalculateCost() => (decimal)JobLength.TotalHours * 8000m;
// BugTicket のコスト計算:深刻度でレートを変更
public override decimal CalculateCost() => Severity switch
{
Severity.Critical => (decimal)JobLength.TotalHours * 10000m,
Severity.Major => (decimal)JobLength.TotalHours * 7000m,
Severity.Minor => (decimal)JobLength.TotalHours * 5000m,
_ => 0m
};
テストコード
var tasks = new List<WorkItem>
{
new ChangeRequest("UI 改善", "アラート文言を変更", TimeSpan.FromHours(2), 5, Priority.Low),
new BugTicket("NullReferenceException 修正", "起動直後に落ちる", TimeSpan.FromHours(3), Severity.Critical)
};
decimal total = tasks.Sum(t => t.CalculateCost());
Console.WriteLine($"合計コスト: {total:C0}");
8. まとめ
- 継承は 再利用 と 拡張 を両立する強力な道具
- ただし “is-a” が成り立つか、基底クラスが肥大しすぎていないかを常に確認
- 実際にコードを書いて 自動採番 と
override
の動作 を体験すると理解が深まる
訪問数 3 回, 今日の訪問数 1回
ディスカッション
コメント一覧
まだ、コメントがありません