クラスを使わないアプリを実際に作ってみた気付き

ども、北米でお勉強してる Nash です。

この記事は「クラスを使わないでアプリケーションを作ってみたことによる気付き」をまとめた記事です。

クラスを使わないアプリを作った

先に結論だけども、「クラスなしでモデルを表現する」はあまりおすすめできないので素直にクラスを使ってください。個人的には immer と併用するのが良いかと思う。

ということで、1つずつ見ていく。

何を作ったの

『Plangoab』という海外留学・移住のスケジュールプランニングを行うサービスを作った。想定する利用者として、海外へ渡航して現地就職を目指す人、またはそれを支援する留学エージェントになる。

Plangoab | Plan to go abroad

技術スタックは TypeScript / React / Reduc あたりがメインになる。

なんでクラスなしで作ったの

根本的なところは「クラスなしでアプリを作ったらどうなるんだろう?」を試してみたかったからという好奇心。「きっとこうだろう」という憶測じゃなくて実体験から得られるナレッジもほしかったから。

2021 年時点の OOP への評価

最近のプログラミングパラダイムを見ていても「OOP って本当に必要?」なところに言及してるのをチラホラ見る。

どちらかというと「継承って必要ないよね」の文脈のほうが正確だとは思うし、実際 Rust・Go みたいな次世代のプログラミング言語ではダックタイピングプログラミングを採用してる。継承はツラミがありすぎるよね、ってのが Rust のチュートリアルドキュメントにも書かれてるくらいだし。


というわけで、「クラスなし」でアプリを作ってみたくなった。

どんな感じでクラスなしのアプリを作ったの

「クラスを使わない」ってのはわかったけど、じゃあ「モデルに相当するデータをどうやって扱うの?」というところが疑問になるかもしれない。

クラスを使えば1つのクラスに「データ」と「操作」をまとめられるが、クラスを使わない場合はそれぞれを別々の場所に定義する必要がある。つまり、下記のような形になっている

  • データ:Plain JavaScript Object
  • データ操作:関数
// データ
type User = { name: string; age: number };
const init = ({ name, age }) => ({ name, age });

// データ操作
const rename = (user: User, newName: String) => ({ ...user, name: newName });

特に操作の関数は1箇所に集めておき、データ操作をする場合は直接変更をしないで必ずデータ操作用の関数を import して操作をすることにした。

クラスなしのアプリを作った結果

実際にクラスなしのアプリを作ってみての所感などを書いていく。

シリアライズをする必要がなくなった

インスタンスJS Plain Object のデータ変換をする必要がなくなった。

今回作ったアプリケーションは下記の2つの特徴がある。

  • Redux を使っている
  • バックエンドがない

そのため、まず Redux だが Store へはインスタンスを格納することができないのでクラスを使う場合は変換する必要があったが、今回のケースでは Plain JavaScript Object をそのままブチ込めばよいだけなので楽だった。

あともしバックエンドがある場合はバックエンド ⇔ フロントエンドの間でもデータ変換が必要になる。つまり、Plain JavaScript Pbjectインスタンスの変換を都度しないといけないと思うが、そこもなくなる。

データ+操作のカプセル化消滅は気合でカバー

インスタンスではなくて Plain JavaScript Object になったことでデータをどこでも気軽に変えられちゃうようになった。

これはとんでもないヤバみである。誘惑に負けて気軽にデータへの操作をそこらへんで書き出すと速攻でやばいコードになる。

このプロジェクトでは「データに対して操作をする場合は model ファイルに集約した関数を import して使うこと」を自分でルール化していたので、データと操作が散らばることはなくてスパゲッティ化することはなかった。

けど、この解決方法って「仕組み化」ではなくて「気をつけていた」という解決方法になるので、「気付かなかった」「面倒だから」みたいな理由でルール破りが発生するとコードが一気にスパゲッティ化する。

なので、チームでやると結構なヤバみだと思う。というか、すでに1人ブロジェクトの今回ですら1箇所に責務のある関数を集める+そこから毎回呼び出すってのが面倒だなーとなっていたので、複数人で開発すると速攻で破綻すると思う。

ちなみに、ふとここの章を書いてる途中に「Plain JavaScript Object に関数をもたせて thisでデータ操作すれば良いじゃん」みたいなことを頭をよぎったけど、それだと本末転倒すぎてもうよくわからなくなった。

クラスを使わないということは、クラスを使わないで済む(進次郎構文)

最近の TypeScript+JavaScript のクラス構文のもろもろを知らなくても書ける。メリットと呼んでよいのかよくわからんけど。

あと、クラスがないので、必然的に継承も発生しない。

所感:無秩序クラスなしプロジェクト

今回は「1箇所にデータ操作をする関数を集める」という方針だったけど、それすらやめて「どこでもデータ操作を可能」にすると無秩序になるなー、とひしひしと感じた。

仮に、めちゃくちゃ小さいアプリでざっと書く場合はこの無秩序なクラスなしは選択肢としてありえて、つまり、「どこでも手軽に書き換え可能」のメリットのほうが大きいケースならありえる。

とはいえ、その結果「プロジェクト全体で責務がバラバラでカオスになる」に至る損益分岐点が、かなり早い段階で来ちゃうと思うし、そこからスケールも出来ないので無秩序クラスなしで書くケースはほとんどないとも思う。


というわけで、クラスなしのアプリを作った結果をつらつら書いてみた。

「クラス使わない」に対する代替方法

今回、クラスなしのプロジェクトを作ってみたけど、メリットはかなり薄いと感じたのでやっぱりクラスを使う事を考える。 個人的にちょうどよい塩梅としては「immer + Class の併用」が良いかと思う。

代替方法「Classes immer」

Immer の機能の1つに Classes な方法がある。

https://immerjs.github.io/immer/docs/complex-objects

詳細は上述のリンクに書いてあるけれども、インスタンスのプロパティが Readonly 化されて Immutable になり破壊的変更ができなくなる。 ロジックに immer の procedure を毎回書かないといけないのでちょっと気持ち悪いけど、その気持ち悪さもクラス内部にカプセル化されるので、外側に漏れることがない。

代替方法「Immutable.js」(非推奨)

https://immutable-js.github.io/immutable-js/

Immutable.js を使うという選択肢もあるけど個人的にはおすすめできない、ということだけを記述しておきたい

  • Immutable.js の API を覚えないといけない
  • Immutable.js の API と Plain JavaScript の API が混同する
  • 間違えて Plan JavaScript の API を使ってもエラーが発生しないケースがある

過去にジョインしてたプロジェクトで使ってたけど、何回こいつのせいでバグを生み出したかわからないってくらいハマったし、毎回 API の呼び出し方を調べないといけなくて生産性も激下がりだったので、immer だけ勧めておきます。

所感

OOP のメリットをぶっ潰すようなムーブとして、あえてクラスを使わないでアプリケーションを作ることでいい感じのヤバさを肌で感じることができる経験だった。

個人的にこれはプロダクションでは採用してはいけないアーキテクチャだと感じる。

なので、次になにかを個人で作ったりこのプロジェクトを大きくリファクタしないといけないなら上述した Classes Immer の代替方法で作ると思う。

(追記)小さい個人プロジェクトを作る機会があったので ClassesImmer を試してみたけどやはり肌感としてはかなり使い勝手が良かった、と記しておく。

https://github.com/snamiki1212/sorting-algorithm-animation

おわりに

クラスを憎まず仲良くなりましょう。