classを使わないカプセル化
オブジェクト指向プログラミングの優れた利点は、データとそれに作用する関数を一つのオブジェクトにまとめられることではない。優れた利点、それは、(実装からインターフェイスを切り離せる)データのカプセル化と、(型の一群の振る舞いを同じようにする)多相性だ。しかしながら、データのカプセル化と多相性は、OOP の専売特許ではない!
Haskell でのデータのカプセル化は、それぞれのデータ型をそれぞれのモジュールで宣言し、そのモジュールからインターフェイスだけを公開することで実現できる。モジュール内部には、内部データに触れる関数群があるが、モジュールの外から見えるのはインターフェイスだけだ。
TypeScriptでそれっぽくやるなら
下記でいうとnewUserだけを公開して、ドメインルール守るようにしてgetFullName, cloneなどの関数は分離する
code:TypeScript
import { z } from "zod";
// 制約
const schema = z.object({
id: z.string().uuid(),
firstName: z.string().nonempty().max(10)
lastName: z.string().nonempty().max(10)
})
type User = z.infer<typeof schema>;
// これだけexportする
// Smart Constructors的な
export const newUser = (data: User) => {
return User(schema.parse(data))
}
// ドメインオブジェクト、適宜導出プロパティつくったり
const User = (data: User) => {
const user = {
...data,
fullName: getFullName(data.firstName, data.lastName)
}
return user
}
const getFullName = (firstName:string, lastName:string) => {
return ${firstName} ${lastName}
}
利用例
code:TypeScript
try {
const user = newUser({
id: 1,
firstName: "foo",
lastName: "bar"
})
// userのデータ構造上の知識がカプセル化されてる
user.fullName // "foo bar"
const user2 = user.clone()
// バリデーションエラーになる
newUser({
id: 2,
firstName: "verylongfirstname",
lastName: "bar",
})
} catch(err) {
console.log(err)
}
これによって
プロダクトの開発初期の頃はモジュール分割が方針が見えづらいのである程度大きく作りたいときがある
オブジェクトのライフサイクルにまつわる知識を外部から隠蔽できる
若干コード量が増えるのがネックかkoushisa.icon