これは、筆者の「2025振り返り用ひとりアドカレ」記事の一つです。
はじめに
筆者がJavaScriptを学び出した頃は「共通のロジックや処理、振る舞いなどはオブジェクトでカプセル化する」と学びました。
学び始めの頃は、うまくイメージ・理解できない部分も多かったのですが、実務で触っていくうちに、そしてReactでコンポーネント指向に触れるうちに自然と実感していきました。
Reactでは現在、関数コンポーネントが主流ですが、クラスコンポーネントというクラス準拠の書き方もあります。
普段筆者は特に意識することなく主流となっている関数コンポーネントで書いているのですが、ふと「そもそも関数でも、オブジェクト(クラス)でも実現したいことはできるよね?じゃあどんなケースで使い分けるの?」と思ったのです。
両者の使い分けについて疑問を感じたきっかけ
AIと協業する中で、彼らにタスクを依頼する際の実装アプローチ(=設計)が重要だと感じました。
そう考える中で「ある処理を実装したいと思った時にそれをオブジェクトで作るか、関数で作るか」という分岐によってAI成果物の精度・品質にも影響が出るのかもと思ったのです。
オブジェクト指向と関数コンポーネント指向
筆者は「オブジェクト指向」や「(関数)コンポーネント指向」を以下のように捉えています。
- オブジェクト指向 (OOP:
Object-Oriented Programming)
特定の処理を実現するための振る舞い(メソッド)やデータ、属性といったものを一つにして、それらを自由・柔軟に組み合わせてシステム・プログラムを効率的に構築していく手法や概念 - コンポーネント指向または 関数型プログラミング (FP:
Functional Programming)
各種機能や描画処理などをパーツごとに分割するイメージで、それらを自由・柔軟に組み合わせてシステム・プログラムを効率的に構築していく手法や概念
上記の通り「自由・柔軟に組み合わせて効率的に構築するという部分(=自由に使いまわせる特性)が両者共通」という前提です。
筆者は慣れ親しんでいる関数(コンポーネント指向)で普段作成するのですが、明確にオブジェクト(指向)の場合が良いケースとはどういったものでしょうか?
例えば、カプセル化は両者ともに似た性質で、継承については少し異なりますがTypeScriptではextends(構造的部分型の制約)がありますし、ポリモーフィズムについてもpropsや引数に渡す実引数でどうにかできそうです。
- ポリモーフィズム
「同じインターフェースで、異なる実装を持つオブジェクトを統一的に扱える仕組み」のことを指します。
例えば、動物という共通のインターフェースがあり、鳴く()メソッドを持つとして、犬クラスでは「ワンワン」、猫クラスでは「ニャーニャー」と実装する。
コードでは動物.鳴く()と書くだけで、実際のオブジェクトが犬なら犬の鳴き方、猫なら猫の鳴き方が実行される、といった具合です。
色々な実装例を経て得た見解
- 1.状態の変化が複雑な場合
// 関数だと状態管理が煩雑
function processOrder(order, action) {
/* 毎回全状態を渡す必要 */
/*(グローバルステートを利用するとかは一旦隅に)*/
}
// オブジェクトなら状態を内包
class Order {
ship() { /* 内部状態を自然に変更 */ }
cancel() { /* 状態遷移のルールを内包 */ }
}
- 2.複数の関連する操作がある場合
// 関数だと操作が散らばる
calculateTax(user, items)
applyDiscount(user, items)
generateInvoice(user, items)
// オブジェクトなら操作をまとめられる
class Invoice {
calculateTax() { }
applyDiscount() { }
generate() { }
}
- 3.複雑な状態とロジックを持つ「ドメインモデル」
関数コンポーネントのメリット
// カスタムフックで実装する例
function useShoppingCart() {
const [items, setItems] = useState([]);
const [appliedCoupon, setAppliedCoupon] = useState(null);
const addItem = (product, quantity) => {
// items配列を更新するロジック...
};
const removeItem = (productId) => {
// items配列を更新するロジック...
};
// 合計金額の計算はuseMemoを使う
const totalPrice = useMemo(() => {
// itemsとcouponから合計金額を計算する複雑なロジック...
}, [items, appliedCoupon]);
return { items, addItem, removeItem, totalPrice, /* ...他の値や関数 */ };
}
- 4.「継承」による差分プログラミングが有効な場合
クラスのメリット
class User {
name: string;
constructor(name: string) { this.name = name; }
login() { console.log(`${this.name}がログインしました`); }
}
class PremiumUser extends User {
// 振る舞いの追加
usePremiumFeature() {
console.log("プレミアム機能を使いました!");
}
// 振る舞いの上書き(オーバーライド)
override login() {
super.login(); // 親のlogin()も呼び出す
console.log("プレミアムユーザー特典があります!");
}
}
得た見解
確かにクラス(オブジェクト)として一元管理できるのは魅力的だが、責務分離を考慮すると個別に用意したほうが良い気もする……。
また、疎結合を意識して、結局外部から処理に必要なデータを渡すのであれば、やはり大きな違いは無いのでは?
あと、一元管理の利点を一旦差し置いて、関数ロジックの肥大化が問題というなら各処理をさらに細かいフック(プライベートメソッドなど)に分けて責務分離を実施することで疎結合かつテスタビリティも向上しそう。
結論
1人でもんもんしても埒が明かないので、AIに相談してみました。
以下がAIの回答を経て得た結論です。
クラス(オブジェクト)によるインスタンス生成や管理と、
Stateなどの生成・管理や再レンダリング処理などパフォーマンスや処理負荷の面は無視できるレベルである。
責務分離を意識過ぎてもコード全体の把握・理解しやすさが損なわれる可能性があるため、各ロジックやデータ、属性など暗黙的部分を明確に所有しているラッパー(親)部分がクラスである というように直感的理解できるクラスのほうが場合によってはベターとなる。
継承に関しても直近の説明と同様、合成/コンポジション(疎結合的なアプローチ)より継承(密結合的なアプローチ)のほうがベターなケースもある
※一般的には「継承より合成(favor composition over inheritance)」という言説が多い
実用的な使い分けの参考例
- 初期フェーズは関数と型だけで構築(柔軟性とスピード重視)
- 共通パターンが見えてきたらクラスや抽象クラスで整理
- フローが分かりにくくなったら「意味単位」でグルーピング(OOP的まとめ)
上記はあくまで参考であり、環境やスケジュール、チームの状況などを考慮したトレードオフのもと選定するのが一番重要。
総括(個人的所感)
クラス(オブジェクト)で作っても、関数で作っても、実現できる対応領域の広さや処理内容はそこまで変わらないという印象を得ました。
ただし、両者におけるアプローチの違いとして、可読性や保守性、拡張性などに影響してくるため常に意識しておく必要はあると思います。
あと、どちらで開発していくかという判断待ちのせいでプロジェクトが進まないのは避けるべきなので、個人またはチームメンバーが慣れ親しんでいるアプローチで実装するのが無難かつ現実的だとも感じました。
冒頭で少し触れたように昨今ではReactの普及により、フロントエンド開発では関数型アプローチが主流となっています。
AIへ指示する際は情報量の多さという観点から関数コンポーネント、関数型プログラミングの方が良いかもしれません。
2025/12/03 追記:
コメント欄で教えていただきました。@junerさん、ありがとうございます!
関数だと呼び出す度に用意しているメソッドまで新たに作成されてしまいます。
しかしclassだと、そういったことはない上にメモリ消費の削減という利点もあるので、細かなパフォーマンスまで意識した場合はclassのほうが優位に立ちます。
- コメント欄で教えていただいた情報リンク先
ここまで読んでいただき、ありがとうございました。
筆者の知識不足もあって至らぬ部分もあるかと思いますので、何かお気づきの方はご教授いただけますと嬉しく思います。