みなさん何となくでプログラミングを勉強していませんか?私ははじめ,なんとなくコーディングしていたのですが,実務からの学びや色々な方のお話を聞くと,言語の本質を知らないとコーディングができないなと感じます.また,AIが出してきた答えをちゃんと理解できないと実装ができない時代にもなってきたと思います.今の時代コーディングにおける力として,コードを書く力だけではなく,それに合わせてコードを読む力も大事だと思っています.(私の意見ですが...)本シリーズ(TypeScriptで学ぶプログラミングの世界)ではエンジニアとして知っておいた方がいいプログラミング言語の概念をTypescriptを用いて学び,プログラミングの世界の本質を記載していこうと思う.Part1の本記事では手続型の書き方からオブジェクト指向の書き方への変遷について考えていこうと思う.
TypeScriptで学ぶプログラミングの世界 でシリーズ化します!今回は初回の記事です
シリーズ TypeScriptで学ぶプログラミング言語の世界
Part2 ORMってなんなんだ?SQLとオブジェクト指向のミスマッチを感じませんか?
他のシリーズ記事
TypeScriptを知らない人は以下の記事から.また本記事ではオブジェクト指向に関する文法やその応用については詳しく説明しません.以下の記事からクラスの見出しを参照してください.
上の記事も〇〇チートシートとしてシリーズ化しているのでぜひご覧ください.git/ghコマンドの概念,SQL,Go言語/Gormなどの概念理解や手引きなどを記載しています.
情報処理技術者試験合格への道 [IP・SG・FE・AP]
情報処理技術者試験の単語集です.
IAM AWS User クラウドサービスをフル活用しよう!
AWSのサービスを例にしてバックエンドとインフラ開発の手法を説明するシリーズです.
そもそもプログラミングとは?
本質的には「さまざまな物事を記述すること」.
その目的は効率化やシステム開発など多岐にわたるが, 根本的には現実世界や仮想世界をどのように表現するかということである.初めてコーディングをすると,オブジェクト指向ってなんなのかわからない上に,下で説明する手続型プログラミングの書き方をする方が多いと思う.手続型は直感的で何を書いているのかが分かり易いし,順番に命令する書き方なのでコーディングをしていて今どこを書いているのかが迷わない利点があるでしょう.
手続型プログラミング:順番に指示を書く方法
手続型プログラミングは, コンピュータに一つずつ順番に指示を出す方法である. 料理のレシピのように, 順を追って作業を進めていくということである.
例えば, 学校の成績をつける簡単なプログラムを TypeScript で書いてみよう.
// 点数を保存する配列
let scores: number[] = [];
// 点数を追加する関数
function addScore(score: number): void {
scores.push(score);
}
// 平均点を計算する関数
function calculateAverage(): number {
let sum = 0;
for (let score of scores) {
sum += score;
}
return sum / scores.length;
}
// 結果を表示する関数
function printResult(): void {
console.log("点数: " + scores.join(", "));
console.log("平均点: " + calculateAverage());
}
// プログラムの使い方
addScore(80);
addScore(90);
addScore(75);
printResult();
このプログラムは, 点数を追加し, 平均点を計算し, 結果を表示する. 小さなプログラムではこの方法でも問題ないが, プログラムが大きくなると難しくなってくる.
手続型プログラミングの問題点:コード例で理解する
手続型プログラミングでは, プログラムが大きくなるにつれて様々な問題が発生する. これらの問題を具体的なコード例を用いて見てみよう.
1. 全体を把握するのが難しくなる
例えば, 学生の成績管理システムを考えてみよう.
// 学生の成績を管理するプログラム
let studentNames: string[] = [];
let mathScores: number[] = [];
let scienceScores: number[] = [];
let literatureScores: number[] = [];
// 学生を追加する関数
function addStudent(name: string, mathScore: number, scienceScore: number, literatureScore: number): void {
studentNames.push(name);
mathScores.push(mathScore);
scienceScores.push(scienceScore);
literatureScores.push(literatureScore);
}
// 平均点を計算する関数
function calculateAverage(scores: number[]): number {
let sum = 0;
for (let score of scores) {
sum += score;
}
return sum / scores.length;
}
// 成績を表示する関数
function printGrades(): void {
for (let i = 0; i < studentNames.length; i++) {
console.log(`${studentNames[i]}の成績:`);
console.log(` 数学: ${mathScores[i]}`);
console.log(` 理科: ${scienceScores[i]}`);
console.log(` 文学: ${literatureScores[i]}`);
}
}
// 科目ごとの平均点を表示する関数
function printAverages(): void {
console.log("科目ごとの平均点:");
console.log(` 数学: ${calculateAverage(mathScores)}`);
console.log(` 理科: ${calculateAverage(scienceScores)}`);
console.log(` 文学: ${calculateAverage(literatureScores)}`);
}
// プログラムの使用例
addStudent("太郎", 80, 75, 90);
addStudent("花子", 95, 80, 85);
addStudent("次郎", 70, 85, 75);
printGrades();
printAverages();
...すでに全体を把握するのが難しくなっていることがわかる. 学生のデータがいくつもの配列に分散しており, それぞれの関数が何をしているのか, どのデータを使っているのかを追跡するのが難しくなっている.
2. 同じようなコードを何度も書くことになる
上記のプログラムに新しい科目(例: 英語)を追加したいとする.
// 英語の点数を追加
let englishScores: number[] = [];
// 学生を追加する関数(更新版)
function addStudent(name: string, mathScore: number, scienceScore: number, literatureScore: number, englishScore: number): void {
studentNames.push(name);
mathScores.push(mathScore);
scienceScores.push(scienceScore);
literatureScores.push(literatureScore);
englishScores.push(englishScore); // 新しい行
}
// 成績を表示する関数(更新版)
function printGrades(): void {
for (let i = 0; i < studentNames.length; i++) {
console.log(`${studentNames[i]}の成績:`);
console.log(` 数学: ${mathScores[i]}`);
console.log(` 理科: ${scienceScores[i]}`);
console.log(` 文学: ${literatureScores[i]}`);
console.log(` 英語: ${englishScores[i]}`); // 新しい行
}
}
// 科目ごとの平均点を表示する関数(更新版)
function printAverages(): void {
console.log("科目ごとの平均点:");
console.log(` 数学: ${calculateAverage(mathScores)}`);
console.log(` 理科: ${calculateAverage(scienceScores)}`);
console.log(` 文学: ${calculateAverage(literatureScores)}`);
console.log(` 英語: ${calculateAverage(englishScores)}`); // 新しい行
}
新しい科目を追加するために, 多くの場所で同じようなコードを追加する必要があった. これは冗長で, ミスを引き起こしやすくなる.
3. 変更が難しくなる
成績の表示形式を変更したい場合を考えてみよう. 例えば, 点数を百分率で表示したいとする.
// 成績を表示する関数(百分率表示版)
function printGrades(): void {
for (let i = 0, i < studentNames.length; i++) {
console.log(`${studentNames[i]}の成績:`);
console.log(` 数学: ${mathScores[i]}%`);
console.log(` 理科: ${scienceScores[i]}%`);
console.log(` 文学: ${literatureScores[i]}%`);
console.log(` 英語: ${englishScores[i]}%`);
}
}
// 科目ごとの平均点を表示する関数(百分率表示版)
function printAverages(): void {
console.log("科目ごとの平均点:");
console.log(` 数学: ${calculateAverage(mathScores)}%`);
console.log(` 理科: ${calculateAverage(scienceScores)}%`);
console.log(` 文学: ${calculateAverage(literatureScores)}%`);
console.log(` 英語: ${calculateAverage(englishScores)}%`);
}
この変更を行うには, 複数の場所を修正する必要がある. もし一か所を変更し忘れたり, 間違って変更したりすると, プログラム全体の一貫性が失われてしまう.
4. 新しい機能を追加するのが難しい
例えば, 各学生の総合評価を計算して表示する機能を追加したいとする.
// 総合評価を計算する関数
function calculateOverallGrade(mathScore: number, scienceScore: number, literatureScore: number, englishScore: number): string {
let average = (mathScore + scienceScore + literatureScore + englishScore) / 4;
if (average >= 90) return "A";
else if (average >= 80) return "B";
else if (average >= 70) return "C";
else if (average >= 60) return "D";
else return "F";
}
// 総合評価を表示する関数
function printOverallGrades(): void {
console.log("学生の総合評価:");
for (let i = 0; i < studentNames.length; i++) {
let overallGrade = calculateOverallGrade(mathScores[i], scienceScores[i], literatureScores[i], englishScores[i]);
console.log(` ${studentNames[i]}: ${overallGrade}`);
}
}
この新しい機能を追加するために, 新しい関数を作成し, 既存のデータ構造(複数の配列)を利用する必要がある. これは既存のコードに強く依存しており, 将来的に科目を追加したり削除したりする際に, この関数も修正する必要が出てくる.
そもそも以前は大きなプログラムを書くこと自体があまりなかったり,ソロプレイで開発をすることが主流だった.そのためオブジェクト指向なんていらなかった.
手続型プログラミングで生じる問題まとめ
これらの例から, 手続型プログラミングでプログラムが大きくなると以下の問題が顕著になることがわかる.
- データと処理が分散しており, 全体の把握が難しくなる.
- 新しい要素(科目など)を追加する際, 多くの場所で同様の変更が必要になる.
- 表示形式などの変更を行う際, 複数の場所を同時に修正する必要があり, ミスが起きやすくなる.
- 新しい機能を追加する際, 既存のデータ構造や処理に強く依存してしまい, 柔軟性が低くなる.
これらの問題は, プログラムがさらに大きくなるにつれてより深刻になる. そのため, 大規模なプログラムを開発する際には, オブジェクト指向プログラミングなど, より構造化されたアプローチが必要になるのだ.
これらの問題を解決するために考え出されたのが, オブジェクト指向プログラミングである.
オブジェクト指向プログラミング:物事をまとめて考える方法
オブジェクト指向プログラミングは, 現実世界の物事をプログラムの中で表現する方法である. 物事の特徴(データ)と, その物事ができること(動作)をひとまとめにして考える.
オブジェクト指向プログラミングの主な特徴を難しい言葉で羅列した結果こんな感じ.
- オブジェクト:現実世界の「もの」や「概念」を表現する
オブジェクトは, データ(属性)と振る舞い(メソッド)を持つ.
例えば, 「車」というオブジェクトは, 色や型番といった属性と, 走る・止まるといった振る舞いを持つ.
- クラス:オブジェクトの設計図
クラスは, オブジェクトの構造と振る舞いを定義する.
クラスを基に, 具体的なオブジェクト(インスタンス)を生成する.
- カプセル化:データとその操作をひとまとめにする
オブジェクトの内部データを外部から直接操作できないようにし, メソッドを通じてアクセスする.
これにより, データの整合性を保ち, プログラムの安全性を高める.
- 継承:既存のクラスを基に新しいクラスを作成する
既存のクラス(親クラス)の特性を引き継ぎ, 新しいクラス(子クラス)を作る.
コードの再利用性を高め, 階層構造を表現できる.
- ポリモーフィズム:同じインターフェースで異なる動作を実現する
同じメソッド名で異なる処理を実行できる.
これにより, コードの柔軟性と拡張性が向上する.
と...特徴を羅列してみたが,どうもイマイチわかりにくいだろう.書き方,文法に関してや応用はTypeScriptチートシートで説明しているのでこの記事ではオブジェクト指向ってなんなの!って話をする.
クラスとインスタンス:設計図と実物
クラスは物事の設計図のようなものである. インスタンスは, その設計図から作られた実際のもの(オブジェクト)である.
「車」クラスは、自動車メーカーの設計図のようなもの
例えば,トヨタがカローラの新しいモデルを設計する際に使う設計図を想像してもらって,この設計図には、車の基本的な構造,部品,機能などが詳細に記載されているとする.
-
工場での生産
クラスからインスタンスを作ることは,設計図を基に実際の車を工場で生産することに似ている.設計図(クラス)は一つでも,それを基に多くの同じモデルの車(インスタンス)を製造できる. -
属性(プロパティ)
クラスの属性は,車の具体的な特徴に相当する.- 車種(セダン、SUV、スポーツカーなど)
- 色(赤、青、白など)
- エンジンの種類(ガソリン、ディーゼル、電気など)
- シートの数
- タイヤのサイズ
これらは,実際の車を見たときに確認できる特徴ですね.
-
振る舞い(メソッド)
クラスのメソッドは,車で実行できる操作や動作に相当する.- エンジンをかける(イグニッションキーを回す,またはスタートボタンを押す)
- アクセルを踏む(加速する)
- ブレーキを踏む(減速する)
- ハンドルを回す(方向を変える)
- クラクションを鳴らす
これらは,実際に車を運転するときに行う操作ですね.
このように 「車」クラスは現実世界の車の概念をプログラミングの世界に抽象化したものと考えることができる.そしてその設計図(クラス)に合わせてできた車そのものをインスタンスと呼ぶ.
この考えをプログラムで表すと...例えば, 「車」というクラス(設計図)を TypeScript で書いてみよう.
一部だけ書きます
// 車の設計図
class Car {
// 車の特徴(データ) これが属性(プロパティ)
private name: string;
private color: string;
private speed: number;
// 車を作るときの初期設定
constructor(name: string, color: string) {
this.name = name;
this.color = color;
this.speed = 0;
}
// 車ができること(動作):加速する これが振る舞い(メソッド)
accelerate(amount: number): void {
this.speed += amount;
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
// 車ができること(動作):ブレーキをかける これが振る舞い(メソッド)
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
}
// 実際の車を作る これがインスタンス
let myCar = new Car("プリウス", "白");
let friendsCar = new Car("フィット", "赤");
// 車を動かす
myCar.accelerate(50);
friendsCar.accelerate(60);
myCar.brake(20);
この例では, Car クラスが車の設計図になっている. myCar と friendsCar は, その設計図から作られた実際の車(インスタンス)である. 各車は自分の名前, 色, 速度を持ち, 加速したり減速したりできる.
インターフェース:約束事を決める
TypeScript では, インターフェースを使って, クラスが持つべき機能を定義できる. これは, クラスが守るべき約束事のようなものである.
インターフェースは,「乗り物」としての基本的な要件や規格を定めた法律や規制のようなもの.例えば,道路交通法のような法律が,すべての車両に対して「加速する機能」と「減速する機能」を持つことを義務付けているようなイメージ.
これをTypeScriptで表すと?
// 乗り物の約束事を定義するインターフェース
interface Vehicle {
// 乗り物は加速して...
accelerate(amount: number): void;
// 乗り物はブレーキをかけて...
brake(amount: number): void;
}
// 車クラスは Vehicle インターフェースの約束を守る
class Car implements Vehicle {
// 車の特徴(データ)
private name: string;
private color: string;
private speed: number;
// 車を作るときの初期設定
constructor(name: string, color: string) {
this.name = name;
this.color = color;
this.speed = 0;
}
// 車ができること(動作):加速する
accelerate(amount: number): void {
this.speed += amount;
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
// 車ができること(動作):ブレーキをかける
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
}
// 自転車クラスも Vehicle インターフェースの約束を守る
class Bicycle implements Vehicle {
// 自転車の特徴
private name: string;
private speed: number;
// 自転車を作る時の初期設定
constructor(name: string) {
this.name = name;
this.speed = 0;
}
// チャリも加速する
accelerate(amount: number): void {
this.speed += amount;
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
// チャリもブレーキをかけられる
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
}
この例では, Vehicle インターフェースが「乗り物」としての約束事を定義している. Car クラスと Bicycle クラスは, この約束事を守っている.
interfaceを複数定義すると以下のような実装になる.
// 基本的な乗り物の機能
interface Vehicle {
accelerate(amount: number): void;
brake(amount: number): void;
}
// 電気自動車の充電機能
interface Chargeable {
charge(amount: number): void;
}
// 自動運転機能
interface SelfDriving {
enableAutoPilot(): void;
disableAutoPilot(): void;
}
// 通常の車
class Car implements Vehicle {
// ... 前回と同じ実装 ...
}
// 電気自動車:Vehicle と Chargeable を実装
class ElectricCar implements Vehicle, Chargeable {
private name: string;
private speed: number;
private batteryLevel: number;
constructor(name: string) {
this.name = name;
this.speed = 0;
this.batteryLevel = 100;
}
accelerate(amount: number): void {
this.speed += amount;
this.batteryLevel -= amount * 0.1;
console.log(`${this.name}の速度が${this.speed}km/hになりました. バッテリー残量: ${this.batteryLevel}%`);
}
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
charge(amount: number): void {
this.batteryLevel = Math.min(100, this.batteryLevel + amount);
console.log(`${this.name}のバッテリーを充電しました. 現在の残量: ${this.batteryLevel}%`);
}
}
// 自動運転車:Vehicle と SelfDriving を実装
class SelfDrivingCar implements Vehicle, SelfDriving {
private name: string;
private speed: number;
private isAutoPilotEnabled: boolean;
constructor(name: string) {
this.name = name;
this.speed = 0;
this.isAutoPilotEnabled = false;
}
accelerate(amount: number): void {
this.speed += amount;
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
enableAutoPilot(): void {
this.isAutoPilotEnabled = true;
console.log(`${this.name}の自動運転モードを有効にしました.`);
}
disableAutoPilot(): void {
this.isAutoPilotEnabled = false;
console.log(`${this.name}の自動運転モードを無効にしました.`);
}
}
// 異なる型の乗り物を同じように扱う関数
function testDrive(vehicle: Vehicle) {
vehicle.accelerate(50);
vehicle.brake(20);
}
// 使用例
const normalCar = new Car("普通の車", "赤");
const tesla = new ElectricCar("テスラ");
const waymo = new SelfDrivingCar("Waymo");
testDrive(normalCar);
testDrive(tesla);
testDrive(waymo);
tesla.charge(50);
waymo.enableAutoPilot();
インターフェースを定義することで特筆すべき点は,ここ!
// 異なる型の乗り物を同じように扱う関数
function testDrive(vehicle: Vehicle) {
vehicle.accelerate(50);
vehicle.brake(20);
}
// 使用例
const normalCar = new Car("普通の車", "赤");
const tesla = new ElectricCar("テスラ");
const waymo = new SelfDrivingCar("Waymo");
testDrive(normalCar);
testDrive(tesla);
testDrive(waymo);
testDrive関数は,引数として Vehicle 型のオブジェクトを受け取る.Vehicle はインターフェースであり、具体的なクラスではないが,この関数は,Vehicle インターフェースを実装している任意のオブジェクトで動作する.
関数内部では,渡されたオブジェクトの accelerate メソッドと brake メソッドを呼び出している.Vehicle インターフェースはこれらのメソッドを定義しているため,この関数はエラーなく動作する.
もしインターフェースがないと...
// インターフェースなしの場合
class Car {
private name: string;
private speed: number;
constructor(name: string) {
this.name = name;
this.speed = 0;
}
accelerate(amount: number): void {
this.speed += amount;
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
}
class Bicycle {
private name: string;
private speed: number;
constructor(name: string) {
this.name = name;
this.speed = 0;
}
accelerate(amount: number): void {
this.speed += amount;
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.name}の速度が${this.speed}km/hになりました.`);
}
}
// 使用例
const car = new Car("マイカー");
const bicycle = new Bicycle("マイチャリ");
// ここに注目!!!
car.accelerate(50);
bicycle.accelerate(20);
car.brake(20);
bicycle.brake(10);
インターフェースがないと以下のこと(例えば)が適用されない.
-
型の保証
インターフェースを使用することで,CarとBicycleが確実にaccelerateとbrakeメソッドを持つことが保証される.これにより、コンパイル時にエラーを検出できる. -
柔軟性と拡張性
testDrive関数のように,Vehicleインターフェースを使用することで,異なる種類の乗り物を同じように扱うことができる.新しい乗り物(例:電動スクーター)を追加する場合も,Vehicleインターフェースを実装するだけで済む.
つまり,簡単に言うと,インターフェースを定義しないと,異なるクラスや型の間で共通の構造や振る舞いを保証する手段がないということである.
継承:特徴を受け継ぐ
継承は, 既にあるクラスの特徴を受け継いで, 新しいクラスを作る方法である. これにより, 似たようなものを効率よく作ることができる.
動物を例に考えてみよう.
// 基本の動物クラス
class Animal {
// 動物の特徴
protected name: string;
// 動物の初期設定(コンストラクタ)
constructor(name: string) {
this.name = name;
}
// 動物は鳴くという動作をする
speak(): void {
console.log(`${this.name}が鳴きました.`);
}
}
// 犬クラス(Animal クラスを継承)
class Dog extends Animal {
// 年齢という特徴を追加
private age: number;
constructor(name: string) {
super(name); // superを使うと親クラス(animalクラス)のコンストラクタを呼び出す
this.age = age; // 独自に年齢を定義することも可能
}
// 犬特有の鳴き声の定義
speak(): void {
console.log(`${this.name}がワンワン吠えました.`);
}
// 犬のボールをとってくるという動作をdogクラス独自に定義
fetch(): void {
console.log(`${this.name}(${this.age}歳)がボールを取ってきました.`);
}
}
// 猫クラス(Animal クラスを継承)
class Cat extends Animal {
constructor(name: string) {
super(name); // 親クラスのコンストラクタを呼び出す
}
// catクラスにはspeakクラスを定義しないと...
// 猫の引っ掻くという動作をcatクラス独自に定義
scratch(): void {
console.log(`${this.name}が爪とぎをしました.`);
}
}
// 動物たちを作って鳴かせる
let animal = new Animal("動物");
let dog = new Dog("ポチ", 3);
let cat = new Cat("タマ");
animal.speak(); // "動物が鳴きました." と出力
dog.speak(); // "ポチがワンワン吠えました." と出力
dog.fetch(); // "ポチ(3歳)がボールを取ってきました." と出力
cat.speak(); // "タマが鳴きました." と出力(animelクラスのspeakメソッドが呼び出される)
cat.scratch(); // "タマが爪とぎをしました." と出力
この例では, Dog クラスと Cat クラスが Animal クラスの特徴を受け継いでいる. そのため, 両方のクラスが Animal クラスの名前と speak という動作を持っている. さらに, 各クラスは自分だけの動作(fetch と scratch)も持っている.
結局,ポリモーフィズムとはなんなのか
ポリモーフィズムとは
同じインターフェースで異なる動作を実現すること.
// ポリモーフィズムを利用した関数
function animalSpeak(animal: Animal) {
animal.speak();
}
const dog = new Dog("ポチ");
const cat = new Cat("タマ");
animalSpeak(dog); // "ポチがワンワンと吠えました" と出力
animalSpeak(cat); // "タマが鳴きました" と出力
Animal,Dog,Catクラスはすべてspeakメソッドを持っている.
DogとCatはAnimalを継承し,speakメソッドをオーバーライドしている.
animalSpeak関数はAnimal型の引数を受け取るが,実際にはDogやCatのインスタンスを渡すことができる.
関数内でmakespeakメソッドを呼び出すと,渡されたオブジェクトの実際の型に応じた適切なメソッドが実行される.
結局,カプセル化とはなんなのか
カプセル化の主な目的と特徴を一旦結論づけると...
-
1. データの隠蔽
- クラス内部のデータを外部から直接アクセスできないようにする
- データへのアクセスは、クラスが提供するメソッドを通じてのみ行える
-
2. インターフェースの提供
- クラスの外部に対して、必要な操作のみを公開する
- データの整合性保護
- データの不正な変更を防ぎ、常に正しい状態を保つ
-
3. 実装の詳細を隠す
- 内部の実装を変更しても、外部のコードに影響を与えない
銀行口座を表現するクラスであるBankAccountクラスで説明する.
動物や車では説明しづらかったので銀行で行きます
// BankAccount クラス: 銀行口座を表現するクラス
class BankAccount {
// privateキーワードで残高を隠蔽
// これにより、クラス外部から直接アクセスできなくなる
private balance: number;
// コンストラクタ: 新しい口座を初期残高で作成
constructor(initialBalance: number) {
this.balance = initialBalance;
}
// 入金メソッド: 指定された金額を残高に追加
public deposit(amount: number): void {
// 入金額が正の値かチェック
if (amount > 0) {
// 残高を更新
this.balance += amount;
console.log(`${amount}円入金されました。残高:${this.balance}円`);
} else {
// 不正な入金額の場合はエラーメッセージを表示
console.log("入金額は0円より大きい必要があります。");
}
}
// 引き出しメソッド: 指定された金額を残高から引く
public withdraw(amount: number): void {
// 引き出し額が正の値で、かつ残高以下であることをチェック
if (amount > 0 && amount <= this.balance) {
// 残高を更新
this.balance -= amount;
console.log(`${amount}円引き出されました。残高:${this.balance}円`);
} else {
// 不正な引き出し、または残高不足の場合はエラーメッセージを表示
console.log("引き出しに失敗しました。残高が不足しているか、引き出し額が不正です。");
}
}
// 残高取得メソッド: 現在の残高を返す
public getBalance(): number {
return this.balance;
}
}
// BankAccountクラスの使用例
const account = new BankAccount(1000); // 初期残高1000円で口座を作成
account.deposit(500); // 500円入金
account.withdraw(200); // 200円引き出し
console.log(account.getBalance()); // 現在の残高を表示
// 以下の行はエラーになる(privateプロパティに直接アクセスできない)
// console.log(account.balance);
このコードでカプセル化が実現している点
- balanceはprivateで宣言されており,クラスの外部から直接アクセスできない
- depositとwithdrawメソッドを通じてのみ,残高を変更できる
- これらのメソッドには,入金額や引き出し額のチェックロジックが含まれており,不正な操作を防いでいる
- getBalanceメソッドを通じてのみ,現在の残高を取得できる
まとめ : 手続型からオブジェクト指向へ
-
1. コードを再利用しやすい
継承を使うことで, 既にあるコードを効率よく再利用できる. -
2. わかりやすい構造
関連するデータと動作をクラスにまとめることで, コードの構造がはっきりする. -
3. 新しいことを追加しやすい
新しい機能や要素を追加する際, 既存のコードを変えずに新しいクラスを作れる. -
4. 管理しやすい
クラスとインターフェースを使うことで, 一か所の変更が他の場所に予期せぬ影響を与えるリスクが減る. -
5. 現実世界を表現しやすい
現実の物事をプログラムの中で直接表現できるため, 問題を理解しやすくなる. -
6. 型の安全性
TypeScript の型システムにより, 多くのエラーを事前に防ぐことができる.
手続型プログラミングは簡単な問題には適しているが, プログラムが大きくなるにつれて管理が難しくなる. 一方, オブジェクト指向プログラミングは, 現実世界の物事をプログラムの中で表現し, コードをわかりやすく構造化する. クラス, オブジェクト, インターフェース, 継承などの考え方を理解し使うことで, 管理しやすく, 拡張性の高いプログラムを作ることができる.
JavaScriptの違いとして...
TypeScript を使うことで, JavaScript の柔軟性と, 静的型付けによる安全性の両方を得ることができる. これにより, より堅牢で保守しやすいコードを書くことができる.
初めてオブジェクト指向プログラミングを学ぶ人にとっては, これらの概念は最初は難しく感じるかもしれない. しかし, 日常生活の例を使って考えることで, これらの概念をより直感的に理解できる. プログラミングを学んでいく中で, これらの考え方を実際に使ってみることで, より深く理解し, 効果的にコードを書けるようになっていくと思うので様々な例でオブジェクト指向の美しさを感じてほしいと思う.