クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

新規事業のメディア開発にContentfulを採用した話

はじめまして!飲むヨーグルト飲みすぎてお腹壊してる系エンジニアの ryt_kwzacky です!!

flintチームに加わってから、クラウドリンクスのフロントエンド開発をしたりチームのビジネス課題をエンジニアリングで解決したり飲むヨーグルトを飲んだりしています。

現在flintではハイエンド向け副業プラットフォームクラウドリンクスという事業を作っています。

2020年1月に正式ローンチした本サービスですが、実際にプラットフォーム上でマッチングする例が多々出てきました。

そこで、発生したマッチング事例を取りまとめメディアとして発信していくことになり、そのメディアを開発する上で採用したHeadlessCMSであるContentfulについて紹介させていただきます。

最終的な結論としては、Contentful + Nuxt.js + Firebaseの構成で自由度の高いメディアをすぐに立ち上げられるよ!ということになります。

記事の内容とターゲット

この記事では、クラウドリンクスのメディアを開発するにあたって採用したContentfulについて述べます。

特に以下のような方々に読んでいただければと思います。

  • HeadlessCMSってそもそも何?という人
  • (Vue.jsやReact.jsなどのフロントエンドの開発ができ、)何かメディアを立ち上げたいと思っている人

前者については、WordPressなどの一般的なCMSと今回紹介するHeadlessCMSの違いについて簡単に掴めると思います。

後者については、コンテンツを管理する際に、noteなどのブログサービスや安直なWordPress以外の選択肢が得られると思います。

逆に、Contentfulの実際の使い方は解説しませんので、詳しい使い方を知りたい方は想定ターゲット外の記事となっています。ご了承ください。

目次

成果物

クラウドリンクス - マッチング事例

f:id:ryt-kwzacky:20200409124107p:plain f:id:ryt-kwzacky:20200409124113p:plain

Contentfulとは?

API-first content platform to build digital experiences | Contentful

f:id:ryt-kwzacky:20200409145643p:plain

Contentfulは、「HeadlessCMS」と呼ばれるAPIベースのCMSです。

APIベースでコンテンツを管理・取得できるため、フロントエンド(表示する側)とバックエンド(管理する側)を完全に切り離すことができます。

表示(フロントエンド)側での制約がなくなり、管理しているコンテンツをマルチデバイス向けに扱うことができるようになりました。

f:id:ryt-kwzacky:20200410115451p:plain
従来のCMS(Wordpress)を使った構成とHeadlessCMS(Contentful)を使った構成の比較図

Contentful採用までの意思決定フロー

1. クラウドリンクス用のメディア開発が決定

まずは、flintでメディアを開発するに至った背景です。

2020年の1月にローンチしたハイエンド向け副業プラットフォームであるクラウドリンクスで、実際にマッチングする例がいくつか出てきました。

そこで、発生したマッチング事例を取りまとめ、メディアとして立ち上げ発信していくことになった、というのが今回の簡単な開発背景です。

2. 既存サービス (noteなど) vs 自前 (CMS, HeadlessCMS)

メディアを立ち上げるにあたって、noteなどのブログサービスを利用することももちろん検討しました。

既存サービスを利用することの主なメリットとして、立ち上げや運用にコストがかからないこと、プラットフォーム自体にユーザーが定着しているためPV数が稼ぎやすいことなどが挙げられます。

しかし、既存サービスを使ってしまうとどうしてもそのサービスの世界観が滲み出てきてしまい、訴求していきたい世界観に多少濁りが出てきてしまうのではないだろうか、ということが懸念されました。

(例えば、同じ記事でもnoteのコンテンツとして読むのと、アメブロのコンテンツとして読むことでは、受ける印象は全く違うと思います。)

また、使用サービス自体を移行する必要性が出てきたときも、使用しているサービス自体のAPIに頼らざるを得なくなり、そこで追加工数が発生してしまうかもしれないということも懸念点としてありました。

これらの要素を比較検討し、既存サービスは使用せずコンテンツは自前で管理する、という意思決定になりました。

3. HeadlessCMS vs (従来の) CMS

そして、自前でコンテンツを管理するにはCMSを導入することが必要になってきます。

