抽象クラスが解決する問題
プログラムが複雑になってくると、「共通の構造は決まっているが、具体的な実装は子クラスに任せたい」という場面が出てきます。
たとえば、図形を扱うシステムを考えてみてください。すべての図形には「面積を計算する」「周囲の長さを計算する」という共通の機能が必要ですが、計算方法は図形によって異なります。また、「図形」自体は抽象的な概念であり、直接インスタンスを作ることは意味がありません。
// 問題のある設計:通常のクラスでは不完全な実装を許してしまう
class Shape {
protected color: string;
constructor(color: string) {
this.color = color;
}
// 基底クラスではどう実装すればよいかわからない
calculateArea(): number {
throw new Error("子クラスで実装してください");
}
// これも同様
getPerimeter(): number {
throw new Error("子クラスで実装してください");
}
}
// この設計では、実装忘れや不完全な実装を防げない
const shape = new Shape("赤"); // 意味のないインスタンスが作成できてしまう
抽象クラスを使うことで、このような問題を解決できます。
抽象クラスの基本概念
抽象クラスは、「直接インスタンス化できないクラス」で、他のクラスの基底クラスとしてのみ機能します。抽象メソッドを含めることで、子クラスに特定のメソッドの実装を強制できます。
// abstract キーワードでクラスを抽象クラスにする
abstract class Shape {
protected color: string;
constructor(color: string) {
this.color = color;
}
// abstract を付けたメソッドは子クラスで必ず実装する必要がある
abstract calculateArea(): number;
abstract getPerimeter(): number;
// 通常のメソッドも定義できる(共通の実装)
getColor(): string {
return this.color;
}
displayInfo(): string {
// 抽象メソッドを呼び出している
// 実際の処理は子クラスの実装が使われる
return `色: ${this.color}, 面積: ${this.calculateArea()}`;
}
}
// 円クラス:Shapeを継承し、抽象メソッドを実装
class Circle extends Shape {
private radius: number;
constructor(color: string, radius: number) {
super(color); // 親クラスのコンストラクタを呼び出し
this.radius = radius;
}
// 抽象メソッドの実装(必須)
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
// Circle独自のメソッドも追加できる
getDiameter(): number {
return this.radius * 2;
}
}
// 長方形クラス:同じくShapeを継承
class Rectangle extends Shape {
private width: number;
private height: number;
constructor(color: string, width: number, height: number) {
super(color);
this.width = width;
this.height = height;
}
// 抽象メソッドの実装(必須)
calculateArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
// 使用例
// const shape = new Shape("赤"); // エラー:抽象クラスはインスタンス化できない
const circle = new Circle("赤", 5);
const rectangle = new Rectangle("青", 10, 20);
console.log(circle.displayInfo()); // "色: 赤, 面積: 78.54"(約)
console.log(rectangle.displayInfo()); // "色: 青, 面積: 200"
// 型としてはShapeとして扱えるため、ポリモーフィズムが実現できる
const shapes: Shape[] = [circle, rectangle];
shapes.forEach(shape => {
console.log(shape.displayInfo()); // それぞれの実装が呼ばれる
});
抽象クラスの利点は次の通りです
- 設計の強制: 子クラスで必ず実装すべきメソッドを明確にできる
- 共通実装の提供: すべての子クラスで共通の処理を基底クラスに集約できる
- 型安全性: 抽象クラス型として扱うことで、インターフェースの一貫性を保証できる
抽象プロパティの活用
メソッドだけでなく、プロパティも抽象として定義できます。これは、子クラスで必ず提供すべき情報を明確にする際に有用です。
abstract class GameCharacter {
protected name: string;
protected level: number;
// 抽象プロパティ:子クラスで必ず定義する必要がある
abstract readonly characterType: string;
abstract maxHealth: number;
constructor(name: string, level: number = 1) {
this.name = name;
this.level = level;
}
// 抽象メソッド:子クラスで実装が必要
abstract attack(): number;
abstract defend(damage: number): number;
// 共通メソッド:すべての子クラスで使用可能
levelUp(): void {
this.level++;
this.maxHealth += 10; // 抽象プロパティを使用
console.log(`${this.name}がレベルアップしました! レベル: ${this.level}`);
}
getInfo(): string {
// 抽象プロパティを使用して情報を表示
return `${this.characterType}: ${this.name} (レベル ${this.level}, 最大HP: ${this.maxHealth})`;
}
}
// 戦士クラス
class Warrior extends GameCharacter {
// 抽象プロパティの実装
readonly characterType = "戦士";
maxHealth = 100;
private strength: number;
constructor(name: string, strength: number = 10) {
super(name);
this.strength = strength;
}
// 抽象メソッドの実装
attack(): number {
const damage = this.strength * 2;
console.log(`${this.name}が${damage}のダメージを与えました!`);
return damage;
}
defend(damage: number): number {
// 戦士は防御力が高い
const actualDamage = Math.max(0, damage - this.strength);
console.log(`${this.name}は${actualDamage}のダメージを受けました`);
return actualDamage;
}
// Warrior独自のメソッド
shield(): void {
console.log(`${this.name}が盾を構えました!`);
}
}
// 魔法使いクラス
class Mage extends GameCharacter {
// 抽象プロパティの実装
readonly characterType = "魔法使い";
maxHealth = 60;
private intelligence: number;
private mana: number;
constructor(name: string, intelligence: number = 15) {
super(name);
this.intelligence = intelligence;
this.mana = intelligence * 5; // 知力に比例してマナを設定
}
// 抽象メソッドの実装
attack(): number {
if (this.mana >= 10) {
this.mana -= 10;
const damage = this.intelligence * 3;
console.log(`${this.name}が魔法で${damage}のダメージを与えました!(残りマナ: ${this.mana})`);
return damage;
} else {
console.log(`${this.name}のマナが足りません`);
return 0;
}
}
defend(damage: number): number {
// 魔法使いは物理防御が低い
console.log(`${this.name}は${damage}のダメージを受けました`);
return damage;
}
// Mage独自のメソッド
castSpell(): number {
if (this.mana >= 20) {
this.mana -= 20;
const damage = this.intelligence * 4;
console.log(`${this.name}が強力な呪文を唱えました!ダメージ: ${damage}`);
return damage;
} else {
console.log(`${this.name}のマナが足りません`);
return 0;
}
}
restoreMana(): void {
this.mana = this.intelligence * 5;
console.log(`${this.name}のマナが回復しました`);
}
}
// 使用例
const warrior = new Warrior("アーサー", 15);
const mage = new Mage("マーリン", 20);
console.log(warrior.getInfo()); // "戦士: アーサー (レベル 1, 最大HP: 100)"
console.log(mage.getInfo()); // "魔法使い: マーリン (レベル 1, 最大HP: 60)"
warrior.attack(); // "アーサーが30のダメージを与えました!"
mage.castSpell(); // "マーリンが強力な呪文を唱えました!ダメージ: 80"
// ポリモーフィズム:同じ抽象クラス型として扱える
const party: GameCharacter[] = [warrior, mage];
party.forEach(character => {
character.levelUp(); // それぞれのキャラクターがレベルアップ
});
抽象プロパティを使用する際は、子クラスのコンストラクタで値を設定する前に親クラスのメソッド(levelUpなど)が呼ばれる可能性を考慮する必要があります。初期化の順序に注意してください。
インターフェースとクラスの関係
インターフェースとクラスを組み合わせることで、より柔軟な設計が可能になります。インターフェースは「契約」を定義し、クラスはその契約を実装します。
インターフェースの実装
一つのクラスで複数のインターフェースを実装することで、複数の能力を持つオブジェクトを作成できます。これは、現実世界の「アヒルは飛べるし泳げる」といった複数の能力を表現する際に有用です。
// 飛行能力のインターフェース
interface Flyable {
altitude: number; // 現在の高度
fly(): void; // 飛行開始
land(): void; // 着陸
getFlightStatus(): string; // 飛行状態の取得
}
// 泳ぐ能力のインターフェース
interface Swimmable {
depth: number; // 現在の深度
swim(): void; // 泳ぎ開始
surface(): void; // 水面に浮上
getSwimStatus(): string; // 泳ぎ状態の取得
}
// 複数のインターフェースを実装するクラス
class Duck implements Flyable, Swimmable {
// Flyableインターフェースの実装
altitude: number = 0;
fly(): void {
this.altitude = 100;
console.log("アヒルが飛び立ちました");
}
land(): void {
this.altitude = 0;
console.log("アヒルが着陸しました");
}
getFlightStatus(): string {
return this.altitude > 0 ? `高度${this.altitude}mで飛行中` : "陸上にいます";
}
// Swimmableインターフェースの実装
depth: number = 0;
swim(): void {
this.depth = 2;
console.log("アヒルが泳ぎ始めました");
}
surface(): void {
this.depth = 0;
console.log("アヒルが水面に出ました");
}
getSwimStatus(): string {
return this.depth > 0 ? `水深${this.depth}mで泳ぎ中` : "水面または陸上にいます";
}
// Duck独自のメソッド
quack(): void {
console.log("ガーガー");
}
// アヒルの総合ステータス
getFullStatus(): string {
return `${this.getFlightStatus()}, ${this.getSwimStatus()}`;
}
}
// 飛行のみ可能な鳥
class Eagle implements Flyable {
altitude: number = 0;
fly(): void {
this.altitude = 500; // ワシはより高く飛ぶ
console.log("ワシが舞い上がりました");
}
land(): void {
this.altitude = 0;
console.log("ワシが降り立ちました");
}
getFlightStatus(): string {
return this.altitude > 0 ? `高度${this.altitude}mで飛行中` : "止まっています";
}
hunt(): void {
if (this.altitude > 0) {
console.log("ワシが獲物を狙っています");
} else {
console.log("ワシは飛行中でないと狩りができません");
}
}
}
// 使用例
const duck = new Duck();
const eagle = new Eagle();
duck.fly();
duck.swim();
duck.quack();
console.log(duck.getFullStatus());
eagle.fly();
eagle.hunt();
// インターフェース型として扱う(ポリモーフィズム)
const flyingAnimals: Flyable[] = [duck, eagle];
flyingAnimals.forEach(animal => {
animal.fly();
console.log(animal.getFlightStatus());
});
コンストラクタインターフェース
時には、クラスのコンストラクタの形状を定義したい場合があります。これは、ファクトリーパターンなどで、異なるクラスを統一的に扱いたい時に有用です。
// 時計の動作を定義するインターフェース
interface ClockInterface {
currentTime: Date;
tick(): void;
getTime(): string;
}
// コンストラクタの形状を定義するインターフェース
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
// デジタル時計の実装
class DigitalClock implements ClockInterface {
currentTime: Date;
constructor(hour: number, minute: number) {
this.currentTime = new Date();
this.currentTime.setHours(hour, minute, 0, 0);
console.log(`デジタル時計を作成: ${hour}:${minute.toString().padStart(2, '0')}`);
}
tick(): void {
this.currentTime.setSeconds(this.currentTime.getSeconds() + 1);
console.log("ピッ");
}
getTime(): string {
const hours = this.currentTime.getHours().toString().padStart(2, '0');
const minutes = this.currentTime.getMinutes().toString().padStart(2, '0');
const seconds = this.currentTime.getSeconds().toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
// デジタル時計独自の機能
setAlarm(hour: number, minute: number): void {
console.log(`アラームを${hour}:${minute.toString().padStart(2, '0')}に設定しました`);
}
}
// アナログ時計の実装
class AnalogClock implements ClockInterface {
currentTime: Date;
constructor(hour: number, minute: number) {
this.currentTime = new Date();
this.currentTime.setHours(hour, minute, 0, 0);
console.log(`アナログ時計を作成: ${hour}:${minute.toString().padStart(2, '0')}`);
}
tick(): void {
this.currentTime.setSeconds(this.currentTime.getSeconds() + 1);
console.log("チクタク");
}
getTime(): string {
const hours = this.currentTime.getHours();
const minutes = this.currentTime.getMinutes();
const hourAngle = (hours % 12) * 30 + minutes * 0.5; // 時針の角度
const minuteAngle = minutes * 6; // 分針の角度
return `時針: ${hourAngle}度, 分針: ${minuteAngle}度`;
}
}
// ファクトリー関数:コンストラクタインターフェースを使用
function createClock(
clockConstructor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new clockConstructor(hour, minute);
}
// 使用例
const digitalClock = createClock(DigitalClock, 12, 17);
const analogClock = createClock(AnalogClock, 7, 32);
digitalClock.tick();
console.log("デジタル:", digitalClock.getTime());
analogClock.tick();
console.log("アナログ:", analogClock.getTime());
// 時計の配列として統一的に扱える
const clocks: ClockInterface[] = [digitalClock, analogClock];
clocks.forEach(clock => {
clock.tick();
console.log(clock.getTime());
});
コンストラクタインターフェースを使用することで、異なるクラスを統一的にインスタンス化でき、ファクトリーパターンやプラグインシステムの実装に活用できます。
ジェネリクスの必要性
プログラミングでは、同じロジックを異なる型に対して適用したい場面がよくあります。たとえば、「配列に要素を追加する」「要素を検索する」といった操作は、数値の配列でも文字列の配列でも同じロジックです。
ジェネリクスを使わない場合、型ごとに同じようなクラスを作成する必要があります
// 文字列専用のコンテナ
class StringContainer {
private items: string[] = [];
add(item: string): void {
this.items.push(item);
}
get(index: number): string | undefined {
return this.items[index];
}
getAll(): string[] {
return [...this.items];
}
}
// 数値専用のコンテナ(ほぼ同じコード)
class NumberContainer {
private items: number[] = [];
add(item: number): void {
this.items.push(item);
}
get(index: number): number | undefined {
return this.items[index];
}
getAll(): number[] {
return [...this.items];
}
}
// この方法では、新しい型に対応するたびに新しいクラスが必要になる
ジェネリクスを使うことで、この問題を解決できます。
ジェネリククラスの基本
ジェネリクスは、「型をパラメータとして受け取る」仕組みです。<T>
のような型パラメータを使用して、汎用的なクラスを作成できます。
// ジェネリッククラス:型パラメータTを使用
class Container<T> {
private items: T[] = []; // Tの配列
// Tの値を追加
add(item: T): void {
this.items.push(item);
}
// Tの値を取得(存在しない場合はundefined)
get(index: number): T | undefined {
return this.items[index];
}
// すべてのTの値を取得
getAll(): T[] {
return [...this.items]; // 配列のコピーを返す
}
// 要素数を取得
size(): number {
return this.items.length;
}
// 特定の要素が含まれているかチェック
contains(item: T): boolean {
return this.items.includes(item);
}
// 全要素をクリア
clear(): void {
this.items = [];
}
// 条件に合う要素を検索
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
// 条件に合う要素をすべて検索
filter(predicate: (item: T) => boolean): T[] {
return this.items.filter(predicate);
}
}
// 使用時に具体的な型を指定
const stringContainer = new Container<string>();
stringContainer.add("Hello");
stringContainer.add("World");
console.log(stringContainer.getAll()); // ["Hello", "World"]
const numberContainer = new Container<number>();
numberContainer.add(1);
numberContainer.add(2);
numberContainer.add(3);
console.log(numberContainer.getAll()); // [1, 2, 3]
// 型安全性が保たれる
// stringContainer.add(123); // エラー:stringのコンテナにnumberは追加できない
// オブジェクトの配列も扱える
interface User {
id: number;
name: string;
email: string;
}
const userContainer = new Container<User>();
userContainer.add({ id: 1, name: "田中", email: "[email protected]" });
userContainer.add({ id: 2, name: "佐藤", email: "[email protected]" });
// 型安全な検索
const user = userContainer.find(u => u.id === 1);
console.log(user?.name); // "田中"
制約付きジェネリクス
時には、ジェネリック型に特定の条件を満たすよう制約を設けたい場合があります。extends
キーワードを使用して制約を定義できます。
// 識別可能なオブジェクトのインターフェース
interface Identifiable {
id: number;
}
// IDを持つオブジェクトのみを扱うリポジトリクラス
class Repository<T extends Identifiable> {
private items: Map<number, T> = new Map();
// オブジェクトを保存(IDをキーとして使用)
save(item: T): void {
this.items.set(item.id, item); // Tがidプロパティを持つことが保証されている
console.log(`ID ${item.id} のアイテムを保存しました`);
}
// IDで検索
findById(id: number): T | undefined {
return this.items.get(id);
}
// すべてのアイテムを取得
findAll(): T[] {
return Array.from(this.items.values());
}
// アイテムを削除
delete(id: number): boolean {
const existed = this.items.has(id);
this.items.delete(id);
if (existed) {
console.log(`ID ${id} のアイテムを削除しました`);
}
return existed;
}
// アイテムを更新
update(item: T): boolean {
if (this.items.has(item.id)) {
this.items.set(item.id, item);
console.log(`ID ${item.id} のアイテムを更新しました`);
return true;
}
console.log(`ID ${item.id} のアイテムが見つかりません`);
return false;
}
// 条件に合うアイテムを検索
findWhere(predicate: (item: T) => boolean): T[] {
return this.findAll().filter(predicate);
}
// 件数を取得
count(): number {
return this.items.size;
}
}
// Identifiableを満たすインターフェースを定義
interface User extends Identifiable {
id: number; // Identifiableから継承
name: string;
email: string;
isActive: boolean;
}
interface Product extends Identifiable {
id: number; // Identifiableから継承
name: string;
price: number;
category: string;
}
// 制約を満たすオブジェクトのリポジトリを作成
const userRepository = new Repository<User>();
const productRepository = new Repository<Product>();
// ユーザーの操作
userRepository.save({ id: 1, name: "田中", email: "[email protected]", isActive: true });
userRepository.save({ id: 2, name: "佐藤", email: "[email protected]", isActive: false });
// 商品の操作
productRepository.save({ id: 1, name: "ノートPC", price: 80000, category: "電子機器" });
productRepository.save({ id: 2, name: "キーボード", price: 5000, category: "電子機器" });
// 型安全な検索
const activeUsers = userRepository.findWhere(user => user.isActive);
const cheapProducts = productRepository.findWhere(product => product.price < 10000);
console.log("アクティブユーザー:", activeUsers);
console.log("安い商品:", cheapProducts);
// 制約により、IDを持たないオブジェクトはコンパイルエラーになる
// const invalidRepository = new Repository<string>(); // エラー:stringはIdentifiableを満たさない
制約付きジェネリクスでは、型パラメータが指定された制約を満たしていない場合、コンパイルエラーが発生します。これで実行時エラーを事前に防げます。
複数の型パラメータ
一つのクラスで複数の型パラメータを使用することも可能です。これは、異なる型の組み合わせを扱う場合に有効かと思います。
// キーと値の型を別々に指定できるキーバリューストア
class KeyValueStore<K, V> {
private store: Map<K, V> = new Map();
// キーバリューペアを設定
set(key: K, value: V): void {
this.store.set(key, value);
console.log(`キー "${key}" に値を設定しました`);
}
// キーから値を取得
get(key: K): V | undefined {
return this.store.get(key);
}
// キーが存在するかチェック
has(key: K): boolean {
return this.store.has(key);
}
// すべてのキーを取得
keys(): K[] {
return Array.from(this.store.keys());
}
// すべての値を取得
values(): V[] {
return Array.from(this.store.values());
}
// すべてのエントリを取得
entries(): [K, V][] {
return Array.from(this.store.entries());
}
// サイズを取得
size(): number {
return this.store.size;
}
// ストアをクリア
clear(): void {
this.store.clear();
console.log("ストアをクリアしました");
}
// 条件に合うエントリを検索
findEntries(predicate: (key: K, value: V) => boolean): [K, V][] {
return this.entries().filter(([key, value]) => predicate(key, value));
}
}
// 異なる型の組み合わせで使用
const stringToNumberStore = new KeyValueStore<string, number>();
stringToNumberStore.set("age", 25);
stringToNumberStore.set("score", 100);
stringToNumberStore.set("level", 5);
console.log("年齢:", stringToNumberStore.get("age")); // 25
const numberToStringStore = new KeyValueStore<number, string>();
numberToStringStore.set(1, "first");
numberToStringStore.set(2, "second");
numberToStringStore.set(3, "third");
console.log("位置2:", numberToStringStore.get(2)); // "second"
// 複雑なオブジェクトの組み合わせも可能
interface UserProfile {
name: string;
bio: string;
avatar: string;
}
const userProfiles = new KeyValueStore<number, UserProfile>();
userProfiles.set(1, {
name: "田中太郎",
bio: "TypeScript開発者",
avatar: "avatar1.jpg"
});
// 型安全な検索
const profiles = userProfiles.findEntries((id, profile) =>
profile.bio.includes("TypeScript")
);
console.log("TypeScript開発者:", profiles);
ジェネリックメソッド
クラス全体をジェネリックにするのではなく、個別のメソッドのみをジェネリックにすることも可能です。
class DataProcessor {
// 汎用的なデータ変換メソッド
process<T, U>(
data: T[], // 入力データの配列
transformer: (item: T) => U, // 変換関数
filter?: (item: U) => boolean // オプションのフィルタ関数
): U[] {
console.log(`${data.length}件のデータを処理開始`);
// 1. データを変換
let result = data.map(transformer);
console.log(`変換完了: ${result.length}件`);
// 2. フィルタが指定されている場合は適用
if (filter) {
result = result.filter(filter);
console.log(`フィルタ適用後: ${result.length}件`);
}
return result;
}
// データをグループ化するメソッド
groupBy<T, K extends keyof any>(
array: T[],
keyFunction: (item: T) => K
): Record<K, T[]> {
console.log(`${array.length}件のデータをグループ化開始`);
return array.reduce((groups, item) => {
const key = keyFunction(item);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {} as Record<K, T[]>);
}
// 配列から統計情報を計算
getStatistics<T>(
array: T[],
valueExtractor: (item: T) => number
): { min: number; max: number; average: number; total: number } {
if (array.length === 0) {
throw new Error("空の配列から統計を計算することはできません");
}
const values = array.map(valueExtractor);
const total = values.reduce((sum, val) => sum + val, 0);
const min = Math.min(...values);
const max = Math.max(...values);
const average = total / values.length;
return { min, max, average, total };
}
}
const processor = new DataProcessor();
// 使用例1: 数値の2倍にして、5より大きいもののみ取得
const numbers = [1, 2, 3, 4, 5];
const doubled = processor.process(
numbers,
n => n * 2, // number → number の変換
n => n > 5 // 5より大きいもののみ
);
console.log("2倍して5より大きい数:", doubled); // [6, 8, 10]
// 使用例2: ユーザーオブジェクトから名前を抽出し、特定条件でフィルタ
interface Employee {
id: number;
name: string;
age: number;
department: string;
salary: number;
}
const employees: Employee[] = [
{ id: 1, name: "Alice", age: 25, department: "Engineering", salary: 80000 },
{ id: 2, name: "Bob", age: 30, department: "Marketing", salary: 70000 },
{ id: 3, name: "Charlie", age: 28, department: "Engineering", salary: 85000 },
{ id: 4, name: "Diana", age: 35, department: "Sales", salary: 75000 }
];
// Employee → string の変換(名前を大文字に)
const upperCaseNames = processor.process(
employees,
emp => emp.name.toUpperCase(), // Employee → string
name => name.length > 3 // 3文字より長い名前のみ
);
console.log("大文字の名前(3文字超):", upperCaseNames); // ["ALICE", "CHARLIE", "DIANA"]
// 部署別グループ化
const groupedByDepartment = processor.groupBy(
employees,
emp => emp.department
);
console.log("部署別グループ:", groupedByDepartment);
// 給与の統計情報
const salaryStats = processor.getStatistics(
employees,
emp => emp.salary
);
console.log("給与統計:", salaryStats);
// { min: 70000, max: 85000, average: 77500, total: 310000 }
全部のせした例
抽象クラス、インターフェース、ジェネリクスを組み合わせた、実際の開発で使えるような設計例を見てみましょう。
// シリアライゼーション(データの文字列化)のインターフェース
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
// IDを持つオブジェクトのインターフェース
interface Entity {
id: string | number;
}
// 基底エンティティクラス(抽象クラス + ジェネリクス)
abstract class BaseEntity<TId extends string | number> implements Serializable, Entity {
protected constructor(protected id: TId) {}
// 抽象メソッド:子クラスで実装が必要
abstract serialize(): string;
abstract deserialize(data: string): void;
abstract clone(): BaseEntity<TId>;
// 共通メソッド:すべての子クラスで利用可能
getId(): TId {
return this.id;
}
// IDの型に基づいた検証
protected validateId(id: TId): boolean {
if (typeof id === 'string') {
return id.length > 0;
} else if (typeof id === 'number') {
return id > 0;
}
return false;
}
}
// ユーザークラス(数値IDを使用)
class User extends BaseEntity<number> {
constructor(
id: number,
private name: string,
private email: string,
private createdAt: Date = new Date()
) {
super(id);
if (!this.validateId(id)) {
throw new Error("ユーザーIDは0より大きい数値である必要があります");
}
}
serialize(): string {
return JSON.stringify({
id: this.id,
name: this.name,
email: this.email,
createdAt: this.createdAt.toISOString(),
type: 'User'
});
}
deserialize(data: string): void {
const parsed = JSON.parse(data);
if (parsed.type !== 'User') {
throw new Error("Userクラスではないデータをデシリアライズしようとしました");
}
this.id = parsed.id;
this.name = parsed.name;
this.email = parsed.email;
this.createdAt = new Date(parsed.createdAt);
}
clone(): User {
return new User(this.id, this.name, this.email, new Date(this.createdAt));
}
// User固有のメソッド
getName(): string {
return this.name;
}
getEmail(): string {
return this.email;
}
updateEmail(newEmail: string): void {
if (newEmail.includes('@')) {
this.email = newEmail;
} else {
throw new Error("有効なメールアドレスを入力してください");
}
}
}
// 商品クラス(文字列IDを使用)
class Product extends BaseEntity<string> {
constructor(
id: string,
private name: string,
private price: number,
private category: string
) {
super(id);
if (!this.validateId(id)) {
throw new Error("商品IDは空でない文字列である必要があります");
}
}
serialize(): string {
return JSON.stringify({
id: this.id,
name: this.name,
price: this.price,
category: this.category,
type: 'Product'
});
}
deserialize(data: string): void {
const parsed = JSON.parse(data);
if (parsed.type !== 'Product') {
throw new Error("Productクラスではないデータをデシリアライズしようとしました");
}
this.id = parsed.id;
this.name = parsed.name;
this.price = parsed.price;
this.category = parsed.category;
}
clone(): Product {
return new Product(this.id, this.name, this.price, this.category);
}
// Product固有のメソッド
getName(): string {
return this.name;
}
getPrice(): number {
return this.price;
}
getCategory(): string {
return this.category;
}
applyDiscount(percentage: number): void {
if (percentage >= 0 && percentage <= 100) {
this.price = this.price * (1 - percentage / 100);
} else {
throw new Error("割引率は0から100の間である必要があります");
}
}
}
// 汎用リポジトリクラス(抽象エンティティを扱う)
class EntityRepository<TEntity extends BaseEntity<any>> {
private entities: Map<string | number, TEntity> = new Map();
save(entity: TEntity): void {
this.entities.set(entity.getId(), entity);
console.log(`エンティティ(ID: ${entity.getId()})を保存しました`);
}
findById(id: string | number): TEntity | undefined {
return this.entities.get(id);
}
findAll(): TEntity[] {
return Array.from(this.entities.values());
}
delete(id: string | number): boolean {
const result = this.entities.delete(id);
if (result) {
console.log(`エンティティ(ID: ${id})を削除しました`);
}
return result;
}
count(): number {
return this.entities.size;
}
// シリアライゼーション機能
exportAll(): string {
const serializedEntities = this.findAll().map(entity => entity.serialize());
return JSON.stringify(serializedEntities);
}
// 複製機能
cloneEntity(id: string | number): TEntity | undefined {
const entity = this.findById(id);
return entity ? entity.clone() as TEntity : undefined;
}
}
// 使用例
console.log("=== エンティティシステムのデモ ===");
// リポジトリの作成
const userRepository = new EntityRepository<User>();
const productRepository = new EntityRepository<Product>();
// ユーザーの作成と保存
const user1 = new User(1, "田中太郎", "[email protected]");
const user2 = new User(2, "佐藤花子", "[email protected]");
userRepository.save(user1);
userRepository.save(user2);
// 商品の作成と保存
const product1 = new Product("LAPTOP-001", "ノートPC", 80000, "電子機器");
const product2 = new Product("MOUSE-002", "ワイヤレスマウス", 3000, "電子機器");
productRepository.save(product1);
productRepository.save(product2);
// 検索とデータ操作
const foundUser = userRepository.findById(1);
if (foundUser) {
console.log(`ユーザー発見: ${foundUser.getName()}`);
foundUser.updateEmail("[email protected]");
}
const foundProduct = productRepository.findById("LAPTOP-001");
if (foundProduct) {
console.log(`商品発見: ${foundProduct.getName()} - ¥${foundProduct.getPrice()}`);
foundProduct.applyDiscount(10); // 10%割引
console.log(`割引後: ¥${foundProduct.getPrice()}`);
}
// 複製機能のテスト
const clonedUser = userRepository.cloneEntity(1);
if (clonedUser) {
console.log(`複製されたユーザー: ${clonedUser.getName()}`);
}
// データのエクスポート
console.log("\n=== データエクスポート ===");
console.log("ユーザーデータ:", userRepository.exportAll());
console.log("商品データ:", productRepository.exportAll());
// 統計情報
console.log(`\n総ユーザー数: ${userRepository.count()}`);
console.log(`総商品数: ${productRepository.count()}`);
まとめ
ジェネリック型を理解するとTypescript使っている感がとても増すので、Typescriptを使う人にとっては必修科目です。
しかしこれを考慮して実装している人は決して多くないのが現実です。