こんにちは。エンジニアの花本です。
この記事では弊社の開発チームが普段どのように開発をしているか事例を交えてご紹介します。
少しでも弊社の開発に興味を持ってもらえると幸いです。
開発チーム体制
現在開発チームは業務委託を含め14名在籍しています。正社員はフルタイム勤務で、業務委託の方は毎日決まった数時間稼働する方もいれば、週3くらいで自由に稼働する方など各自の生活にあわせて自由に働いてもらっています。
人によっては、バックエンドとフロントエンドの両方のコードを書くこともあり、本人の希望でいろいろな分野の技術に挑戦することができます。
私も入社時はバックエンドメインでしたが、今は半分くらいReactでフロントエンド開発もしています。
開発プロダクト
プロダクトは、toCの「シェアダイン」です。
シェアダインは個人のお宅にシェフが出張し料理を作る、個人とシェフのマッチングサービスです。
また新規事業としてtoBのサービスを開発中です。
こちらは既存プロダクトを軸に、飲食業界のビジネストランスフォーメーションを目指すサービス展開を見据えています。
今回はtoCのシェアダインでメニュー機能を開発した話をします。バックエンドもフロントエンドも私が担当しました。
要件
メニュー機能は、シェフがチャット上でお客様にメニューを提案しやすくする機能です。
以下のような要件があります。
- 毎回同じようなメニューを提案するシェフや、メニューを書くのが苦手なシェフのために簡単にメニュー提案できるようにしたい
- シェフの管理画面であらかじめ提案するメニューのテンプレートを作っておき、シェアダイン内のチャットでクリックするだけでメニューを呼び出して提案することができる
実装
メニュー機能はシェフロールで完結しているので開発はシェフ管理画面(以下シェフ画面)だけを触ります。
シェフ画面のバックエンドはScalaで書かれています(フロントエンドは全面React/Next.jsとTypeScriptです)。そのためAPIはScalaで書きました。
私はScalaを今年の3月ごろに仕事で書き始め4,5ヶ月くらい経過していました。それまではGoを書いていたので命令型から関数型の考え方にすこしずつ慣れていきました。
『Scalaスケーラブルプログラミング 第4版』を1周したあとは辞書代わりに何度も参照しています。またScalaの開発経験が豊富なメンバーもいるのでわからないことは聞くようにしています。
実装の手順は以下のように行いました。
- テーブル設計
- シェフ管理画面のAPI
- シェフ管理画面のフロントエンド
- チャット画面のフロントエンド
テーブル設計
機能は大きく3つにわかれ、メニューブック・メニュー・材料となっています。
メニューブックは複数のメニューで構成され、メニューは複数の材料が登録でき、材料は単体で登録します。MySQLテーブル構造はmenu_book, menu, materialsをもとに3つにわかれた各機能を中間テーブルでつないでいます。
API
Goではアーキテクチャ設計にDDDを使っていて、ScalaもPlay Frameworkをカスタマイズし、application層・domain層・infra層を用意しDDDライクな設計にしています。
DBライブラリはScalikeJDBCを使っていて、infra層にあるDBアクセスするコードを、テーブル構造からリバースエンジニアリングして自動生成しています。
sbt "scalikejdbcGen menu"
リクエスト・レスポンスのJSON変換はCirceを使い、クラス定義しておいて簡単にJSONのエンコード・デコードができるようにしています。
case class MenuInput(
name: String,
description: Option[String] = None,
materials: List[MaterialChildInput],
caution: Option[String] = None,
category: MenuCategory
)
object MenuInput {
import me.sharedine.application.json.DateTimeDerive._
implicit val encoder: Encoder[MenuInput] = deriveEncoder
implicit val decoder: Decoder[MenuInput] = deriveDecoder
}
メニュー作成時は、名前と単位が同じ材料があったら材料テーブルの材料を再利用し、重複した材料を登録してデータが肥大化しすぎないように工夫しました(例えば、「牛肉」「100」「g」と入力し、「牛肉」「g」の組み合わせのレコードがあればそのidを中間テーブルで利用する)。
// materialsにシェフと材料と単位の同じ組み合わせがあれば再利用する
menu.materials.foreach { requestMaterial =>
Materials.findBy(
sqls
.eq(ma_c.chefId, chefId)
.and
.eq(ma_c.name, requestMaterial.name)
.and
.eq(ma_c.unitLabel, requestMaterial.unitLabel)
) match {
// 材料一覧に同じ材料があった場合
case Some(materialRecord) =>
// menu_materials作成
MenuMaterials
.create(
menuId = createdMenu.id,
materialId = materialRecord.id,
unitCount = requestMaterial.unitCount,
displayOrder = displayOrder,
createdAt = DateTime.now,
updatedAt = DateTime.now
)
displayOrder += 1
// 同じ材料がなかった場合
case None =>
val createdMaterial = Materials.create(
chefId = chefId,
name = requestMaterial.name,
unitLabel = requestMaterial.unitLabel,
createdAt = DateTime.now,
updatedAt = DateTime.now
)
MenuMaterials
.create(
menuId = createdMenu.id,
materialId = createdMaterial.id,
unitCount = requestMaterial.unitCount,
displayOrder = displayOrder,
createdAt = DateTime.now,
updatedAt = DateTime.now
)
displayOrder += 1
}
}
フロントエンド
チャット画面ではこのようにモーダルでメニューの提案を呼び出すことができます。
工夫した点の1つがモーダル移動です。モーダル内で次へ進むボタンを押したり、戻るボタンを押せるようにコンポーネントを設計しました。
親コンポーネントでModalTypeを用意しておき、useStateで状態を管理します。遷移する各モーダル画面は子コンポーネントとして1つ1つコンポーネント化しておき、stateを更新するための関数setModalTypeをpropsで親子間で受け渡しできるようにします。各モーダル画面でボタンを次へや戻るボタンを押したときにsetModalTypeにModalTypeをセットし、再レンダリングをする仕組みです。
interface Props {
chatId: number
onClose: () => void
}
export type ModalType =
| 'SelectMenu'
| 'MenuList'
| 'MenuBookList'
| 'MenuBookInput'
| 'MenuBookConfirm'
export const SelectMenuModal: React.ComponentType<Props> = props => {
const { chatId, onClose } = props
const [modalType, setModalType] = useState<ModalType>('SelectMenu')
const menuBookForm = useForm()
return (
<section className={style.contentWrapper}>
<Fab
icon="close"
onClick={onClose}
height="40px"
maxWidth="40px"
smHeight="28px"
smMaxWidth="28px"
className={style.closeButton}
/>
{modalType === 'SelectMenu' && (
<div className={style.selectMenuWrapper}>
<h2>
<button
type="button"
onClick={() => setModalType('MenuBookList')}
className={style.selectButton}>
メニューブックから選択する
</button>
</h2>
<h2>
<button
type="button"
onClick={() => setModalType('MenuList')}
className={style.selectButton}>
メニューから選択する
</button>
</h2>
</div>
)}
{modalType === 'MenuList' && (
<MenuModal chatId={chatId} setModalType={setModalType} onClose={onClose} />
)}
<FormProvider {...menuBookForm}>
{modalType === 'MenuBookList' && <MenuBookModal setModalType={setModalType} />}
{modalType === 'MenuBookInput' && <MenuBookInputModal setModalType={setModalType} />}
{modalType === 'MenuBookConfirm' && (
<MenuBookConfirmModal chatId={chatId} setModalType={setModalType} onClose={onClose} />
)}
</FormProvider>
</section>
)
}
反省と改善点
初回リリースにしてはやや機能を詰め込み過ぎた感があり、複雑で使いにくくなってしまいました。定型文を用意しておき、それを編集して提案するようなもっとシンプルなものからはじめて使われ始めて要望が増えてきたら機能追加していくくらいでもよかったかもしれません。
ユーザーサービス画面、管理画面3つと見るべき領域が広いのですが、CEOや事業部長が掛け持ちでやっているような状況で、要件や仕様を十分に練ることができていませんでした。そのため要件定義者も開発メンバーもいきなりあれもこれもとやりたいことを詰め込みすぎる傾向があったので、現在は要件を小さくしようとコミュニケーションに気をつけるようにしています。MVPのようにまずは小さく最低限の機能をリリースすることを目指し、最初のリリースまでは要件を足さないよう心がけています。
ほかにデザインドックを導入するなど、要件をスムーズに練ることができるように、開発側とビズ側の認識齟齬がでないように仕組みを入れていければいいなと思います。
まとめ
いかがでしたでしょうか?
今回は実際の事例をもとにシェアダインでどのように開発を行っているかをご紹介しました。
現在シェアダインは開発メンバーを大募集中です。スタートアップの例に漏れず、開発もまだまだ整っていないことが多く、メンバーも日々試行錯誤しながら改善をし続けています。
成長期なので工夫した仕組みや体制をつくったり、貪欲にあたらしい技術をためしてみたい人にはとても面白い会社です。
もしシェアダインのソフトウェア開発に興味をお持ちであれば、ぜひご応募していただいてお話できればと思います。