CMSというと一般的にはWordPressが第一に思い浮かぶと思いますが、今回flintで採用したものはContentfulというHeadlessCMSです。

従来のCMSではなくHeadlessCMSを採用した大きな理由は、先の説明にもあったようにフロントエンドとバックエンドの切り離しが可能になるためです。これにより

  • デザインが(WordPressに比べて)自由になる
  • 結合度が下がることで保守性が上がる
  • 既にNuxt.jsでSPAを構築していたので取り扱いが楽だった

などの利点が見込めたため、新技術チャレンジという意味も含め、今回は従来のCMSではなくHeadlessCMSを採用するに至りました。

4. Contentful vs microCMS vs GraphCMS

次に、いくつかあるHeadlessCMSの候補の中からなぜContentfulを採用するに至ったかです。 具体的には以下のような候補がありました。

  • Contentful
  • microCMS
  • GraphCMS

これらの中からContentfulを選んだ理由は、(料金)×(機能)で考えたときのコストパフォーマンスが最大になる選択肢だったからです。

無料の範囲で十分な運用ができそうだったことに加え、特に重要だったのが採用検討中2019年11月下旬時点でWebhookが使えたこと(コンテンツのPublishやUpdate時にCIを走らせたかったため)です。これが決め手となりContentfulを採用する意思決定をしました。

ちなみに、現時点で改めて同様の要素で比較をしようとすると判断するのがかなり難しくなっていると思います。なぜならmicroCMSにも2019年末にWebhook機能が追加されたからです。(リンクスメディアの実装がほぼ終わったのと同時に追加されたのは笑いました。)

microCMSは日本製なので日本人にとってかなりとっつきやすく何ができるか簡潔に分かりますし、Webhook以外にも日々機能も増えてきているので非常におすすめのHeadlessCMSです。

microCMS | APIベースの日本製ヘッドレスCMS

また、ContentfulとGraphCMSの比較はこちらの記事が非常に参考になります。

【邦訳】GraphCMSとContentfulの比較 - Qiita

(極極) 簡単な使い方

簡単な実装コードを載せようと思いますが、検索すれば丁寧に解説してくれている下記のような良記事が数多くありますので、細かい使い方はそちらを参照してください。

Contentfulの料金と使い方を整理しつつ、Nuxt.jsと組み合わせてブログを作る - Qiita

Nuxt.jsとContentfulでブログ作ってみた - Qiita

コンテンツを表示するコード

// _slug.vue

<template>
  <div class="posts">
    <div v-for="(post, index) in posts" :key="index">
      {{ post.fields.title }}
    </div>
  </div>
</template>

<script>
import client from '~/plugins/contentful'

export default {
  asyncData() {
    return client
      .getEntries({
        content_type: 'post',
        order: '-sys.createdAt'
      })
      .then((entries) => {
        return { posts: entries.items }
      })
      .catch((e: any) => console.log(e))
  }
}
</script>

コンテンツを静的ファイルとして生成するコード

// nuxt.config.ts

generate: {
  routes() {
    return client
      .getEntries({ content_type: "post" })
      .then((entries: any) => {
        return entries.items.map((entry: any) => {
          return {
            route: "/articles/" + entry.fields.slug,
            payload: entry,
          }
        })
      })
  },
},

Contentful管理画面

f:id:ryt-kwzacky:20200409125718p:plain
コンテンツ一覧

f:id:ryt-kwzacky:20200409125648p:plain
コンテンツ詳細①

f:id:ryt-kwzacky:20200409125654p:plain
コンテンツ詳細②

今後のクラウドリンクス構成

これまでの内容を踏まえて、参考までにContentfulを組み込んだクラウドリンクスの全体的な構成を簡単に紹介して締めとしようと思います。

f:id:ryt-kwzacky:20200415150946p:plain

クラウドリンクスはモノリシックリポジトリ(モノレポ、monorepo)で運用しています。

GitHubにプッシュするとそれを検知してCIが走り変更点を検証し、問題がなければ自動的にビルド、Firebaseへのデプロイが行われ変更を反映できるようになっています。

