設計の仕方を学んだ

こんにちは！Yuyaです。
現在Xのクローンアプリの開発をしています。

つよつよエンジニアの方々から、設計の考え方でアドバイスをいただいたのでアウトプットしていきます。

今回の記事は以下の人に向いています。
- 設計から実装までの思考プロセスを知りたい
- Web開発初心者の方
- いきなり実装に入るのではなく、段階的に考えることの重要性を知りたい方


# 前提
以下の要件を与えられた前提で考えます。
## ポスト詳細ページ作成
- 該当のポストをクリックするとポストの詳細ページに遷移する
- 最上部にポストの詳細
- ポストの詳細の下にコメント投稿フォーム
- コメント投稿フォームの下に該当ポストに対するコメント一覧
- ポスト詳細画面からコメントを投稿できるようにすること
- 各投稿のポストアイコンの横にコメント総数を表示

:::note warn
コメント への コメントは今回考慮しない
:::

## モデル
以下のPost, Commentモデルが既に用意されていることを前提とします。
```posts/models.py
from django.db import models
from cloudinary.models import CloudinaryField
from django.core.validators import ProhibitNullCharactersValidator
from django.db.models import Q, CheckConstraint

class Post(models.Model):
    user = models.ForeignKey(
        "accounts.CustomUser",
        verbose_name="ユーザーID",
        on_delete=models.CASCADE,
        related_name="posts"
    )
    message = models.CharField(
        verbose_name="ポスト文",
        null=True,
        blank=True,
        max_length=140,
        validators=[ProhibitNullCharactersValidator('スペースだけの投稿はできません')],
        help_text='140文字以内で投稿してください。',
        error_messages={
            'max_length': "140文字以下にしてください。"
        }
    )
    image = CloudinaryField(
        'image',
        null=True,
        blank=True,
        help_text='画像だけでも投稿できます。'
    )
    created_at = models.DateTimeField(
        verbose_name="作成日時",
        auto_now_add=True
    )
    updated_at = models.DateTimeField(
        verbose_name="更新日時",
        auto_now=True
    )


    class Meta:
        ordering = ["-id"]
        db_table = "posts"
        constraints = [
            CheckConstraint(
                check=Q(message__isnull=False) | Q(image__isnull=False),
                name='message_or_image_required'
            )
        ]

    def __str__(self):
        user = self.user.username if self.user else "Unknown"
        return f"{self.message} | {user}"
```
```comments/models.py
from django.db import models
from cloudinary.models import CloudinaryField
from django.db.models import Q, CheckConstraint

class Comment(models.Model):
    user = models.ForeignKey(
        "accounts.CustomUser",
        verbose_name="ユーザーID",
        on_delete=models.CASCADE,
        related_name="comments"
    )
    post = models.ForeignKey(
        "posts.Post",
        verbose_name="ポストID",
        on_delete=models.CASCADE,
        related_name="comments"
    )
    message = models.CharField(
        verbose_name="コメント",
        null=True,
        blank=True,
        max_length=140,
    )
    image = CloudinaryField(
        'image',
        null=True,
        blank=True,
        help_text='画像だけでも投稿できます。'
    )
    created_at = models.DateTimeField(
        verbose_name="作成日時",
        auto_now_add=True
    )


    class Meta:
        ordering = ["-id"]
        db_table = "comments"
        constraints = [
            CheckConstraint(
                check=Q(message__isnull=False) | Q(image__isnull=False),
                name='comment_message_or_image_required'
            )
        ]

    def __str__(self):
        return f"comment: {self.user} -> {self.post}"
```
# 基本設計をもらう → コードを書くまで
以前までの私は基本設計をいただいてから
すぐ以下のように箇条書きで考えていました。

![スクリーンショット 2025-04-30 3.18.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3918502/825fb913-177c-4bb4-a0cd-249d0dfae8ac.png)

結論、いきなり抽象的な設計から具体的なコードレベルまで細かく落とし込みすぎていました。
ここまで細かく書いていること自体は悪くないと思います。

ただ、いきなり細いコードと変わらないレベルまで書くのは非効率なのではないかと考えました。

# 4ステップに分ける
つよつよエンジニアに聞いてみたところ、以下のステップに分けてみたらどうかとアドバイスをいただきましたので、実践してみました。

1. 目に見える動きを挙げる
1. 目に見えない裏側の処理を挙げる
1. 1,2 で抜けている部分がないか確認する
1. コードに記述する

:::note info
現在、取り組んでいることがXのクローンアプリであるため
どのような挙動が起こるか目に見えて分かりやすいです。
:::

# 眼にみえる動き
まずは非エンジニアの方でも分かる、**目にみえる動き**から考えていきましょう

:::note warn
今回はトップページ、ポスト詳細ページの2つの画面遷移についてだけ考えていきます。それ以外の機能については今回は無視します。
:::

要件から考えられることは以下です。
## トップページでの動き
※ トップページの画像

![スクリーンショット 2025-04-26 3.37.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3918502/4f74b020-13da-474e-b924-704ae0b5c35b.png)

- 該当ポストクリックでポスト詳細ページに遷移

## ポスト詳細画面での動き
※ ポスト詳細のイメージです。

![スクリーンショット 2025-04-30 2.46.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3918502/dce8606c-00d8-4cc5-a342-634d042239d8.png)
- 「Post」の左矢印をクリックでトップページに遷移
- コメントフォーム入力して Reply ボタンをクリックすると、自分のコメントがコメント一覧の最上部に表示される
- コメントが完了すると以下の項目が最上部へ表示される
  - 投稿者情報（ユーザー画像、ニックネーム、ユーザー名）
  - コメント内容
- 以下のことをするとバリデーションエラーが表示
  - 141文字以上で Reply クリック
  - 文章、画像両方ともなしで Reply をクリック

ざっと目にみえる動きとしてはこのくらいです。
# 裏側の処理
まず、目にみえる動きから分かる裏側の処理は以下です
### ポスト詳細ページに遷移
- ページ遷移に伴うURL設計
- クリックして投稿情報、投稿者情報をURLに渡す

### ポスト詳細ページ
- 矢印クリックでトップページへ遷移するようにリンクを追加
- URLでパラメータとして渡した投稿者情報を出力
- 該当ポストのコメント総数取得

### コメント作成
- コメント作成に伴うURL設計
- バリデーションエラーの実施

# 抜けているところがないか確認
特に確認すべきところはデータベースの部分です。
制約やカラム追加などがあると、後々の変更が面倒だからです。

最悪データ移行になるパターンもありますので、細心の注意を払いましょう！

# 最終的なイメージ
簡単ですが、まとめると以下のようになります。

![スクリーンショット 2025-04-30 3.04.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3918502/8d7a8a10-32dc-423d-abf5-4fbd171aadb5.png)

# 結論
このように分けて考えると思考が整理されると思います。

目に見える動き → 裏側の処理 → コード記述
この順番になります。それからコード記述の段階で以下のようにさらに粒度を細かくして考えることができます。
- ポスト詳細だから `DetailView`
- Comment モデルに紐づくからコメント作成の際は `ModelForm`
- 該当ポストのコメント総数は「集計」にあたるから `annotate`

このように考えられます。
これでわからない部分が出てきたら、調べていきコードを実装していきます。

このステップをすることで、いきなり抽象から具体すぎるということは無くなりました。まだ、割と簡単な実装しかしていないので、メッセージ機能や通知機能といったモデルから作成する必要がある場合、新しい問題点が出てくるかもしれません。

その時はまたアウトプットします！