0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

天地創造で学ぶTypeScriptクラス入門

Posted at

長い前置き

AIがコードを生成するようになってきた昨今, 最終的な品質保証ができるかどうかの重要性は増すばかりです. 2000年代から続いたアジャイルの潮流が今まさに終わりを告げ, 1〜3人の少人数チームでの開発の方が都合が良くなってきたと感じています. つまり, そこで必要なメンバーはミドル以上であり, 「ジュニアの育成が最重要」と声を大にして述べる人は数年以内には主流ではなくなるでしょう. もちろんジュニアを育ててあげたいんだけど.
かくいう私もここ2年ほどでデベロッパーからプロダクトオーナーとして開発に携わってきました. そして職位が上がり, 実開発から少しずつ遠ざかっています. そんな中で, 「コードを書く人」と「書かない人」を分ける垂直型組織構成に疑問を感じるようになりました.
極論すれば, 人間による不安定・不確定な組織よりも, 人間がPOかつ品質保証, AIがデベロッパーという体制がたくさんあるフラットな組織の方が理想的ではないかとすら思っています.
アジャイルの最大の問題は人間基準でメソッドが形成されていることです. チームメンバーは休むし, 辞めるし, 認識がずれる. AIは24時間稼働し, 指示通りに動き, 型が合わなければコンパイラが教えてくれる. これは私の中での一抹の人間性の欠如ではなく, ゲームチェンジを冷静に受け止めていると自負していますが, あまり自信はありません.
AIがコードを書く時代, 上流にいる人間に求められるのは「コードを書く能力」ではなく「コードの品質を担保する能力」, 「そして設計能力」です. そしてその担保を可能にするものの一つが, 型システムだと思っています. (もちろんそれだけじゃないよ)
型があれば, 実装の詳細を追わなくても, コードの意図と構造を理解できます. たとえばAIが生成した関数のシグネチャが (user: User) => Promise だったとする. 実装を読まなくても「ユーザーを受け取って, その注文一覧を非同期で返すんだな」とわかる. 中身が100行あろうが, この1行が契約書です. AIが何を書こうが, 契約違反はコンパイラが弾く. 人間がレビューするより早く, 確実で, 24時間文句を言わない.
では「品質を担保する能力」とは何か. それは結局, コードを読める力だと思っています. 書く力ではなく, 読む力.
AIが書いたコードを「なんとなく動いてるからOK」で通すのではなく, 構造を理解した上で「これで正しい」と言える力. それが品質保証です. そしてTypeScriptの型システムは, その精読を助けてくれる文法書のようなものです.
そんなことを考え, 改めてオブジェクト指向と型について整理してみました. 個人的に「箱がどうしたみかんだりんごだ果物だ」みたいなオブジェクト指向の話がさっぱり理解できないので, まずは数学ちっくな定義だけで読み物を書いていたんですが, 少し見直して「箱がどうしたみかんだりんごだ果物だ」に寄せたら読みやすくなったきがします.
神様の天地創造になぞらえて. そして願わくばジュニアを早急にミドル以上にするべく….
(長い前置きはここまで. 本編をどうぞ)

神は1日目に型を作りし 型の創造

type x = number;
type y = number;

2日目に演算を作りし 演算の創造

type x = number;
type y = number;

// 演算 +: ℝ² × ℝ² → ℝ²
function add(v1: [x, y], v2: [x, y]): [x, y]{
	return [v1[0] + v2[0], v1[1] + v2[1]];
}

// 演算 ·: ℝ × ℝ² → ℝ²
function multiply(v: [x, y], scalar: number): [x, y] {
	return [v[0] * scalar, v[1] * scalar];
}

3日目に集合と演算を結びつけし(構造を作りし)

type x = number;
type y = number;
type Vector = [x, y]; // 神様がお作りになったもの

function add(v1: Vector, v2: Vector): Vector {
	return [v1[0] + v2[0], v1[1] + v2[1]];
}
function multiply(v: Vector, scalar: number): Vector {
	return [v[0] * scalar, v[1] * scalar];
}
// (Vector, add, multiply) という代数構造

3日目に神は type Vector = [x, y] という型エイリアス(名前付け) によって, 「この集合とこれらの演算がセットである」という関係性を明示されました.

4日目にクラスといふ器を作りし(3日目までのものを統合せし)

class Vector {
  x: number;
  y: number;
  
  constructor(x: number, y: number) {
    this.x = x; 
    this.y = y;
  }
  