Contentfulの機能として備わっているWebhookを使用することで、新コンテンツをパブリッシュしたときやコンテンツに変更を加えたときCircleCIに通知することで、GitHub上のコードは変更されておらずとも再ビルド・アップロードを行うことができ、コンテンツの変更を反映できるようになります。

なお、図中のCloud Functionsは現在Kotlinに置き換え中です。

おわりに

Contentful + Nuxt.js + Firebaseですぐにクラウドリンクス仕様のメディアを作ることができました。完全に0からでも表面的な実装をするだけなら1週間もかかりません。

HeadlessCMSを採用するメリットとしては、自由度が高いこと、保守性が高いことです。 デメリットとしては、既存サービスを利用することに比べ工数がかかること、そもそもフロントエンドの開発技術が必要なことです。 まとめると、メディアを作る際に自由度の高いことが望まれ、かつVue.jsやReact.jsなどのフロントエンドの技術があればHeadlessCMSを採用することは第一候補になり得ます。

その他の使い方としては、画像ペライチのLPを管理して、LPページを量産することもできるのでは?というようなこともチーム内で軽く話しています。 このように、工夫して使うことでさらに可能性が見えてきそうです。

We are Hiring!!

クラウドワークス では、成長し続けたいエンジニアを募集しています! www.wantedly.com

東京都 新型コロナウイルス対策サイトにアクセシビリティ視点でコントリビュートしてみた

腹筋ローラーしろよ。

はじめまして。2019年10月に中途入社したフロントエンドエンジニアの yamanoku と申します。

デザインシステムの開発チームとして入社して、現在はマーケティングチームと連携したフロントエンド開発をメインに行っております。個人的に社内や外部勉強会などでアクセシビリティ*1にまつわる啓蒙活動なども行ったりしています。

2020年3月現在、新型コロナウィルスの脅威は依然続いており、弊社でも一時的に全拠点在宅勤務(テレワーク)が行われる*2など対策を講じていたり、個人でも社会でも常に身の回りの危険を配慮しなければならない予断を許さない状況です。

色々と自粛が続き苦しい状況ですが、今回はそんな新型コロナウィルスに関連するOSSプロジェクトに、私がアクセシビリティの観点でコントリビュートしたことについて紹介させていただきます。

*1:アクセシビリティとは「アクセスしやすさ」とも訳され、どんな人でも共通に情報にたどり着けるようにする目的・考え方です。

*2:https://crowdworks.jp/press/?p=9767 , https://crowdworks.jp/press/?p=9790

続きを読む

リファクタリングチームに入ってから学んだ理解しやすいコードを書くための基本的なこと

こんにちは!

去年の4月に新卒入社してからお酒ばかり飲んでいるエンジニアのd4teです。

4月から11月まではUX改善チームにてお仕事検索画面のフロントエンド開発を担当しておりましたが、11月からはリファクタリングチームにてcrowdworks.jpリファクタリングをしています。

現在のcrowdworks.jpの状況

過去の記事にもあるように、crowdworks.jpはサービスインから約8年が経過し、30万行を超えるモノリシックなRailsアプリケーションになってきていて、コード行数の増加量やファイル変更数の推移は年々鈍化してきています。

内部には開発生産性を低下させる技術的負債が溜まってきており、技術的な投資がしづらくなってきているという課題があります。

自分が所属しているチームは、外部から見た動作を変えずに内部のコードを整理するリファクタリングで技術的負債を解消し、開発生産性の向上をミッションとしたチームです。

リファクタリングチームの活動については是非こちらもご覧ください。

engineer.crowdworks.jp

qiita.com

この記事で書くこと

サービス事情はわかったのですが、リファクタリングチーム移動直後は「技術的負債、、なんかやばそう」くらいの認識でコードの良し悪しや複雑さを生んでいる原因については自分で説明できるほど理解していませんでした。

そんな中同僚の@MinoDrivenさんに「リファクタリング 既存のコードを安全に改善する」という本を貸していただきました。

その本曰く、問題を引き起こす可能性の高いコードからは不吉な臭いがするらしく、本にはその不吉な臭いを改善するための手段が詳細に書かれており、自分でもすぐに実践することができました。

コードの臭いとは、コンピュータプログラミングにおいてプログラムのソースコードに深刻な問題が存在することを示す何らかの兆候のことを言う。

この記事では、自分がリファクタリングチームで学んだ不吉な臭いの改善方法について実際にリファクタリングを行いつつ書いていこうと思います。

※ 記事中のコードはRuby書かれており、全て架空のものです。

意図が伝わりにくい名前

def calculate(bottom, height)
  return (bottom * height) / 2;
end

result  = calculate(3, 4)

何が悪いのか

  • 定義元ではロジックが記述してあるため、処理の内容は理解できますが、その後関数を利用する場合、何を計算するものなのかは瞬時に理解できず、開発者は定義元のロジックを観に行かなけれはなりません。
  • 名前が曖昧なので、本来この関数でやりたかったこと以外のロジックが入り込んでくる可能性がある(例えば四角形の計算ロジックなど)

改善策

このような曖昧な名前に対して、処理内容が伝わるように具体的な名前に変更すると下記のように修正できるのではないでしょうか?

def calculate_triangle_area(bottom, height)
  return (bottom * height) / 2;
end

area = calculate_triangle_area(3, 4)

今回は非常に簡単な例ですが、処理内容が伝わる特化した名前にすることで何を意味するものなのか開発者は瞬時に判断できますし、具体性のある名前にしたことによって意味の範囲を狭めることができ、無関係なロジックの混入も防ぎます。当然仕様変更時に混乱することも少なくなります。

リファクタリングチームではこのように特化した名前を考えることを 「名前の設計」 と呼んでいて、変数名一つとってもお互いに厳しくレビューしあっていますし、この 「名前の設計」 があらゆるリファクタリングの基本になると思っています。

リファクタリングの「第3章 コードの不吉な臭い」では「不可思議の名前」として紹介されています。

コードには突飛な箇所があってはならず、明快でなければなりません。明快なコードにするためにもっとも重要なのは、適切な名前付けです。そのため開発者は関数・モジュール・変数・クラスなどの名前について行っていることや利用方法がはっきり伝わるように考えなければなりません。

複雑な条件分岐

def purchase_alcohol(customer)
  if customer.age >= 20
    if all_you_can_drink_plan == false
      case customer.selected_alcohol
      when "wine" then
        selected_alcohol = Wine.new(
          price: 1008,
          dead_flag: false
        )
      when "tequila" then
        selected_alcohol = Tequila.new(
          price: 0,
          dead_flag: true
        )
      when "spiritus" then
        selected_alcohol = Spiritus.new(
          price: 1595,
          dead_flag: true
        )
      else
        raise Exeption.new("エラー")
      end
      notification = Notification.new
      self.class.transaciton do
        execute_payment(selected_alcohol)
        notification.save!
      end
    end
  end
end

何が悪いのか

  • 複数のif分岐とcase文が絡み合っており、仕様を理解しにくい
  • サンプルコードのような範囲では、さほど問題にはなりませんが、プログラムに機能が追加されるたびに似たような条件分岐の構造が繰り返し出現するようになり、複雑性が増す可能性が高い。
  • 冗長にインスタンス生成処理が記述されているため、関数自体が長大になってきていて、仕様の変更時に意図しないバグを仕込んでしまう可能性がある。

改善策

シンプルな例ではありますが、下記の2つを使ってこれら複雑なコードを改善していこうと思います。

  • 関数の抽出
  • ガード節

関数の抽出

case customer.selected_alcohol
when "wine" then
  selected_alcohol = Wine.new(
    price: 1008,
    dead_flag: false
  )
when "tequila" then
  selected_alcohol = Tequila.new(
    price: 0,
    dead_flag: true
  )
when "spiritus" then
  selected_alcohol = Spiritus.new(
    price: 1595,
    dead_flag: true
  )
else
  raise Exeption.new("エラー")
end

まず、これら↑case文内部で行われているインスタンスの生成処理を関数として抽出します。

def generate_wine
  Wine.new(
    price: 1008,
    dead_flag: false
  )
end

def generate_tequila
  Tequila.new(
    price: 0,
    dead_flag: true
  )
end

def generate_spiritus
  Spiritus.new(
    price: 1595,
    dead_flag: true
  )
end

そして既存のインスタンス生成処理を関数に置き換えます。

