🐈

Reactでオブジェクト指向やってみる #状態管理ライブラリ一切不要

2024/07/06に公開
4

オブジェクト指向を取り入れると

  • 『可読性』『拡張性』『汎用性(=ひいては保守性)』 の三大柱(自称)が担保され、現場でドヤれる
  • メソッド名やメンバ変数名が1~2単語で済む #ポリモーフィズム
  • 主語.動詞(補語 | 目的語) S+V+C、S+V+O の形になる
    • 例1: $(A).appendTo(B)
    • 例2: user.name: string
    • 例3: user.birthday: Date // 2021/01/02
    • 例4: user.getAge(): number // ${3}歳
  • wonderful!! ツリー状のオブジェクト
    • 例5: user.posts[n].likes[n].createdBy: User Aの投稿にイイネしたBさん
    • 例6: user.posts[n].comments[n].likes.length コメントにイイネされた数
    • 例7: user.posts[n].comments.create({ title, body, by }: Comment)
    • 例8: user.posts[n].comments.fetch(): Promise<Array<Comment>>
    • 例9: user.posts[n].comments[n].update({ title, body })
    • 例10: user.posts[n].comments[n].medias[n].delete()
  • 上の例では、さらにAPI通信のCRUDを備えた抽象クラス(例: Document API CRUD)からextendsすればすべてのエンティティがCRUD通信を行えるようになる。 #拡張性

オブジェクト指向のコツは主語を見極めること。

(例えば画像編集コンポーネントを作る際、以下のような登場人物(=エンティティ)がある

これらの依存関係を整理して、親となるオブジェクトを見極め、それを主語とすると良い

※サンプルコード欲しければ掲載します

Reactとオブジェクト指向の融合

DI(依存性注入)。

ちょっと凝った例でごめんなさい。push通知関係のカスタムフックです。

FCM(Firebase Cloud Messaging)
const useFCM = (mine: User) => {
  const [permission, setPermission] = useState(Notification.permission);
  const [isLoading, setIsLoading] = useState(false);
  const fcm = new FCM({ permission, setPermission, isLoading, setIsLoading });

  useEffect(() => {
    // 初期処理
    if (mine && fcm.isGranted()) {
      fcm.getToken().then((token) => {
        mine.tokens.push({ token });
      });
    }
  }, []);

  ...

  return fcm; // 外部にインスタンスを提供し、自由にpush通知を操作できるようにする

SWRなどのライブラリを使っている場合は、SWR.dataなどの返り値をDIしてあげてください

どうやってこんな綺麗なコードが書けるのか?

責務がスッキリ。
疎結合にしてテストを容易にしています #DI((Dependency Injection))

FCM.ts
import { getMessaging, getToken, onMessage, isSupported } from "firebase/messaging";

export default class FCM {
  messaging: Messaging;
  // DI
  constructor({ permission, setPermission, isLoading, setIsLoading }) {
    this.permission = permission;
    this.setPermission = setPermission;
    this.isLoading = isLoading;
    this.setIsLoading = setIsLoading;
    this.messaging = getMessaging(app);
  }
  isGranted() {
    // 個人的にはこういうのもDRYの法則で隠蔽化。修正範囲を最小限にしています。
    return this.permission === 'granted'; // 'approve'という値も入ってきたとしても
    // たったこの行↑を OR演算子(||) 追加するだけで修正が済みます
    // #保守性 #可読性 #規模が大きいほどに威力増すタイプ
  }
  async getToken(): Promise<string | undefined> {
    ... // 4行くらい
  }
  // INFO: push通知用メソッド
  async pushTo(targets, data: FCMpayLoad) {
    ... // 7行くらい
  }
  async listen() {
    ... // 3行くらい
  }
  async subscribe() {
    ... // 5行くらい
  }
  ... // 他メソッドも追加し放題。パケ放題。            ※2024年
}

仕事の割り振りもかんたん。PMうれぴ。 (テストも同様)

粒度: メソッド単位 / クラス単位 / 機能単位 / 要件単位

Need more sample codes?

Reactのstate管理
const [name, setName] = useState('ハリー・ポッターと秘密の部屋');
const room = new Room({ name, setName });
room.name // stated variable
room.setName('アズカバンの部屋');
API通信系
// メソッドの内部でstateをいじってあげるとViewと同期するようになって、脳汁でる
class Room extends CRUD {...}
room.update({ name: '赤くない部屋' });

class ChatRoom extends Room {...}
chatRoom.messages.create({ title, body, from: User, to: User });
chatRoom.messages[n].update({ title, body }); 

class PrivateChatRoom extends ChatRoom {...}
privateChatRoom.messages[n].update({ title, body });

const data = await room.fetch();
privateChatRoom.delete();

ディレクトリ構成

  • 推奨ディレクトリ: src/model/
    • 例: User.ts
    • 例: Media.ts
    • 例: Comment.ts
    • 例: Thumbnail.ts
    • 例: Room.ts
    • 例: Member.ts
    • 例: List.ts
    • 例: Content.ts

まとめ

手続き型プログラミングして関数の多さに泣いてるひと、コード量が増えて依存関係の見通しの悪さに難読を強いられている&解読している時間のほうが長ぇひとは、まずはいくつかクラス(エンティティ)定義して、グルーピングしちゃいましょう。

長い関数名引数の羅列で悩まされていたあなた、
(sendMessageInPrivateChatRoom(fromUser, toUser, body, title, createdAt) とか笑😂)。
それから大量のif文にテスト書くの疲れた、学生諸君。
そもそもif文が入れ子してインデントの深淵ができてしまう、番台のおじいちゃん。

幸せになれます。

Discussion

llc_starhacksllc_starhacks

お持ち帰り用Code:

/**
 * @ref: https://zenn.dev/llc_starhacks/articles/929e58b503d6ce
 */
class ChatRoom extends Room {...}
chatRoom.messages.create({ title, body, from: User, to: User });
chatRoom.messages[n].update({ title, body });

class PrivateChatRoom extends ChatRoom {...}
privateChatRoom.messages[n].update({ title, body });

const data = await room.fetch();
privateChatRoom.delete();

const [name, setName] = useState('ハリー・ポッターと秘密の部屋');
const room = new Room({ name, setName });
room.name // stated variable
room.setName('アズカバンの部屋');
Hidden comment