  // 2日目の演算 add をメソッドとして内包
  add(other: Vector): Vector {
    return new Vector(this.x + other.x, this.y + other.y);
  }
  
  // 2日目の演算 multiply をメソッドとして内包
  multiply(scalar: number): Vector {
    return new Vector(this.x * scalar, this.y * scalar);
  }
}

// (Vector, add, multiply) という代数構造が一つのクラスに収まった

// 集合(データ)と演算(メソッド)が一つのクラスに内包された
// これまで分離していた要素が統合された

4日目に神は class というカプセル化の器によって, 「データ(集合の要素)と演算(関数)が一体である」というオブジェクト指向の構造を実現されました.
しかし神が創造されたものは, それだけではありませんでした. class Vector と宣言した瞬間, 神は二つのものを同時に創造されていたのです:

  1. 生成の力new Vector(1, 2) で実体を生み出す能力
  2. 型という名Vector という名前そのものが, 型として機能する力

これにより, 分散していた要素が単一の名前空間に統合され, new Vector(1, 2) という形で要素の生成と演算が密接に結びつきました.

なぜ神様はConstructorをお作りになられたのか

constructor(x: number, y: number) {
  this.x = x;
  this.y = y;
}

神は, ベクトルを作るたびに [3, 4] という配列を書くのは面倒だと感じておられました. そして4日目に次のように思われました.

  • [0][1] という添字でアクセスするのは分かりにくい」
  • 「x成分なのか y成分なのか, 名前で呼びたい」
  • 「ベクトルを作る統一された方法が欲しい」
  • 「そして, 生成されるベクトル一つ一つが, 自分自身のデータを持つべきだ」

そこで神はConstructorという正式な生成儀式を定められました. this とは「今まさに生成されようとしているこのベクトル自身」を指します.

// 3日目まで:配列で表現
const v = [3, 4];  // これが x なのか y なのか分かりにくい

// 4日目から:コンストラクタで生成

const v1 = new Vector(3, 4);
// この瞬間, this は v1 そのもの
// this.x = 3 は「v1 の x に 3 を入れよ」
// this.y = 4 は「v1 の y に 4 を入れよ」

const v2 = new Vector(1, 2);
// この瞬間, this は v2 そのもの
// this.x = 1 は「v2 の x に 1 を入れよ」
// this.y = 2 は「v2 の y に 2 を入れよ」

const v3 = new Vector(-1, 1);  // 明示的に「Vectorを作っている」
//         ^^^^^^^^^^
//            ↑これは今まで神が作られた[3,4]というベクトルと同じ実体である    

神は宣われました:
「各ベクトルは自分自身の identity(同一性)を持ち, 自分のデータを自分で管理せよ」

これが, 神が示されたオブジェクトの本質です. 我々人間も一人ひとりが人間ですが, 異なる値, 罪を背負っているのです.

なぜ神様はaddをMethodとされたのか

add(other: Vector): Vector {
  return new Vector(this.x + other.x, this.y + other.y);
}

3日目までは, 神は関数を別に用意されていました:

add(v1, v2) // v1 と v2 を外から渡す

しかし神は思われました:

  • 「演算は, その対象(ベクトル)に属するべきだ」
  • 「v1 に『自分と v2 を足せ』と命令する方が自然だ」
  • 「データと演算は一体であるべきだ」

そこで神は演算を内包させました.


const v1 = new Vector(3, 4);
const v2 = new Veector(1, 2);

// 3日目まで:関数を外に置く
add(v1, v2) // どちらが主体か不明確
// 4日目から:オブジェクトに演算能力を持たせる
v1.add(v2) // v1 が主体. v1 自身が演算できる

これがカプセル化です. 神は「データと, そのデータに対する演算は, 一つの器に収めるべし」と定められました.

なぜクラスは型でもあるのか

4日目の終わりに, 神はふと立ち止まられました. 3日目まで, 型と実体は別々に存在していました:

