タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

不要なクラス宣言、やめちゃおっか?

Sponsored link

今回のエントリは特定の言語に向けて書いているわけではありませんが、関数をサポートしていない言語では必然的にクラスをベースに実装していくことになるのである程度は対象となる言語は絞られます*1
また特に説明がなければサンプルはTypeScriptで書きます。

さて、あくまで傾向としてではありますが関数を作れる言語の経験が短い人(例えばJavaやRubyですと、関数ではなくクラスに対するメソッドという形で実装することになります)は、単機能の振る舞いを実装するためだけであっても以下のようなコードを実装しがちです

class DoSomethinger {
  constructor(private something: Domething) {}

  public doSomething() {
    return dooo(something);  // do something
  }
}

これは

const doSomething = (something: Domething) => {
  return dooo(something);  // do something
};

と書きましょう。

今回は以上です。 :+1:

で終わらせるのはあんまりなので理由について述べます。

理由

これにはいくつかの理由があります。代表的なものを以下で説明します。

0. 簡潔に書く

単純に、関数で済むものは関数で書いた方があらゆる意味で簡潔です。

1. 不要な状態を持ち込まない

例に挙げた DoSomethinger のようなクラスは、すぐに setAnything のようなメソッドを生やしはじめて管理不能に陥ります。
引数と結果のみが存在する関数と比べて、クラスのコンストラクターとメソッドという中間状態にmutabilityが残る手法は不確定要素が増えてしまい、その分保守性や利便性が下がります。

またこのようなクラスは往々にして責務が曖昧なため、1ヶ月後の自分や他のメンバーが無計画に新しいメソッドを増やしがちです。そしてそれに伴って新たな状態setterが追加されてしまい、初期化や特定のメソッド呼び出しに暗黙の順序が増えていってしまいます。
それを避けるための方法としてはBuilderPatternなどが存在しますが、全てのオブジェクトをそのパターンにのっとって実装してしまうとプロジェクトはenterprise fizzbuzz化してしまいます。

まとめ

色々と書こうとしたのですが、本稿に関してはとにかく「意味もなくクラスを使うのをやめろ」という以外にいうことがないことに気がつきました。
現代のプログラミング、特にGUIの中でも宣言的UIを採用しているもので最も重要な要素はimmutabilityです。クラス(そしてデータに振る舞いを持たせること)はあらゆる意味でそこと相性が悪いです。

上の理由では頑張ってクラスを「使わない理由」という形で文章をひり出しましたが、本来は逆で「クラスを使う理由」があるときに限ってクラスを使うべきです。
しかしほとんどのケースは関数の機能(クロージャーとか合成)でまかなえます。せいぜいメソッドチェーンでいい感じに書くインターフェース作りてぇなぁとかその程度のユースケースじゃないでしょうか。
特にReact Hooksがリリースされた今となっては、フロントエンドの実装でクラスが必要になることはほぼ皆無といっていいでしょう。悔い改めよ。

以前に書いた記事: https://hachibeechan.hateblo.jp/entry/pipeline-operator-will-be-good-friend-of-typescript ではまさにメソッドチェーンしたいという気持ちからクラスを使っています。

補足: Dart

Dartのように、関数をサポートしていてもクラスとメソッドで実装したほうがベターということになりがちな言語も存在しています。その辺は大人の判断力でどうかひとつ、よろしくお願いします。

補足: TypeScript

クラス使わないならどう書くんや的な疑問を持つ方への補足です。

こう書きます。

type User = Readonly<{
  name: string;
  email: string;
}>;

const setName = (u: User, name: string) => ({...u, name});
const setEmail = (u: User, email: string) => ({...u, email});

>>> const userA = {name: 'user a', email: 'user-a@hoge.com'};
>>> userA2 = setEmail(setName(userA, 'user a2'), 'user-a2@hoge.com');

無限に括弧がネストされて辛い、メソッドチェインみたいに括弧よく書きたいという場合は先に紹介した記事を参考にすると上のコードをリファクタリングできます。

え、でもその書き方古臭くないですか?

察しの良いかたはお気づきかもしれませんが、上の書き方は古のコーディングスタイルを踏襲しています。
そう、C言語です。

C言語ではstructで宣言したデータに対して、それをレシーバにとる関数群を用意して振る舞いを表現していました。
本稿で僕が推奨してる書き方はほぼそのまんま、その書き方を踏襲する形をとっています。古き良きじゃん〜。

このスタイルはStoreをPOJOに保つことを強く推奨しているReduxと特に相性が良いです。
また、やってみるとわかりますが構造的部分型による型検査を行い、クラス宣言されたものをのぞいてはランタイムに型情報が消滅してしまうTypeScriptと非常に親和性が高いです。あと継承とかいう主に自分の両足を縛り付けてうまく歩けないようにするための機能が付属してきません。

ちなみにPythonもメソッドの第一引数は必ずthis(self)を宣言する必要があるということになっていますが、これも実質的に僕が推奨しているようなスタイルと同じことをやっているわけですね(強引な結論)。
実際、Pythonでメタプログラミングをしていると関数を動的にクラスに紐づけていくようなことをしょっちゅうするわけですが、そういうことしているとメソッド呼び出しはselfを第一引数にする関数呼び出しの糖衣構文なんだ*2なぁというのを強く感じたりします。

ちなみにモダンな言語でもこのようなスタイルを取り入れている言語がありますね、みんな大好きGo言語とRustとか。それぞれ実際の振る舞いや指向は微妙に違っている気がしますが?

そういえばDiscordがGo言語で書いてたマイクロサービスの一つをRustで書き換えたら最高にハッピーだった、みたいな記事でまた界隈がザワついていましたが、ざっくりとだけ記事を読んだ自分でもその焦点がGCであって、つまりGCがボトルネックになっちゃったからGCがない言語(C++とか)で書き直したらすごく速くなったよ、また光もらっちゃったね。
そういう今までよくあるストーリーだったんだけどその乗り換えた先がC++じゃなくてRustだった、つまりあの記事でなんらかのX vs Y要素を見出すならC++ vs Rustなんだよね。

そういえばPOJOってなんかジョジョっぽいですよね。
おしまい。

*1:もちろん、関数型言語は用語の指す意味が違ったりするし内容も言わずもがなといったかんじなので対象外です

*2:筆者のおぼろげな2系の知識によればPython内でラムダと関数とクロージャーとメソッドは振る舞いの互換性はありつつそれぞれ別の型として定義されてはいる