case customer.selected_alcohol
when "wine" then
  selected_alcohol = generate_wine # 追加
when "tequila" then
  selected_alcohol = generate_tequila # 追加
when "spiritus" then
  selected_alcohol = generate_spiritus # 追加
else
  raise Exeption.new("エラー")
end

続けて、それらを制御しているcase文自体も関数として抽出後、メインロジックの処理を置き換えます。

# 抽出したロジック
def generate_selected_alcohol(selected_alcohol)
  case selected_alcohol
  when "wine" then
    selected_alcohol = generate_wine
  when "tequila" then
    selected_alcohol = generate_tequila
  when "spiritus" then
    selected_alcohol = generate_spiritus
  else
    raise Exeption.new("エラー")
  end

  selected_alcohol
end
# メインロジック
def purchase_alcohol(customer)
  if age >= 20
    if all_you_can_drink_plan == false
      selected_alcohol = generate_selected_alcohol(customer.selected_alcohol) # 追加

      notification = Notification.new

      self.class.transaciton do
        execute_payment(selected_alcohol)
        notification.save!
      end
    end
  end
end

これで長い記述が関数に分割されて見通しがよくなりました。 分割された関数は、それぞれ責務ごとに分割されているので、仕様追加などの場合にどこに変更を加えれば良いか瞬時に理解できるようになったのに加えて、それぞれ変更した場合の影響範囲を狭めることができ、変更しやすくなりました。

ガード節

続いて、ガード節を利用してネストしたif文を解消します。

ガード節とは処理の対象外とする条件を関数やループの先頭に集める方法で、ネストを減らして正常系の処理を理解しやすくなる利点があります。

今回の例で言えば、「客(customer)の年齢が20才以上である場合」と「飲み放題プラン(all_you_can_drink_plan)ではない場合」に購入することが可能、ということが読み取れます。 逆に言うと「客が未成年」である場合と、「飲み放題プラン」の場合は購入することができないことがわかります。

if age >= 20 ... end # 客の年齢が20才以上である場合

if all_you_can_drink_plan == false ... end # 飲み放題プランではない場合

すでに「飲み放題プラン」を表現する関数はありそうなので「未成年者であるかどうか」をチェックする関数を作成します

# 未成年者であれば true を返す
def minor?(customer_age)
  customer_age <= 19
end

そしてガード節としてif文を移動します。

def purchase_alcohol(customer)
  return if minor?(customer.age) # 追加
  return if all_you_can_drink_plan # 追加

  selected_alcohol = generate_selected_alcohol(customer.selected_alcohol)

  notification = Notification.new

  self.class.transaciton do
    execute_payment(selected_alcohol)
    notification.save!
  end
end

ガード節を使って条件記述を置き換えたおかげで、条件分岐のネストが解消され、処理の対象外とする条件が理解しやすくなったと思います。

また、追加したガード節は下記のように購入不可能なケースかどうかを評価する関数として切り出しても良いかもしれません。

def purchase_impossible?(customer_age)
  minor?(customer_age) || all_you_can_drink_plan
end

冗長な処理や複雑な分岐処理を関数として抽出し、条件記述をガード節に置き換えただけですが、メインロジックの複雑性は解消され、容易に理解できるようになったかと思います。

リファクタリングの「第3章 コードの不吉な臭い」では「長い関数」として紹介されています。

長い関数は理解するのが困難であり、長い関数は小さい関数に分けるべきです。小さい関数に分けることでコードを追っていくのが大変になりますが、「意図」を示した適切な名前に命名することで、内部の実装を見なくとも先に読み進めていけるはずです。

また今回のサンプルコードではcase文は重複はしていませんでしたが、複雑さを生みやすいものとして書籍中では「重複したswitch文」として紹介されています。

switch/case文や、ネストしたif/else文の形で、コードの様々な箇所に同じ条件分岐ロジックが書かれていればそれは「不吉な臭い」です。重複した条件分岐が問題なのは、新たな分岐を追加したら、すべての重複した条件分岐を探して更新していかなければならないからです。

冗長なコメント