// 3日目:型は型, 関数は関数, データはデータ
type Vector = [x, y];           // 型の定義
const v: Vector = [3, 4];       // 型を使ってデータを作る
function add(v1: Vector, ...    // 型を使って関数を定義する

しかし4日目に class を創造したとき, 神は気づかれました:
「クラスとは, 設計図であると同時に, その設計図から生まれるものの名前でもある」

class Vector {
  constructor(public x: number, public y: number) {}
  add(other: Vector): Vector { ... }
//          ^^^^^^   ^^^^^^
//          ここで Vector を「型」として使っている
}

神は宣われました:
class Vector と書いた瞬間, 汝は二つのものを手にしている」

// 一つ目:実体を生み出す力
const v1 = new Vector(3, 4);
const v2 = new Vector(1, 2);

// 二つ目:型として語る力
function transform(v: Vector): Vector {
//                    ^^^^^^   ^^^^^^
//                    「Vectorを受け取り, Vectorを返す」と宣言できる
  return new Vector(v.x * 2, v.y * 2);
}

const result: Vector = transform(v1);
//    ^^^^^^
//    「resultはVectorである」と宣言できる

3日目までは type Vector = [x, y] と別途宣言が必要でした. しかし4日目からは, class Vector と書くだけで, 生成の力と型の名が同時に手に入るのです.

神は宣われました:
「これは偶然ではない. 何かを作れるということは, それが何であるかを知っているということだ. クラスは『何を作るか』と『それは何か』を一つに統合したのだ」

こうして, 3日目まで分離していた「型の定義」と「実体の生成」と「演算の定義」が, 4日目にすべて class という一つの器に収められたのです.

5日目に神は簡潔さの恵みを与えし

class Vector {
  constructor(public x: number, public y: number) {}
  
  add(other: Vector): Vector {
    return new Vector(this.x + other.x, this.y + other.y);
  }
  
  multiply(scalar: number): Vector {
    return new Vector(this.x * scalar, this.y * scalar);
  }
}

4日目の記述を見て, 神は思われました:

  • x: number を2回, y: number を2回書くのは冗長だ」
  • this.x = x という代入も機械的で本質ではない」
  • 「本質(データの型と初期化)だけを書けるようにしよう」

そこで神はパラメータプロパティという恵みを与えられました. public と書くだけで,

  • 宣言
  • 代入
  • 公開
    の3つが同時に行われるようになりました.

public の三位一体

constructor(public x: number, public y: number) {}

1. 宣言(Declaration)

class Vector {
  x: number;  // ← これが自動的に行われる
  y: number;  // ← これが自動的に行われる
}

4日目の終わりに, 神は全てのクラスを見渡されました. そして思われました:
「各ベクトルが持つべきプロパティを, わざわざクラスの先頭で列挙するのは冗長ではないか. コンストラクタを見れば何を持つかは明らかなのに」

そこで神は定められました:
「コンストラクタの引数に型を書けば, それはプロパティの宣言でもある」

2. 代入(Assignment)

constructor(x: number, y: number) {
  this.x = x;  // ← これが自動的に行われる
  this.y = y;  // ← これが自動的に行われる
}

神は嘆かれました:
this.x = x という儀式は, あまりにも機械的で本質的ではない. 引数を受け取ったら, それを自分のプロパティに入れるのは当然のことではないか」

そこで神は恵みを与えられました:
「引数として受け取ったものは, 自動的に this に格納される」

3. 公開(Public Access)

const v = new Vector(3, 4);
console.log(v.x);  // 3 ← 外部からアクセス可能
console.log(v.y);  // 4 ← 外部からアクセス可能

神は問われました:
「データを隠すべきか, 開くべきか. デフォルトでどうあるべきか」
そして神は決断されました:
public と明示しせれば世界は開かれ, 誰もがアクセスできる. 闇の帷の如き隠したき者は private と明示せよ」
神は宣われました:
public という一語で, 『存在を宣言し, 値を保持し, 世界に公開する』という三つの行為を同時に行わせよう. これが簡潔さである」

publicprivate
constructor(private _x: number, public y: number) {}

const v = new Vector(3, 4);
// 闇の法
console.log(v._x); // エラー!private なので外部からアクセス不可
// 太陽の法
console.log(v.y); // 4 ← OK

6日目に神は二重性を与えし(継承と多態性)

// 神は基底なるものを定めし
abstract class Vector {

  constructor(public x: number, public y: number) {}
  
  abstract add(other: Vector): Vector;
//^^^^^^^^
  abstract multiply(scalar: number): Vector;  
}

// 2次元の民
class Vector2D extends Vector {
//             ^^^^^^^^^^^^^^
  add(other: Vector2D): Vector2D {
    return new Vector2D(this.x + other.x, this.y + other.y);
  }
  
  multiply(scalar: number): Vector2D {
    return new Vector2D(this.x * scalar, this.y * scalar);
  }
}

// 3次元の民
class Vector3D extends Vector {
  constructor(x: number, y: number, public z: number) {
    super(x, y);
//  ^^^^^    
//   ↑父なる Vector を呼び出す
  }
  
  add(other: Vector3D): Vector3D {
    return new Vector3D(this.x + other.x, this.y + other.y, this.z + other.z);
  }
  
  multiply(scalar: number): Vector3D {
    return new Vector3D(this.x * scalar, this.y * scalar, this.z * scalar);
  }
}

5日目の終わりに, 神は被造物を見渡されました. そして思われました:

  • 「2次元のベクトルと3次元のベクトルは, 異なる存在でありながら, 同じ本質を持つ」
  • 「両者は『ベクトルである』という共通の性質を持ちながら, それぞれ固有の振る舞いをする」
  • 「この『同じでありながら異なる』という二重性を表現できないか」

なぜ神様は abstract をお作りになられたのか

abstract class Vector {
  abstract add(other: Vector): Vector;
}

神は宣われました:
「『ベクトルとは何か』という本質は定めよう. しかしその具体的な振る舞いは, 子らに委ねよう」

abstract とは契約です. 「この能力を持つべし」という約束であり, 具体的な実装は持ちません.

// これはできない:
const v = new Vector(1, 2);  // エラー!abstract は直接生成できない

// これはできる:
const v2d = new Vector2D(1, 2);  // 具体的な子は生成できる
const v3d = new Vector3D(1, 2, 3);

神は宣われました:
理念としてのベクトルは実体を持たない. しかし2次元のベクトル, 3次元のベクトルは実体を持つ. 理念は子らを通じてのみ, この世に現れる」

なぜ神様は extends をお作りになられたのか

class Vector2D extends Vector { ... }
class Vector3D extends Vector { ... }

神は定められました:
「子は親の性質を継承する. 親が持つものは, 子も持つ」

// Vector3D は Vector の x, y を継承している
const v = new Vector3D(1, 2, 3);
console.log(v.x);  // 1 ← 親から継承
console.log(v.y);  // 2 ← 親から継承
console.log(v.z);  // 3 ← 自分固有

これが継承です. 神は「車輪を再発明するな. すでに定めた本質は引き継ぎ, 新たに必要なものだけを加えよ」と定められました.

なぜ神様は super をお作りになられたのか

constructor(x: number, y: number, public z: number) {
  super(x, y);  // 父を呼び出す
}

子が生まれるとき, まず親の生成儀式を行わねばなりません. super(x, y) とは **「父なる Vector よ, まず汝の初期化を行いたまえ」という呼びかけです. **

神は宣われました:
「子は親を敬うべし. 子が存在する前に, まず親としての部分が存在せねばならない」

多態性(ポリモーフィズム)という奇跡

function printLength(v: Vector): void {
  console.log(Math.sqrt(v.x * v.x + v.y * v.y));
}

const v2d = new Vector2D(3, 4);
const v3d = new Vector3D(3, 4, 0);

printLength(v2d);  // 5 ← Vector2D を Vector として扱える
printLength(v3d);  // 5 ← Vector3D を Vector として扱える

神は最後に奇跡を起こされました:
異なる存在を, 同じ名で呼ぶことができる

これが**多態性(ポリモーフィズム)**です. Vector2D も Vector3D も, 「Vector である」という点では同じです. しかし add を呼び出せば, それぞれが自分の流儀で振る舞います.

const vectors: Vector[] = [new Vector2D(1, 2), new Vector3D(1, 2, 3)];

for (const v of vectors) {
  // どちらも Vector として扱えるが, 
  // 実際の振る舞いはそれぞれ異なる
}

神は宣われました:
一つの名のもとに, 多くの姿を持つことを許そう. これが被造物に与える最後の恵みである. 同じメッセージを送っても, 受け手によって応答は異なる. これこそがオブジェクト指向の真髄である」

7日目に休まれた

const origin = new Vector(0, 0);

神はこれらすべてを見渡され, こう宣われました:

良し

そして7日目に神は休まれました.


安息日の教え

神は7日目に休まれましたが, それは疲れたからではありません.

神は我々に示されたのです:

創造には終わりがある. そして終わりの後には, 振り返りの時が必要だ」

プログラマよ, 汝もまた7日目を持つべし.

コードを書き続けるだけでなく, 立ち止まり, 見渡し, 問うのだ:

  • なぜこの型なのか?
  • なぜこの構造なのか?
  • なぜこのクラスなのか?

理解なき創造は, 砂上の楼閣である.

こうして天地創造は完了した
型から始まり, 多態性に至る
オブジェクト指向のすべてが, 6日間で創造された

そして7日目, 神は休まれた
我々に学ぶ時間を与えるために

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?