TypeScriptでクラス思考と関数思考のギャップを少し埋める
クラスのコンストラクタと関数
class Person {
private name: string;
private age: number;
constructor(name: sring, age: number){
this.name = name;
this.age = age;
}
selfIntroduce(): string {
return `Hi! I'm ${this.name}!`;
}
isAdult(): boolean {
return this.age >= 18;
}
}
const person = new Person('takeshi', 20);
const introduce = person.selfIntroduce();
const isAdult = person.isAdult();
selfIntroduceはageつかってないし、isAdultはnameつかってない。
コンストラクタを依存とするなら、それぞれのメソッドに使ってない依存があることになる。
そこで、これを1つのメソッドにつき1つのクラスにする。
class selfIntroduce {
private name: string;
constructor(name: sring){
this.name = name;
}
do(): string {
return `Hi! I'm ${this.name}!`;
}
}
const introduce = new selfIntroduce('takeshi').do();
class isAdult {
private age: number;
constructor(age: number){
this.age = age;
}
do(): boolean {
return this.age >= 18;
}
}
const isAdult = new isAdult(20).do();
毎回コンストラクタを挟むのも、ファイルを別にする(1クラス1ファイルというプラクティスがある)のもめんどいので関数にする。
export const selfIntroduce = (name: string) => {
return `Hi! I'm ${this.name}!`;
}
export const isAdult = (age: number) => {
return this.age >= 18;
}
JSにおけるビルダーと関数のカリー化
JSは関数を返り値として持てる(関数が第一級オブジェクトである)ので、次のようなビルダーを作成できる。
/**
* hogeとfugaをいれて、最後に連結させて返す。
*/
const hogeFugaBuilder = (hoge: string) => (fuga: string) => {
return `${hoge}${fuga}`;
}
// これでも同じ意味。関数を返す関数。
const hogeFugaBuilder = (hoge: string) => {
return (fuga: string) => {
return `${hoge}${fuga}`;
}
}
// こう使う
hogeFugaBuilder('hoge!')('fuga!') // hoge!fuga!;
べつにこのようなことをしなくても、一気に初期化すればいいと思うかもしれない。
const hogeFuga = (hoge: string, fuga: string) => {
return `${hoge}${fuga}`;
}
// こう使う
hogeFuga('hoge!', 'fuga!') // hoge!fuga!
hogeFugaBuilder
関数とhogeFuga
関数の違いはなんだろうか?それはビルダーパターンのほうは、依存に時間差をつけることができることだ。すぐにhogeは決まるけど、fugaはフローの後の方で入れたいとかに使える。またさらによいことに1つ目の依存を固定した新しい関数を作りやすい。この引数の1つ目の依存さえ決めればとりあえず成り立つということが関数の再利用性と取り回しやすさを向上させている。
const mochiFuga = hogeFugaBuilder('mochi');
const tamaFuga = hogeFugaBuilder('tama');
mochiFuga('fuga!!!') // mochifuga!!!
tamaFuga('fuga!!!') // tamafuga!!!
関数型言語だとこれらはカリー化とよばれ、すべての関数がデフォルトでビルダーパターンになっている。
クラスのデコレータと高階関数
クラスにデコレータがある言語もある。TypeScriptもデコレータが使える。デコレーターをつけたクラスやメソッドの情報を受け取って加工して返したり、メタデータに一時的な変数を記録しておいたりする。
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
例は上記のリンクからとってきた。上記のデコレータは、@format
でプロパティの値をメタデータとして保持し、getFormat
でメタデータから値を取得している。取得した値を用いて書き換えている。
const greeter = new Greeter('タマですにゃ');
console.log(greeter.greet()); // Hello, タマですにゃ
話がそれるが、これせっかくデコレータ使ってるのに、getFormat
をgreet
の中に入れてしまってるのはもったいない。getFormat
をgreetの引数にしたほうが、よりgreet
メソッドがピュアになると思う。
これを関数で再現してみる。
目標
// 純粋
const resGreet = greet('タマですにゃ');
console.log(resGreet); // タマですにゃ
// フォーマット
const formatGreet = format("Hello, %s", greet);
const resFormatGreet = formatGreet('タマですにゃ');
console.log(resFormatGreet); // Hi! タマですにゃ
また話がそれるが、関数として再現してみたらわかるが、format("Hello, %s", greet)
の部分、どのようにフォーマットするのかのロジックがgreet
に入り込んでしまっている。format("Hello, %s", howFormatFunction, greet)
のようにフォーマットの仕方を別に分離して実装できたほうがより扱いやすいコードになるはず。
話を戻して、
クラスを最初の例のようにクラスを関数に変換する。まずはデコレータを無視してつくってみる。
const greet = (message: string) => {
return message;
}
しかしよくみると、クラスの例ではgreet
メソッドでフォーマット用の文字列を受け取っている。これを受け取るものが必要なのでもう少し書き換える。
const greet = (message: string, formatString?: string) => {
// formatStringが存在すればフォーマット、しなければmessageをそのまま返す。
return formatString
? formatString.replace("%s", message)
: message;
}
format
関数を考えてみると。
type GreetFunction = (message: string, formatString?: string) => string;
const format = (formatString: string, greet: GreetFunction) => {
return (message: string) => greetFunction(message, formatString); // greetFunction(formatString)は関数を返す。
}
greet
関数にformatString
を入れた新しい関数を返していることに注意。
これで目標が達成できた。ここまでのコード。
Observerパターンとデータとパイプ
デザインパターンの一つにオブサーバーパターンというものがある。
これは、クラスA -> クラスB と クラスA -> クラスC という処理の流れがあったときに、クラスAがクラスBとクラスCのメソッドをコールするのではなく、クラスAが処理が終わったイベントを発火して、クラスBとクラスCがそれを検知して処理を行うもの。
メリットとしては、クラスAがクラスBとクラスCを知らなくてもよく(逆にクラスBとクラスCは検知する仕組みが必要)、クラスAが責任をもちすぎて神クラスになることを防ぐ。
ただObserverパターンのデメリットとしては、クラスA -> クラスB -> クラスC のように複数のクラスの流れがある処理だと、全体のの流れを把握しにくいというものがある。なぜならクラスBはクラスAのイベントを検知するようにして、クラスCはクラスBのイベントを検知するようにするコードがバラバラにかかれてあるからだ。
このような場合、データとパイプという考え方が役に立つ。
(データ)
|> 処理1
|> 処理2
|> 処理3
クラスはデータとふるまいが紐付いているが、関数はデータと振る舞いが紐付いていないため、データとパイプという考え方を発展させてきた。上記はF#のコードで、データ元に対して、処理1~ 処理3が順に適応される。
TypeScriptでは残念ながらこんなにきれいにはかけず関数を順番に呼び出すことになる。
処理3(処理2(処理1(データ)))
Haskellだとドット演算子で合成できたりする。
処理3 . 処理2 . 処理1
Promiseがもつコンテキスト
JSにPromiseが普及してきたおかげでコードが書きやすくなった。単純にコールバック地獄を解決してくれただけでなく、もしその関数がPromiseを返すのであれば以下の2つのことを伝えてくれる
- このメソッドは非同期であること
- このメソッドは例外が発生する可能性があること
Promiseがなければこの関数は例外を投げるのか、同期的なのか非同期的なのか知るには、例えば関数名を工夫したり中を覗いたりとコードの仕組み外でやるしかなかった。
const res = someFunction();
このPromiseがもつ2つのコンテキスト(文脈)が僕らを楽にさせてくれる。
コンテキストは変数をもつことができる。TypeScriptではジェネリクスと呼ばれている。
type stringPromise = Promise<string>;
type numberPromise = Promise<number>;
type booleanPromise = Promise<boolean>;
つまりコンテキストをプログラミングできるようになっている。
TypeScriptには一部存在しないが例えば他にも便利なコンテキストが考えられる
- 失敗する可能性
- 値がない可能性
- 非同期で実行される
- 副作用が生じる
そういえば、関数型言語ででてくるモナドという言葉がでてくるが、これはこの同じコンテキスト内でのプログラミングを楽にするためのツール。
Author And Source
この問題について(TypeScriptでクラス思考と関数思考のギャップを少し埋める), 我々は、より多くの情報をここで見つけました https://zenn.dev/dove/articles/8faa222faa57f1著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol