Help us understand the problem. What is going on with this article?

リリースまで至らなかった個人開発サービスのコードを全公開して反省してみる【Nuxt + Go】

これはなに

これはDeNA20卒内定者エンジニアによるアドベントカレンダーDeNA 20 新卒 Advent Calendar 2019の記事として書かれています。

はじめに

僕は趣味の一環でWebサービスを作ったり作ろうとしたりしています。ちょうど一年程前に企画・開発を始めたのですが、リリースまで至らなかったサービスがあったことを思い出したのでこれを機にコードを全公開して振り返ってみることにします。

公開したコードはこちらになります。

https://github.com/tockn/emukone_public

自分で実装しておきながらすごく無責任なのですが、正直どういう思想で実装していたのかその詳細はもう忘れてしまっているので、コードを読んで思い出しながら書く形になります。

なぜリリースしなかったのか?

まずはこれです。当時DDDやクリーンアーキテクチャといったソフトウェアアーキテクチャに興味があり、どうせ実装するなら試したことない構成で色々やってみようとしていました。しかしこれが罠で、要件に合っていないオレオレな酷い構成で作り始めてしまい、実装すればするほどモチベーションが下がり、最終的に死んでしまいました。悲しいですね。

どんなサービスだったのか?

ズバリ、「アーティストとライブハウスをマッチングをするサービス」です。高校時代バンド活動をしていた経験から思いつきました。お金儲けだけを中心に考えている質の低いイベントを淘汰し、本当に良い時間・空間を作り上げようと真剣になっている素晴らしいイベントが生き残る世界を実現しようとしたサービスでした。

ざっくり使用技術とか

フロントエンドはNuxt.jsで、バックエンドはGoのよくある構成です。そもそもローカルで開発したまま死んだのでインフラまでは考えてません。
UIフレームワークとして一部SemanticUIを使いました。
データベースとしてMySQLを、マイグレーションにはsql-migrateを使いました。
ローカル開発環境にdocker-composeを使っていました。
CircleCIを使ったCI/CD環境も一応整えていた気がします。

DB設計

MySQLWorkbenchでモデリングしたものがまだ残っていたので貼っておきます。

image.png

ユーザーが、アーティストである「アーティストユーザー」と、イベント主催専用ユーザーの「ブッカーユーザー」に別れていたので、それに合わせた設計になっています。
このDB設計は非常に苦しんだ覚えがあります。READMEにこんな一言が添えられていました。。。

image.png

(どうせ個人開発なので問題ないけど。)

フロントエンド(Nuxt.js)

動作

どんな感じの見た目で動作をするのか紹介してみます。例としてアーティストユーザーのプロフィールの編集画面を晒します。

Youtubeのリンクを貼ると埋め込み形式に変換してくれる機能なんかも作りました。

構成

.
├── assets
├── components
│   ├── atoms
│   │   └── AtomButton.vue
│   ├── molecules
│   │   └── MolContentCard.vue
│   ├── organisms
│   │   └── OrgProfileHeader.vue
│   └── pages
│       └── PageArtistProfile.vue
├── layouts
├── middleware
├── node_modules
├── pages
├── plugins
├── static
└── store

Atomic Designの概念を取り入れたコンポーネント構成になっています。

コンポーネントとして切り出す作業ですが、初めから部品を切り出して組み合わせるのではなく、まず初めに一つのコンポーネントでページ一枚を作りました。
そしてそのページを実装していく上で再利用することになった(なりそう)な部品を切り出していくことで、最終的にAtoms, Molecules, Organismsとして構成していきました。

Atomsはその名の通り原子のようなそれ以上分解できないコンポーネントとして、Moleculesは複数のAtomsを組み合わせることで実装されるよう分解していきました。
Atoms, Moleculesはサービスのドメイン情報を一切含めないようにし、他サービスでも利用できるような疎結合なものにすることを心がけるようにしました。
そして、OrganismではAtoms、Moleculesを利用してドメイン情報の入った再利用するコンポーネントとして構成しました。

図示すると以下のような感じです。

image.png

サーバーサイド(Go)

構成

オレオレなレイヤードアーキテクチャになっています。