# 購入者が選択した酒の購入処理を実行する
def purchase_alcohol(customer)
  # 年齢が20才以上であれば購入可能
  if customer.age >= 20
    # 飲み放題プランでなければ購入可能
    if all_you_can_drink_plan == false
      # 顧客が選択したお酒ごとにインスタンスを生成する
      case customer.selected_alcohol
      # ワインの時
      when "wine" then
        selected_alcohol = Wine.new(
          price: 1008,
          dead_flag: false
        )
      # テキーラの時
      when "tequila" then
        selected_alcohol = Tequila.new(
          price: 0,
          dead_flag: true
        )
      # スピリタスの時
      when "spiritus" then
        selected_alcohol = Spiritus.new(
          price: 1595,
          dead_flag: true
        )
      else
        raise Exeption.new("エラー")
      end
      # 通知を生成する
      notification = Notification.new
      # トランザクションの処理を実行する
      self.class.transaciton do
        execute_payment(selected_alcohol)
        notification.save!
      end
    end
  end
end

何が悪いのか

  • 実装内容と同じことをコメントしている

改善策

処理の責務ごとに関数やクラスに分離され、適切に名前がつけられている処理はコードを読めば十分に理解できるのでコメントは不要になると考えます。 また、コードと比べると、コメントはメンテナンスされづらい傾向にあり、開発が進むにつれてコードとコメントの内容に乖離が発生し、混乱の元になります。 一方でコメントはコードを読む人の手がかりとなるので、コメントには実装ロジックをそのままなぞる説明ではなく、実装上表現が困難な意図・目的・理由を必要に応じて書くべきだと考えます。

# 購入者が選択した酒の購入処理を実行する
def purchase_alcohol(customer)
  return if purchase_impossible?(customer.age)

  selected_alcohol = generate_selected_alcohol(customer.selected_alcohol)

  notification = Notification.new

  self.class.transaciton do
    execute_payment(selected_alcohol)
    notification.save!
  end
end

リファクタリングの「第3章 コードの不吉な臭い」では「コメント」として紹介されています。

コメントは不吉な臭いではなく、むしろ良い香りです。問題なのはコメントが消臭剤として使われることがあるからです。コメントが非常に丁寧に書かれているのは、実はわかりにくいコードを補うためだったということがよくあるのです。適切なリファクタリングを行った後は、消臭剤として使われているコメントは表面的で意味のないものだったことがわかるでしょう。

おわりに

リファクタリングチームでの活動を通して学んだことを書いてみました。

不吉な臭いがしていたサンプルコードはリファクタリングによって改善され、理解しやすくなったと思います。

リファクタリングチームに加入する前の自分と同じように、コードの良し悪しについてあまりよくわからない方の参考になれば幸いです。

また、今回紹介させていただいた不吉な臭い や それを改善するためのテクニックはごく一部なので興味のある方は「リファクタリング 既存のコードを安全に改善する」を是非読んでみてください!

We are Hiring!!

クラウドワークス では、積極的にリファクタリングを推進していけるエンジニアを募集しています!

www.wantedly.com

Rollbar による快適通知生活

こんにちは、エンジニアの Bugfire です。

最初 protoc について書こうかなと思っていたのですが、時間的な都合で Rollbar についての記事を書きます! 入社してから初めて知ったのですがとても良いサービスです。

クラウドワークスでは、モニタリング用に様々なサービスを利用しています、一例をあげると CloudWatch, New Relic, Datadog そして今回の Rollbar です。

過去の記事 「エラーモニタリングサービス Rollbar と GitHub Issue を利用したエラー対応フロー」 でも一度書かれていますが、今回は設定を詳細に記述します。

サンプルは全て Ruby で書かれています。
SDKの組み込み方法については、プラットホーム毎に異なりますので、ここでは記述しません。

公式サイトのドキュメント からの抜粋であり特別なノウハウはあまりありません。

続きを読む

競技プログラミングにハマっている話

自己紹介

成城石井が大好きなエンジニアの@wonda-tea-coffeeです。普段はユーザーサポート部の方々が使う管理画面の改修をしたりしています。今回は私が最近ドハマリしている競技プログラミングについてお話したいと思います。始めたばかりでちょっと偉そうなこと書いてる気がしますが、温かい目で見てもらえると嬉しいです。

続きを読む

© 2016 CrowdWorks, Inc., All rights reserved.