.
├── docs
│   └── swagger
├── domain
│   ├── entity
│   ├── repository
│   └── service
├── infrastructure
│   └── repository
│       ├── mysql
│       └── s3
├── interface
│   └── web
│       ├── handler
│       ├── middleware
│       └── router
├── migrations
│   └── mysql
├── registory
└── usecase

図示すると以下のような形です。

image.png

構成についての感想

正直、これがとにかく失敗でした。結局今回の実装の大部分って普通のCRUDなんですよねー。それをこんな形で過度にレイヤ化したり抽象化していってしまうと、普通のCRUDの実装に膨大な時間がかかってしまいます。当たり前ですが。

テストコードも殆ど書けていないので、レイヤ化の意味が殆どありません。

おまけにdomain/entityのstructにjsonタグを書いてしまっているので、レイヤードにすら作れていません。APIレスポンスの形式を変更するためにドメインエンティティーを触らなければいけないのは、レイヤードアーキテクチャの意味を成してないですよね。

domain/entity/user.go
type User struct {
    ID              string        `json:"id"`
    Name            string        `json:"name"`
    IconURL         string        `json:"icon_url"`
    Identifier      string        `json:"identifier"`
    HeaderImageURL  string        `json:"header_image_url"`
    MetaDescription string        `json:"meta_description"`
    Tags            []*UserTag    `json:"tags"`
    Locations       []*Location   `json:"locations"`
    WebsiteURLs     []*WebsiteURL `json:"website_urls"`
    UserImages      []*UserImage  `json:"user_images"`
}

とは言っても、これでAPIレスポンスのためのstructを別のレイヤに生やすと、entity.Usermysql.User間に加えてさらにresponse.User間を変換するマッパーのコードを書くことになるので、しんどさが増していたと思います。
実装の大部分が普通のCRUDなのにこんな構成にしてしまうのは、失敗以外の何物でもないです。これなら他の言語やフレームワークを使ってMVCだけで作った方が断然良いです。

swaggo

interface/web/handlerを覗くとわかるのですが、swaggoを使って自動でswaggerの生成なんかもやっていました。以下のようにコメントで書くだけでswaggerを自動生成してくれます。便利〜

interface/web/handler/user.go
// GetUserMeta godoc
// @Summary  ユーザーのメタ情報(概略)を取得する
// @Description
// @Accept  json
// @Produce  json
// @Param userID path string true "user id"
// @Success 200 {object} entity.UserMeta
// @Router /users/{userID}/meta [get]
func (h *user) GetUserMeta(c *gin.Context) {
    id := c.Param("userID")
    du, err := h.usecase.ShowMeta(id)
    if err != nil {
        ErrorResponse(c, err)
        return
    }
    c.JSON(http.StatusOK, du)
}

開発を振り返って

コードを読んでその時考えていたことを思い出しながら記事を書いていましたが、今なら絶対やらないような事をたくさんしていて恥ずかしくなりました。

振り返れば、個人でパブリックにリリースして使ってもらったサービスは一つだけで、しかも初めてWebプログラミングを学んだ勢いで作りリリースしたものでした。
それ以降様々な概念を学び、より良いコードを書けるように、より良い設計ができるように励んできましたが、逆にその知識が足枷となり「こんなコードじゃダメだ」「こんなアーキテクチャはクソだ」と実力が理想を上まらず、落ち込み、結果モチベーションがなくなりリリースできない。。。その繰り返しでした。

ではなぜ初めてのサービスはリリースできたのか?と考えてみると、自分の技術に対して一切期待をしていなかったからだと思います。初めてだし仕方ないや〜の精神を貫いた結果、爆速実装と爆速リリースができました。「動いてるからヨシッ!」を肯定するわけではないのですが、結局どんなに素晴らしいコードで、アーキテクチャで、思想を持って作っていたとしても、リリースしてユーザーに触ってもらわない限りは何の意味もありません。

Done is better than perfect

本当に良い言葉だと思います。
新しく何かを作り始めるときは、とにかく「作りきる事」を第一に考えて手を動かしていこうと心に刻みました。

(改めて見てみると、ファイル数すごいw)
image.png

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away