TDDで開発すべき理由 Pourquoi il faudrait toujours développer en TDD

こんにちは、フロントエンドエンジニアのCouthouis Paulです。メンバーの皆からはクーさんと呼ばれています。

今回、TDDについてお話ししてみたいと思います。私はフランス出身なので、母国語のフランス語と日本語の両方で書いています。 挿絵イラストは同じチームの山本さんが描いてくれました! よければ、ぜひご覧下さい。

introduction

一つに統制された開発手法によって仕様が実現されることが理想的ですが、現実的には開発手法は開発者と同じくらい多くあります。 そんな状況で、過去の開発手法によって仕様の変更に対応することが難しくなる、という問題に直面します。

そしてその変更自体がさらに次の変更を困難にしてしまい、保守がどんどん困難になるコードが引き継がれるという悪循環が発生します。 これは開発者にとってよく知られた問題です。

Dans un monde idéal, il n'y aurait qu'une seule façon de développer une spécification. Mais dans le monde réel, il existe en réalité autant de façon de développer qu'il existe de développeur.

C'est déjà un premier problème en soi car si la spécification évolue, son évolution peut être difficile selon sa facon d'être développé dans le passé. D'ailleurs, l'évolution elle même peut être fait avec une infinité de méthode ce qui rendra la prochaine évolution encore plus laborieuse et ainsi de suite.

C'est un problème bien connu des développeurs, le cercle vicieux d'héritage de code de plus en plus difficile à maintenir.

Comment éviter cela ? Pour commencer, observons la façon de faire "naturel" du développeur. Si vous examinez de près son code, vous constaterez souvent que l'application développée ne ressemble pas à la spécification requise à l'origine.

De manière instinctive, le développeur lit la spécification et la développe. S'il oublie quelque chose, il la relit, mais peu à peu, il divague et construit le code comme un puzzle, ce qui peut entraîner soit des erreurs, soit du code inutile.

Au finale, que fait vraiment le développeur ? Sans même s'en rendre compte, il découpe la spécification en petites étapes. Une fois qu'une baby step est terminée, il commence une autre baby step.

Maintenant, imaginez un moyen de vous assurer que chacune de ces baby step est couverte par un test. Mieux encore, vous pouvez également vérifier si les précédentes baby step fonctionnent toujours. Cette méthode est appelée TDD.

TDD : Test Driven Development

開発手法としてのTDDで特徴的に感じるのは「テスト」です。 ですからTestが注目され「TDDはテストのための手法である」と言われることがあります。

しかしそれは本質的ではありません。 TDDで一番注目すべき文字は、Test ではなく Drive なのです!

TDDにおいて、テストは開発にありがちな無理や徒労、脱線を防止してくれる、あなたのためのガイドなのです。

原理は「ベイビーステップ(小さなステップ)」です。

  • テストを書く
  • 最初はテストに失敗するコードを書く
  • できるだけシンプルにコードを修正し、テストが成功するように修正します(この時点でコードが美しい必要はありません)
  • テストを通過したら、充分に時間をかけて、コードを美しく、読みやすく、リファクタリングします。

これで次のステップに進むことができ、TDDの輪をさらに広げることができます。

Vous verrez qu'en procédant ainsi, vous aurez toujours une couverture de test à 100% (et si ce n'est pas le cas, c'est que vous vous êtes raté quelque part). Ce n'est pas le seul avantage : TDD vous empêche aussi d'anticiper ou imaginer le développement général et de briser les principes YAGNI.

Avec TDD, vous vous concentrez uniquement sur votre baby step actuelle. Les prochaines étapes arriveront au moment adéquat alors n'anticipez pas ! En d'autres termes, TDD est votre guide, voir votre conducteur. C'est comme être assisté par un GPS automobile qui vous dit si vous allez dans la bonne ou mauvaise direction. C'est ça la vrai force de TDD.

これを実践することで、常にテストカバレッジが100%になります。 (そうでなければ、何かがおかしい)

メリットは他にもあります。

開発者が全体を慮り、予想・想像しながら開発し、結果的に迷走や蛇行運転となり、徒労となっている状態はよくあります(YANGI の原則から逸脱する)。

TDDは、現在の「ベイビーステップ」にのみ焦点を当てます。 いま行おうとしている次のステップは自ずと明らかになっていて、あらかじめ思考しておく必要もありません。

つまり、TDDはあなたのガイドであり、ドライバーでさえあるのです。 まるで、車のGPSが正しい方向か間違った方向かを教えてくれるように、アシストしてくれるのです。

それがTDDの醍醐味です。

TDD:Une discipline exigeante

しかしTDDを実践するには、厄介な問題があります。 TDDを成功させるためには、今までの習慣を捨て、いくつかの必須ルールを守る必要があるのです。

  • テストを合格するためだけに、コードを追加する
  • テストに合格するため以外のコードを書いてはならない。「必要になるかも」等の予想は無用です。
  • テストを失敗したままにしてはいけない。

また、テスト対象のレイヤーが他のレイヤーに依存している時に、記述することが難しくなります。 依存性の注入(DI)と Clean Architecture は、TDDに取り組むために知っておくべき重要な概念です。

お勧めは、TDDの鍛錬に役立つ様々な「KATA:martial_arts_uniform:」がインターネット上にありますから、参照し練習することです。

Développer en TDD dans une base de code existante

また、既存のコードをベースにしてTDDを始めることに難しさを感じるでしょう。私が Baseconnect に入社したとき、まさにそんな状況でした。

ゼロから立ち上げるプロジェクトにTDDで取り組むことが理想的ですが、実際の現場にそんな機会はなかなか恵まれません。対峙しなければならないコードには、独自のテストが実装されていたり、テストが無かったりするのです。

私はTDD無しではやっていけないことに気づき、解決策を模索し続け、ようやくその方法に辿り着きました。 いわば、私が編み出した「KATA」です。

その方法とは最小限のコードを既存のコードに注入することです。

  • まず、新しいスタンドアロンモジュールにTDDを使用して新しいコードを実装します。
  • 完了したら、依存性注入の原則を使用して、このモジュールを既存のコードに注入します。

実際にやってみましょう!Voyons ça en pratique.

TDDで書かれたコードはすべて「new_src」フォルダに、古いコードは「src」フォルダに収められています。

まずディレクトリ構成です。(Frontendの例で説明します) 古いコードは「src」フォルダに収められており、TDDで書かれたコードはすべて「new_src」フォルダに収められるものとします。 最終的に「src」のコードは「new_src」に移動することを目指します。

> new_src
> node_modules
> src

「new_src」フォルダは、独立したアプリケーションとして機能します。 Reactコンポーネントがあります。

export const Header: React.FC = () => {
  return (
    <x.div data-testid="header" display="flex" justifyContent="space-between">
      <div>
        <Responsibles />
        <TagManager />
      </div>
      <div>
        <StatusManager />
      </div>
    </x.div>
  )
}

また、Redux Toolkitのスライスもあります。

import { createSlice } from '@reduxjs/toolkit'
import { CurrentUserEntity } from './entities'
import { retrieveCurrentUser } from './use-cases'

const initialCurrentUser: CurrentUserEntity = {
  uuid: '',
  authorityName: null,
  branchOfficesStatusOptions: [],
  statusOptions: [],
}

export const currentUserSlice = createSlice({
  name: 'currentUser',
  initialState: initialCurrentUser,
  reducers: {},
  extraReducers: builder => {
    builder.addCase(
      retrieveCurrentUser.fulfilled,
      (_, { payload: { currentUser } }) => ({
        ...currentUser,
      })
    )
  },
})

もちろんテスト用ファイルも。

it('should retrieve status options', async () => {
    const {
      dispatchRetrieveCurrentUser,
      selectStatusOptions,
    } = retrieveCurrentUserSUT().withCurrentUser(fakeAdmin).build()

    await dispatchRetrieveCurrentUser()

    expect(selectStatusOptions()).toEqual<CurrentUserEntity['statusOptions']>([
      {
        label: 'label1',
        value: 'value1',
      },
      {
        label: 'label2',
        value: 'value2',
      },
    ])
  })

最後に、モジュール全体を構築するメインファイルを用意します。 実際のアプリケーションとの唯一の違いは、注入に使用するキーワード 'export' の存在です。

import { Provider as ReduxProvider } from 'react-redux'
import { storeQueries } from './api/queries'
import { AppShell } from './app/AppShell'
import { createStore } from './core/store'

const store = createStore(storeQueries)

export const MainShell: React.FC = ({ children }) => {
  return (
    <ReduxProvider store={store}>
      <AppShell>{children}</AppShell>
    </ReduxProvider>
  )
}

目標は「new_src」のファイルを「src」のファイルに注入できるようにすることです。 そのため、MainShellは "src "のAppShellコンポーネントにインジェクションされます。

import { ThemeProvider } from '@xstyled/styled-components'
import { QueryClientProvider } from 'react-query'
import { NotificationProvider } from '~/components/Notification'
import { queryClient } from '~/queries/queryClient'
import { theme } from '~/style/theme'
import { MainShell } from 'new_src/main'

export const AppShell: React.FC = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={theme}>
        <NotificationProvider>
          <MainShell>{children}</MainShell>
        </NotificationProvider>
      </ThemeProvider>
    </QueryClientProvider>
  )
}

これにより「src」フォルダ内のファイルであっても、すべてのアクションとreduxセレクタにアクセスすることができるようになります。 ただし、既存の「src」テンプレートにReactコンポーネントを注入する必要があることに変わりはありません。

import { Header } from 'new_src/app/design/Header/Header'
~
~
~
 <FormProvider {...methods}>
      <Box backgroundColor={'mainBg'} marginTop="13.5rem" px="1rem">
        <input name="currentFocus" ref={register} style={{ display: 'none' }} />
        <Header />
        ~
        ~

こうすることで、コードのどの部分が100%テストでカバーされているかが分かります。 また、コードをリファクタリングして「src」から「new_src」にコードを移せば、いつか100%TDDのコードを手に入れられるでしょう。

Conclusion

開発者の誰もが、膨大で理解し難い、またメンテナンスが困難な開発を経験しています。

でも、最初から最後まですべきことをシンプルに保ち、メンテナンスの問題も無く、アプリケーション開発を成功させる解決策があるのです。それがTDDです。

最初、TDDはとっつきにくい開発方法です。 しかし、一度マスターすれば、あなたとそのチームを幸せにすることができます。

納得できない?ぜひ一度試してみてください!!最後まで読んでいただきありがとうございました!

herp.careers

React18のAutomatic Batchingを試してみてわかったこと

こんにちは!フロントエンドエンジニアの川瀬です。

少し前に、SuspenseやTransitionなど楽しみにしていたReact18がリリースされたのですが、 自動バッチングというのも新要素としてあったのでどういったものかなと試してみました。

自動バッチング https://ja.reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching

バッチングとは React がパフォーマンスのために複数のステート更新をグループ化して、単一の再レンダーにまとめることを指します

試してみたreact18とreact17のソース

// App.tsx 18も17も同じ
import React from "react"

function App() {
  const [count, setCount] = React.useState(0)
  const [flag, setFlag] = React.useState(false)
  const render = React.useRef(0)  //レンダー回数を数えます
  
  render.current++

  const update = () => {
    setCount(c=>c+1)
    setFlag(f=>!f)
  }
  
  React.useEffect(()=> {
    const id = setTimeout(()=>{
      setCount(c=>c+1)
      setFlag(f=>!f)
    } ,1000)
    return ()=> clearTimeout(id)
  },[])
  

  return (
    <div style={{
      display: 'flex',
      height: '100vh',
      justifyContent: 'center',
      alignItems: 'center',
      background: '#777',
      color: '#fff'
    }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10}}>
        <div>Version: (18 or 17)</div>
        <div>render:{render.current}</div>
        <div>count:{count}</div>
        <div>flag:{flag? 'true': 'false'}</div>
        <button onClick={update}>update</button>
      </div>
    </div>
  )
}

export default App;
// index.tsx
import React from 'react';
import App from './App';
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
// 18はcreateRootを使います
import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
// StrictModeにすると開発環境ではrenderが2回走ってしまうのでやらない
root.render(
  // <React.StrictMode>
    <App />
  // </React.StrictMode>
);
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
//17
import { render } from 'react-dom';

// StrictModeにすると開発環境ではrenderが2回走ってしまうのでやらない
render(
  // <React.StrictMode>
    <App />
  // </React.StrictMode>
,document.getElementById('root') as HTMLElement);

動き確認

ブラウザ表示したばかりでは、どちらもレンダー1回目、countやflagの更新はなしです。

useEffectで、setTime内の処理が1秒後に実行され、countとflagが更新されます。 この時、React18ではrender回数は1回更新されのに対し、React17では2回更新されました。 中でstateを更新している分だけrenderが走ってしまいます。

続けてUpdateボタンで更新してみます。

ボタンで更新した場合はどちらも1回のrenderで済みました。

※公式より抜粋

自動バッチング以前は、React のイベントハンドラ内での更新のみバッチ処理されていました。promise や setTimeout、ネイティブのイベントハンドラやその他あらゆるイベント内で起きる更新はデフォルトではバッチ処理されていませんでした。

参考:https://github.com/reactwg/react-18/discussions/21 より

// promise
fetch(/*...*/).then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
})

// つまりasync awaitでも起こる
const update = async () => {
  await console.log('promise!')
  setCount(c=>c+1)
  setFlag(f=>!f)
}
// ネイティブイベント
elm.addEventListener('click', () => {
  setCount(c => c + 1);
  setFlag(f => !f);
});

非同期とかaddEventListenerとかよく使ってる、気をつけねば・・

つまり?

React17の場合、上記条件でstateが複数更新される場合、その分renderが走ってしまいます。 onClickなどのReactのイベントハンドラであれば問題なし☆

React18からは、予期せぬrenderが走らない => パフォーマンスがよくなっている、ということですね。

React17でどうしてもsetTimeoutやpromiseの中でstateを更新したい場合は、stateをまとめてみるしか?

const [state, setState] = React.useState({count:0, flag:false})

React.useEffect(()=>{
  setTimeout(()=> setState(v=>{ count: v.count+1, flag: !v.flag}),1000)
},[])

また、色々と試していきたいと思います! 最後まで読んでいただきありがとうございました。

herp.careers

Rubyの「=」について無限に紐解いてみる

はじめまして、Baseconnectエンジニアの御園と申します。 私は2022年の4月1日にBaseconnectにエンジニアとしてJOIN、実務として経験するのは初めてのRubyを使って開発業務を行うことになりました。

そして、入社してから1ヶ月にも満たない4月22日、無茶振りによって 社内向けにLTをすることになり、私はプログラミング言語オタクとしての本領を発揮すべく、Rubyにまつわる些細な疑問を深堀りしたLTを行いました。

結果として割と反応はよく、社内の皆さんに良い感想を沢山頂けたので、今回はそのLTの中から抜粋し、テックブログとして記事を書かせてもらうことになりました。どうぞ、よろしくお願いいたします。

introduction

さて、皆さんがRubyを学ぶ時一番最初に学んだ事を覚えていますか? Rubyでプログラムを書く準備、Rubyという言語について……様々な事を学んだでしょう。 そして、Rubyイカしたプログラムを書く時、誰もが最初に学ぶものがあります。 そう、それは変数に値を格納するための「=」ですね。

プログラマが一生付き合うことになっていく「=」。それを記述した数はもはや数え切れないでしょう。 その「=」がもつ働きは、実はRubyという言語の性質を追求するのに実はもってこいという事を知っていましたか? 一回立ち止まって、あるいは少し後ろを向いてみて、「=」にもう一度目を向けてみましょう。 さぁ、私と一緒にRubyの「=」について追求する旅に、一緒に出かけてみましょう。

motivation

一番最初の些細な疑問は、私がRubyでとある演算子を使おうと思ったときに生まれました。 以下のようなコードです。

value++

++ はインクリメント演算子と呼ばれ、JavaScriptPHPなど、Cに影響を受けた色々な言語で使うことが出来ます。 意味は単純で、"変数の値を1加算する"と言ったものです。よく for と一緒に使用されたりしますね。例えばC++のコードでは以下の様に使えます。

for (int i = 0; i < 10; i++) {
  // iは0〜9まで、1ずつ加算されながら、このブロックの中の処理は反復される
}

さて、Rubyの世界に戻りましょう。Rubyに堪能な読者の人ならば、既に「そんなもの、Rubyには無いよ」と鼻息を荒くしていることでしょう。 まさしくその通りです。Rubyには ++ が定義されていません。なので、私は残酷にもRubyにエラーを吐かれてしまいました。 ( ++を使った直後の行を対象に以下のエラーを出される )。

syntax error 

Rubyにはインクリメント演算子も、"変数の値を1減算する"ところのデクリメント演算子 -- もありません。 例えば1加算する、といった事を実現するためには、以下の様にする必要があります。

value = value + 1

ただし殆どの人がこう書かず、必ずこう書くでしょう。

value += 1

自己代入などと表現されるこの += は、 value = value + 1 と同じ意味を持っています。 これはある意味シンプルです。 変数の値を加算、あるいは減算するという事に対しては、全て同じ演算子を使えば良いということになります。 1を足す時も、何かそれ以外を足す時も+=。1引く時も、何かそれ以外を引く時も -=。 わざわざ ++-- という特別な演算子の事を学習する必要がありません。 しかし、JavaScriptPHPなどに見られるように、”あっても良さそう”という気持ちが生まれるのも確かです。

何故Rubyにはインクリメント、デクリメント演算子が無いのでしょう? プログラマは探求をするのが好きです。私もご多分に漏れずその性を持っていますから、こんなちょっとした興味で、Rubyにこれら2つの演算子が無いことを調べ始めました。 そして、すぐさま、その疑問が解消される事については、Rubyという言語そのものの性質を知らなければならないことに気づくのでした。

奇妙なコード

Rubyを学習しようと文献を漁ると、高確率で目にしたり、聞いたりするものがあります。

Rubyでは全てがオブジェクト

これはどういう事なのでしょうか? この事を知るために、まずは次のようなシンプルなコードを実行してみます。

class Hoge
end

i = Hoge.new
puts i.object_id

object_id メソッドは、そのオブジェクトのIDを取得する事が出来ます。 Hoge クラスは new メソッドによってオブジェクトとして実体化され、 i に格納されました。 そして、その i に対して object_idメソッドを使用することによって、実体化したオブジェクトのオブジェクトIDが取得出来るというわけです。 例えば私の環境では、以下のように出力されました。

60

では、もう一つ、次のようなコードではどの様に出力されるでしょうか?

class Hoge
end

puts Hoge.new.object_id
puts Hoge.new.object_id
puts Hoge.new.object_id

このコードは、例えば私の環境では以下のように出力されます。

60
80
100

おそらくどのような人の環境でも、3つとも違うオブジェクトIDが出力されるでしょう。 通常、Rubyではオブジェクトが作成された時、他のオブジェクトと衝突しないようにユニークなオブジェクトIDが割り振られます。 上記のコードでは、3回 Hoge クラスのオブジェクトの作成をしており、それぞれユニークなオブジェクトIDが割り振られた結果、3回とも違うオブジェクトIDが出力されたという事になっています。

今、私達は object_id でそのオブジェクトが持つオブジェクトIDを知る事が出来る事を理解しました。 では、次のようなコードはどうでしょうか?

puts 20.object_id

一見これはとても奇妙です。 もし、これが奇妙に見えない人がいたら、ニヤニヤしながらこの先に進んでください。 しかし、JavaPHP、他のオブジェクト指向の特性を持つプログラミング言語をやったことのある人であればあるほど、このコードがとても奇妙であるということは共感できるはずです。 そして、Rubyではこのコードは問題なく解釈でき、例えば次のような出力を生むはずです。

41

なんと 20 という数字に対して、 object_id メソッドが使えてしまいました。 Rubyでは全てがオブジェクト という言葉がここで脳裏を反芻します。 もう一度先のコードを見てみましょう。

puts 20.object_id

このコードは何が奇妙なのでしょうか? 先程 object_id メソッドは、オブジェクトのオブジェクトIDを取得できるものという説明をしました。 その「オブジェクト」とは、潜在的に「new メソッドを使ってクラスを実体化したもの」という印象があったはずです。ですが、このコードでは new メソッドはおろか、クラスの定義すら出てきません。

プログラマ、特にRubyをやったことのないプログラマ達にとっては、20という数字は「ただの20という数字」でしかありません。それ以上でもそれ以下でも無く、20という数学的性質を表す分量以外の何者でも無いのです。 しかし、Rubyでは、それに対して直接 object_id を使うことが出来てしまいました。 一体何が起きているのでしょうか?

全てがオブジェクトということ

Rubyのリファレンスマニュアルを参照すると、オブジェクト の欄に以下のような記述が出てきます。

オブジェクトとは Ruby で扱える全ての値はオブジェクトです

これはとても興味深いことです。 この事をあえて、他の言語で擬似的に表現することにしてみましょう。 例えば、

hoge = 42

このようなRubyのコードがあったとして、C++では通常このコードと同じような事をするならば

int hoge = 42;

となるはずです。しかし、Rubyでは 扱える全ての値はオブジェクト ということでした。 つまり、実際にRubyで起こっていること を忠実に再現するならば、C++では以下のようなコードになると言えます。

auto hoge = new Integer(42);

42という数字を代入するために、Integer クラスのオブジェクトを42という数字で初期化し、それを変数に代入しています。 これが意味するのは、プログラム中に記述された

= 42

というものが、C++Rubyで決定的に違うということです。 この事を踏まえると、先程起きた奇妙な出来事について、紐解いていけそうです。 もう一度コードを見てみましょう。

puts 20.object_id

直感的に言えば、これはC++ではこのようなコードになりそうです。

std::cout << 20.object_id();

しかし、このコードは大方の予想通り通りません。20 という数字そのものに対して、 object_id などというメソッドは無いからです。 20はあくまで20という数字リテラルでしかなく、それ以上でもそれ以下でも無いのです。 ですが、 実際にRubyで起っていること をまたここでも忠実に再現してみましょう。

std::cout << (new Integer(20))->object_id();

これなら通りそうです。とどのつまり、Integer クラスのオブジェクトを20で初期化したあと、 object_id というメソッドをそのオブジェクトに対して呼び出し処理をしています。

Rubyでの 全てがオブジェクト という事がここまでで具体的に見えてきました。 実際にRubyのリファレンスマニュアルを見ると、Integer という整数クラスの存在を確認する事が出来ます。 その継承を追ってみると

Integer < Numeric < Comparable < Object < Kernel < Basic Object

となっており、これのうち Object クラスに object_id メソッドが定義されていることが確認できます。 Rubyでは、ただの数字に見えても、実際には Integer クラスのオブジェクトであり、それに付随して実装されているメソッドをダイレクトに呼び出せたという訳です。

これはRubyの大事な性質です。何気なくタイピングしている数字、文字列、それらはRubyの中では全てオブジェクトとして顕在化するということに留意しなければなりません。 しかし、この性質を意識していると、奇妙なコードも徐々に"Rubyでは当たり前"という見方に変わっていくでしょう。

もっと踏み込んでいく 〜Integerクラスの工夫〜

ところで、ここで一つ疑問が生まれます。 全てがオブジェクトなのであれば、

・数値を宣言する毎にオブジェクトが生み出されている ・同じ数字を宣言したとしてもオブジェクトが生み出され、パフォーマンスが落ちるのではないか?

という疑問です。先程、Rubyでの次のようなコードを

hoge = 42

C++で擬似的に表すのに次のようなコードを用いました。

auto hoge = new Integer(42);

このコードのままであれば、 同じ数字を宣言したとしてもオブジェクトが生み出され、パフォーマンスが落ちるのではないか? という疑問に対してはYESと答えるしかありません。 例えば、

hoge = 42
huga = 42
hugi = 42

というのはC++での疑似コードでは

auto hoge = new Integer(42);
auto huga = new Integer(42);
auto hugi = new Integer(42);

と表せるはずですから。

ですが、面白いことに実際にはRubyでは工夫されています。 まず、次のようなコードを見てみましょう。

puts 20.object_id
puts 20.object_id
puts 20.object_id

例えばこのコードは、私の環境では以下のように出力されます。

41
41
41

おや?これは少し興味深そうです。冒頭で私は

通常、Rubyではオブジェクトが作成された時、他のオブジェクトと衝突しないようにユニークなオブジェクトIDが割り振られます。

この様に説明しました。とすると、20というオブジェクトは、一回しか作成されておらず、そして同じオブジェクトIDが出ていることから、あたかもそれを使いまわしているかのような挙動をしています。 そのため、このコードのC++での擬似コード

std::cout << (new Integer(20))->object_id();
std::cout << (new Integer(20))->object_id();
std::cout << (new Integer(20))->object_id();

これは真に正解ではなさそうです。

これの種はとてもシンプルです。 Rubyでは全てのIntegerオブジェクトは、シングルトンパターンによってオブジェクト化されます。 つまり、1度登場した数字オブジェクトは、以降同じオブジェクトを使い回すということになります。 次のようなコードで確認してみましょう。

v = 20
puts v.object_id
puts 20.object_id

例えば私の環境では以下の様に出力されます。

41
41

変数 vに代入されている20という数字オブジェクトの object_id と、 そもそもの20という数字の数字オブジェクトの object_id が一致しました。

20というオブジェクトは、一回しか作成されておらず、そして同じオブジェクトIDが出ていることから、あたかもそれを使いまわしているかのような挙動

RubyのIntegerは、シングルトンによって工夫したデザインをする事によって、この挙動を実現しています。 つまり、同じ数字を宣言したとしてもオブジェクトが生み出され、パフォーマンスが落ちるのではないか?に対してはNoと返答する事が出来そうです。 私達は、知らずIntegerの工夫によって、こういった細かいパフォーマンスを気にすること無く、コーディングが出来ているというわけです。

「=」が持つ 本当の意味

さて、ここまで読み進めた方は、一つの違和感を覚えるでしょう。 この記事のタイトルは「=」について紐解いてみるというタイトルでした。 しかし、実際言及したのはRubyという言語が持つ、全てがオブジェクトという性質についてでした。 冒頭で私は、

「=」がもつ働きは、実はRubyという言語の性質を追求するのに実はもってこいという事

などと述べました。ここまでの言及で実は先に、Rubyという言語の性質については追求できましたが、肝心の「=」がもつ働きについては追求できていません。 いやいや、と思うこともあるでしょう。 変数に値を格納 という表現を、同じく冒頭で私はしています。そしてあるいは多くの人が同じような認識を持っているかもしれません。 「変数に値を格納する事」こそが「=」の働きである。これは本当なのでしょうか?

まず、次のようなコードを見てみます。

a = "hoge"
b = a 

puts a
puts b

このコードの出力は自明です。どちらの putshoge を出力することでしょう。 しかし問題はもっと深い所にあります。 この時、 ab が参照している hoge という文字列は果たしてどうなっているのでしょうか? 「格納」という表現が合っているのならば、a に 「hogeを格納」した後、 b に 「a の内容を(コピーして)格納 」といった表現が出来るはずです。 これは直感的には ab にはそれぞれ別々に値が入っているイメージを作ります。 しかし、実際にはどうでしょう。次のようなコードを試してみます。

a = "hoge"
b = a
puts a.object_id
puts b.object_id
puts "hoge".object_id

このコードの出力は、私の環境では以下のようになります。

60
60
80

まず、文字列 hoge 自体に object_id を行った時、違うidを参照していることから、文字列はIntegerとは違い、同じ文字列でも新たな文字列オブジェクトが生成され、別のユニークなobject_idが割り当てられることがわかります。

では、 abはどうでしょうか?こちらは同じobject_idを指しています。 つまり、abが指している文字列オブジェクトは同じものであるという事なのです。 その事実を確かめてみましょう。

a = "hoge"
b = a
a.upcase!
puts a
puts b

upcase! は、格納されている文字列を大文字に変換するメソッドです。そして、このコードの出力は

HOGE
HOGE

となります。 b には何ら作用していないように見えるのに、 bの出力も大文字になってしまいました。 これは、

abが指している文字列オブジェクトは同じものであるという事

という事実を裏付けているものに他なりません。 そして、これは「格納」という表現が期待するものとは違うように思えてきます。 とすると

b = a

というコードが持つ意味は

  • aにbの内容をコピーして格納する

のではなく、

  • bはaと同じオブジェクトIDを持つようにする

という事に他ならないのです。 もっと、スッキリする言い方にするのであれば、

a = "hoge"

というのは、 - aという変数は、生成された文字列オブジェクト "hoge" を参照する

となり、

b = a

というのは - bという変数は、aの参照しているオブジェクトを参照する(=aと同じオブジェクトを参照する)

という言い方が出来る事になります。 つまり、「=」が持つ本当の働きは、右辺のオブジェクトの参照先を、左辺に”束縛”する という事なのです。 「=」は、変数にオブジェクトを格納するものではなく、ただ束縛する、結びつけるという働きを持っているのです。 これがRubyにおける「=」の本当の意味なのです。

インクリメント演算子が無いわけ

「=」の紐解きが完了しました。 今こそ、motivationで生まれた疑問を解消するときです。 まず、自己代入 += は何故許されているのか。 これはとても単純な問題です。

value += 1

これは、次のコードのシンタックスシュガーでした。

value = value + 1

このコードが意味することは、value が参照している数字オブジェクトに対して、「1を足した数字オブジェクト」を新たに生成し、それを新たに左辺の value に束縛している。 という事になり、これはRubyの性質から見ても筋が通っています。

value = 1
puts value.object_id
value += 1
puts 2.object_id
puts value.object_id

このコードは、私の環境では以下のようになります。

3
5
5

1という数字オブジェクトを保持していた valuevalue+1 によって新たに生成された2という数字オブジェクトを value = によって新たに参照するようになった。 この事を考えると、 2.object_id と2回目の value.object_id が同じ値を出力するのは、当然のことと言えます。

さぁ、かたや ++ はどうでしょう。 この ++ は "変数の値を1加算する" という意味でした。 では、次のようなコードを考えてみます

a = 1
b = a
a++

もしこれが許されればどうでしょうか? まず、aに数字オブジェクト1が束縛されます。そして、 b も、 a と同じオブジェクトを参照します。 その後、++ によって、 a が参照している数値に対して1を加算しようとします。この操作は破壊的です。 そうすると、どういった事が起きるでしょうか。

a = 1
b = a
a++
puts a
puts b

このputsは両方とも2を返してしまうわけです。もっと言うなれば、 1.object_id を束縛していたものは、全て「2」になってしまうわけです。1が2になる世界。意味が分からなくなってきました。

数値はプログラミングにとって欠かせません。このような意味が分からないことが起きないように、Rubyでは数値オブジェクトには、Immutableという「破壊的な変更」を出来ないようなルールが課せられています。

そして、このルールを破らないように、Rubyには ++ が無いというわけなのです。 同じ様に、"変数の値を1減算する" デクリメント演算子もありません。 これで疑問が解消できました。

outro

長い旅路でした。 私達は、Rubyにおける「=」の持つ意味、そしてその本質であるところのRubyでは全てがオブジェクトである、というものの片鱗を見ました。 これで明日から「=」を書く時も、実際に行われている事を思い浮かべながらコーディングが出来そうです。

いかがだったでしょうか? 生まれた些細な疑問は、時として実に深くプログラミング言語の性質を追求することになる訳ですから、プログラミングは面白いものです。 日常的に生まれた疑問は、是非とも疑問のまま終わらせず、深く追求していきたいですね。

Baseconnectでは、Rubyの「=」のようにデータを結ぶ(Musubu)事に興味があるエンジニアを募集しています。

herp.careers

気持ちをちょっと楽にする工数見積もりの技術

はじめに

こんにちは、Baseconnectでエンジニアをしている米丸です。みなさん、工数見積もりって難しくないですか?しんどくないですか?嫌いじゃないですか?

私も以前「見積もり、やだなーこわいなー」と強く思っていた時期があったのですが、工数見積もりが持つ特徴について学ぶことで、ちょっと気持ちが楽になった経験があります。同じように苦手意識を持っている人向けに、「これを知っているだけでも見積もりがしやすくなる」という、Tips的な話をしたいと思います。

なお、この内容はBaseconnect社内で行っているエンジニア共有会(通称:Developer Closer)でのLTをもとに作成したブログになります。社内エンジニアから役に立ったよーという声をいただけたので、ブログとしてまとめてみました。

工数見積もりはなぜしんどいか?

本題に入る前に、工数見積もりはなぜしんどいかを考えてみます。工数見積もりがむずかしい理由はいくつかありますが、エンジニアが工数見積もりをしんどいと感じる理由としては、「早くしなきゃと遅れちゃいけないの心理的ストレッチ」があると考えます。

早くしなきゃと遅れちゃいけないの心理的ストレッチ

一般論としてですが、ソフトウェア開発において機能のリリースは早ければ早いほど価値につながるため、エンジニアは管理者から「できるだけ早く開発してね!」とプレッシャーがかけられます。一方で、期限に遅れてしまうとユーザーや関係部署に迷惑がかかってしまう場合があるため「絶対に遅れないでね!」とも釘をさされます。

この「早くしてね」「遅れないでね」というメッセージングがあるため、工数を短めに見積もりたくなる気持ちと、長めに見積もりたくなる気持ちという相反する想いが同時に生じ、心理的ストレッチがかかってしまいます。

くわえて、工数見積もりは非常に難しく、実装経験さえあればできるというものではありません。心理的ストレッチがある中で見積もりを大きく外してしまい、トラウマになっているエンジニアは少なく無いのではないでしょうか?

個人的には、工数見積もりは技術を要するものだという認識をもっと広めたいです。

気持ちをちょっと楽にする工数見積もりの技術

ここから本題です。工数見積もりに苦手意識を感じていた私が、知るだけでもだいぶやりやすくなった見積もりの技術を紹介します。

1.ぶれ幅の特徴を掴む

工数と確率のベータ分布

工数見積りにはぶれ幅があります。

ソフトウェアの開発期間のばらつきを、縦軸に確率、横軸に時間軸を置いたグラフに表すと、このようなベータ分布になるといわれています。うまくいけば左端の短い期間(楽観値)で開発が終わるが、問題が起きれば右端の期間(悲観値)まで開発は長引いてしまう、という開発期間のぶれ幅を表しています。山になっている最頻値は、もっとも着地する確率の高い期間です。

まずは、見積もりには幅があるものなので、神様でも点での見積もりを正確に出すことはできないのだと、安心してください笑。また、自分が出した工数見積もりが、楽観値、最頻値、悲観値のどれにあたるのか意識するだけでも精度は上がると思いますし、受け取り手にも、それが伝わるようにしてみてください。

工数と確率のベータ分布 - 50%ライン

さらに、このグラフが面白いのは、左右対象ではなく後ろに尾をひく形になっていることです。非対称の理由としては、開発期間の短縮には限界があるが、開発を遅らせる問題には制限がないためと言われています。

グラフに赤線でひいていますが、50%の確率でここまでには開発が終わるという、50%ラインというものが存在します。左右非対称のため、この50%となるラインは、最頻値よりも少し右側にあります。そのため、一見妥当そうに見える最頻値の見積もりを基準にスケジュールを引いてしまうと、確率としては 2回に1回以上、スケジュール遅れになってしまうことになります。必ずしも最頻値の見積もりを出せばよいというわけではないのです。

もう少し言うと、大きなプロジェクトでは、50%ラインをベースにスケジュールを引きつつ、「各タスクの50%ラインと悲観値の差の標準偏差」をバッファとして持つ、といった方法もあったりします。ここでは詳細は言及しませんが、楽観値、最頻値、悲観値から50%ラインを割り出す計算式もあったりします。

2.場合分けする

いつもきれいなベータ分布になるとは限らない

先程のベータ分布のグラフはあくまでモデルであり、実際にはいつもきれいなベータ分布となるとは限りません。 例えば、ライブラリ更新だけで終わると思っていたら、思ったように動かずスクラッチでつくることになった・・・という悲しい経験が個人的にはあるのですが、そのような事例では、うまくいく場合といかない場合とで、開発工数の差は大きく乖離ができ、確率と工数のグラフもフタコブラクダのようになります。このような見積もりを大きく左右する要素が最初から分かっている場合は、それがうまくいく場合とそうでない場合とで、場合分けをして見積もりを立てるのが一つの手だとおもいます。

3.相対的に見積もる

「相対的に見積もる」とは、とあるタスクを基準として2倍/3倍/4倍といった比較値をだしたり、アバウトにS/M/L/XLといったTシャツのサイズでタスクの大きさを表す手法を指します。このとき比較するのは、過去に実施したタスクとの比較、あるいはこれから実装するタスク同士で比較をします。

個人的には、絶対時間の見積もりが必要な場合も、まず相対的な見積りをだして考えるようにしています。なぜ相対的な見積もりを出すかと言うと、まず、絶対時間の見積もりよりも相対的な見積りの方が人間は出しやすいからです。実際に、人は10倍以内のものならうまく見積もれる、という研究結果もあるそうです。あとは、絶対的な見積もりは、実装者のレベルなど諸条件によって変わるところがありますが、タスク間の相対見積もりは比較的影響をうけないので、管理がしやすいことがあげられます。

フィボナッチ数列での見積もり

アジャイルの文脈では、相対見積もりであるストーリーポイントとして、「1, 2, 3, 5, 8, 13, …」というフィボナッチ数列が使われることがあります。ソフトウェア開発は規模が大きくなるにつれて不確実性が増していくため、数が大きくなるにつれて間隔が大きくなっていくフィボナッチ数列は見積もりを表すのに相性がよいと言われています。

個人的な理解では、「うーん、このタスクの見積もりは、10、11、いや11.5かな…」などと細かい数字の違いに気をとられることなく、大きく外さないレベルまでの見積もりを意識せず出せることが、フィボナッチ数列を使うメリットだと理解して、アジャイルな開発ではなくても使える技術だと思っています。

4.理想時間と現実時間を区別する

理想時間と現実時間とは?

  • 理想時間

    • 何者にも邪魔されない、精神と時の部屋のような理想の環境で作業した場合の、開発にかけられる時間
  • 現実時間

    • 予定されていない差し込みのタスクや、切り替えのオーバーヘッドの影響を受けた、実際に開発にかけられる時間

例えば、とあるタスクを3日程度でできそうだと見積もった時に、その3日は8時間フルでタスクにあてた場合の見積もりでしょうか?カレンダーに入っている予定は考慮して差し引いているかもしれませんが、差し込みで入る小タスクや、slackで送られてくる連絡や質問へのリアクション、カフェスペースでの同僚との雑談などにかかる時間は考慮されていないことが多いのかなと思います。特にマルチタスクをしているような場合は、タスクを切り替えるときのスイッチングコストもそれなりに掛かっています。

理想時間と現実時間の活かし方

見積もりを出す時に、理想時間と現実時間を明確に分けずに考えると、失敗しがちです。理想時間での換算なのか、現実時間を考慮したものかを区別する事がまずは大事です。

ただ、現実時間をとらえることは難しいです。計測することも一つの手ですが、細かいタスクの積み重ねやスイッチングコストがどれくらいかかっているかは計測しにくいところです。一つの手として、極力理想時間と現実時間の差分が無くなるように予定を組むのは有効です。ミーティングをまとめたり、複数のタスクを持たないようにしたり、差し込みをシャットアウトする集中時間を設けたり、などです。

5.複数人で見積もる

プランニングポーカーというチームで見積もる技法があります。

  • プランニングポーカーの手順
    • 開発に携わる全員で集まる
    • それぞれで見積もりを考え、数字が書かれたカードを伏せた状態で自分の前に出し、せーので公開する
    • 自分が立てた見積もりの根拠を議論する。ギャップがあればすり合わせる

この方法の良いところは、一人よりも数人で考えた方が正確性が増すことと、開発内容について認識ずれがあった時に見積もり段階ですり合わせができることにあります。

そうはいってもチームで毎回集まるのは難しい場合があります。プランニングポーカーまでは行かなくとも、コードレビューのように見積もりをレビューしてもらう方法は、効果もあり取り入れやすいと思います。

実際にプランニングポーカーをおこなった経験では、人によって出てくる見積もりはおもしろいほど異なり、正解は無いものだと実感することができました。

むすび

工数見積もりと仲良くなるための技術をご紹介しました。どうでしょうか。ちょっとでも工数見積もりへの苦手意識がなくなったら幸いです。

本来、工数見積もりとは不確実な未来に対して道筋を建てるための有効な武器のはずですが、出す側も受け取る側もコミットメント(期限の約束)と捉えてしまうことで、エンジニアを苦しめる足かせになっていることがあります。

正確な見積もりを出すことは不可能だと認めた上で、見積もりの特性を理解し精度を上げ、足かせではなくプランを建てる武器としてうまく使って行きたいものです。

Baseconnectでは不確実性と戦うエンジニアリングマネージャーを絶賛募集しています。興味を持たれた方は、是非カジュアル面談で一度お話しましょう。お気軽にご応募ください。

meety.net

herp.careers

SCRUM FEST Osaka 2022 に参加しました!イベントレポート2

メガネひげ面、のび太型サブマネージャーの、富田と山本が「SCRUM FEST Osaka 2022」に行ってきたよ!

ほう、なんとこなれたオン・オフハイブリッド型カンファレンス! ちょっとまて、これって会場に来る必要あったのか?(伏線)

印象に残ったセッション(山本版)

フルリモート下でのチームビルディング

推進するフルリモートの中で、カルチャーを大事にしながら、ポテンシャルが発揮できる場をどう醸成するのだろうか...最近はずっとそんなこと考えています。

参考 → Scrum Fest Osaka 2022 フルリモート下でのチームビルディング

チームメンバーを巻き込んでロードマップを作成・アップデートする

メンバー全員が現在地を認識し、目的地を共有しながらロードマップを調整する。そのことでオーナーシップが生まれそうだ。

組織作りと個人の成長の両方に役立つワークシップ

相互理解を深めるワークショップ、たとえば「バリューカード」や「スキルマップ」。

実は弊社でも「※オフサイトMTG記事」のアイスブレイクでバリューカードをプレイしてみました。 むちゃくちゃ盛り上がったし、メンバーの意外な一面を発見することができました。

うむ

チームという土台の上に「成果」を積み上げていく... チームビルディングとは、チームで最大の成果を出すための土台作りである

私たちの土台とはなんだろうか?

モブとソロを織り交ぜてハイアウトプットなチーム開発

ソロワーク、モブワーク...組み合わせて、良いとこ取りをしてアウトプット増大! 言うには容易いが、その運用の知恵には「ほうほう、なるほど」

参考 → モブとソロを織り交ぜてハイアウトプットなチーム開発

ソロワークとモブワーク

担当したタスクに単独で集中して成果を出していく「ソロワーク」しかやったことがない私は、モブプロやモブワークに実体験がなく、頭では目的やメリットを理解しつつも、うまくやる知恵が足らない。(やってみればいいんだけどね)

ソロワークとボブワークそれぞれに、向き不向きがあるって事。 そこで両方を織り交ぜたハイブリッドな運用で、フロー効率とリソース効率のバランスを取って行こうぜ、というアイデアに注目したい。

うむ

モブかソロ、どちらで取り組むかを最初に宣言することが大事(ソコがScrumらしさかと感じた)。 そして、モブで取り組んでいることが単純作業などのソロ向きに移行した時に、ソロに移行したことを宣言し、残りのリソースは別のタスクに着手する。ソロが完了したとき、モブに合流するという流れもある。

モブワークとソロワークは、柔軟に使い分ければよい でも、いま取り組んでいることがどっちなのかを共有しないと、デメリットが出てきてしまうだろう

面白いなぁ

実践!勝手に育つチームの作り方

自律したチームとなるには、どこまで権限があるのかを明確にしておく必要がある。 実際に「デリゲーションポーカー」を体験して、認識を合わせ、未来像まで描くことが出来た面白い体験

参考スライド

チームを強くする

必要なスキルに対して、チームメンバーの現状のスキルレベルを明らかにする。そこから、成長すべき方向や採用計画を明らかにする。

デリゲーションポーカー

権限を委譲するレベルを段階に分けて。マネージャーの期待値とメンバーの希望を話し合い、両社で決定する。 そして、将来はどういう状態が望ましいかを共有し、方向性を共有する。

うむ

意思決定のあり方について「責任と権限の委譲」の課題は、なかなか進捗しない、あるあるだ。

「デリゲーションポーカー」ってのを初めて知ったが、ワークショップで良い体験ができたことは、むっちゃ楽しかった。

権限移譲は Baseconnect でもよく話題になる課題。 それを明快に表現できる手法なので、ぜひやってみたい。

本編は懇親会にあり

会場に足を運んでみると、セッションの内容はオンライン... 確かに熱量やムードを感じ、パワーを受け取ることができるのだけど...

と、おもいきや、オマケと思っていた懇親会では、「濃い面々」の興味深い話が... 「スクラム、良さそうだからやってみたくて」なんて漏らそうもんなら、嵐の禅問答が!

勉強不足でしたぁ

Scrum とは何なのだ?

面白いことに、スクラムを知る人ほど、この問いで遠目になる。 それはどういう事象なんだろうと思案していると、散歩中にふとシナプスが繋がった。

私はデザインを学んだが、「デザインとは何ですか?」という問いに似ているんだろう。しかし未だに、デザインの本質を射抜く言葉は持ち合わせていない。そして、デザインを知れば、良いプロダクトが生み出せるというわけでもない。あえて、デザインは何かと言えば、それを探し続けるマインドそのものなのかもしれない。

チームメンバーの成長に期待するから、ScrumというKATAを学ぶのだ

Scrumは、近代的な開発手法、あるいはフレームワークだと思っていたが、実は、チーム自身が成長するための鍛錬でありKATA(型)なのである。

SCRUM FEST Osaka 2022に参加しました!イベントレポート1

6/17(金)、6/18(土)に開催された「SCRUM FEST Osaka 2022」に、サブマネージャーの山本と一緒に参加しました。

今回のscrumfesはオンラインがメイン、現地会場からの参加も可能なハイブリッド開催でした。

6/17(金)

この日は、16:30からオープニングトーク、17:00から角 征典さんの基調講演でした 私達は、 業務後に会社から鑑賞しました。

基調講演「クリーンスクラム―基本に立ち戻れ―」

感想

DS(どうかしている)というパワーワードは、しばらくアジャイル界隈で語られそう…

尖った人もいるチームはそれだけイノベーションが起こりやすく、そんなチームを取りまとめるのがスクラムマスターの役割の一つ。多様性大事。

スクラムの要素を新しい角度から一つ一つ分析していく内容は圧巻でした。

F1大阪グランプリ22

感想

基調講演と各コミュニティからの紹介セッションの後は、「F1大阪グランプリ」が開催されました。

「F」はフィードバック。とのことです。

各地で活躍しているアジャイルコーチの方々が、お題に対してフィードバックしていく「大喜利」的な内容です。

ワイワイしながらな感じですが、各マシン(参加者の方々)の走り(フィードバックの内容)は、エッジの効いたものばかりでした。

6/18(土)

2日目は、大阪本町の現地会場に移動して参加しました。

日本全国のアジャイルコミュニティがトラックを持ち、同時に幾つものセッションがオンラインで進行します。 タイムテーブル

大阪の会場では、各コミュニティ(トラック分)のipadが設置され、ipadにイヤホンを刺すことで、そのトラックで行われているセッションに参加出来る形です。 なかなか斬新…

どのセッションに参加するか悩んだのですが、下記のセッションに参加しました

組織の崩壊と再生、その中で何を考え、感じたのか。そして本当に必要だったもの

感想

現在、どんどん組織が拡大中の弊社ですが、いつか「うまくいかなくなる時」がやってくると思っています。

Rettyさんは、今でこそLeSSや自己組織化の浸透が進んでいるとのことですが、数年前まではツラい時期があったとのことです。

「崩壊しない組織より、適度なスクラップ&ビルドを乗り越えることが出来る組織の方が良い」という言葉は印象に残りました。

Managing for Happinessまもなく出版!プラクティス欲張り全部盛りジェットコースターワーク

感想

カンファレンスといえばワークショップ!ということで参加してみました。 90分でManagemnet3.0のワークをガンガンやっていくセッションです。

先日、オフサイトミーティングを開催し、メンバーがお互いに理解しあうアクティビティを行いましたが、このワークショップでは色々な角度からチームを掘り下げる方法を学ぶことが出来ました!

機会があれば、メンバーとManagement3.0のワークショップに参加して、弊社にも取り入れることが出来れば…!と思いました。

プロダクトってなに? マネジメントってなんなの? ゼロからプロダクトマネジメントを明らかにするぞ

感想

マネジメントとは?プロダクトとは?という根源的な問を、分析・明らかにするセッションでした。

スタートアップはプロダクトがすべてのところがあるので、再度、動画をじっくり見させて頂こうかと思います。

各登壇のスライドはスクラムマスダーさんがブログにまとめられているので、ご参考下さい!

他にも聞いてみたいセッションが山のようにあるので、動画が公開されるのが楽しみです。 しばらくはscrumfes漬けの日々になりそうです…

CSSフレームワーク「xstyled」について

こんにちは、Baseconnectのエンジニアインターンの大島です。 この記事では私が主に開発を行っているMusubuのフロントエンドの技術スタック、特にCSSフレームワークに注目して紹介したいと思います。

Musubuのスタイリング

Musubuの開発ではCSSフレームワークに、styled-componentsxstyledを用いて開発を行っています。

今回は聞き馴染みがない方が多いであろうxstyledについて注目し、Baseconnectでの使われ方も合わせて紹介したいと思います。

xstyledとは

xstyledはReact開発におけるpropsベースのCSS in JSフレームワークです。 styled-componentsやemotionに互換性があり、すでにこれらのCSS in JSを用いて開発を行っているプロダクトには簡単に導入ができます。

コンポーネント

import { x } from '@xstyled/styled-components'

type ButtonProps = typeof x.button.defaultProps & {
  leftIcon?: React.ReactNode
}

const Button = ({children, disabled, leftIcon, ...props}: ButtonProps) => {
  return (
    <x.button
      w="60px"
      fontWeight="bold"
      backgroundColor={disabled ? 'blue-500' : 'blue-400'}
      {...props}
    >
      {leftIcon && <IconWrapper>{lefIcon}</IconWrapper>}
      {children}
    </x.button>
  )
}

xstyledを使うことによるメリット

  1. utility first
  2. 命名コストがない
  3. presentational componentなのか、styled-componentなのか迷わない
  4. レスポンシブ対応や擬似クラスも容易に対応可能
  5. 開発速度の向上

などが挙げられます。一つずつ見ていきます。

1. utility first

xstyledでは、各タグに<x.*>をつけることでpropsでスタイルを当てることができます。そのため、事前にスタイルを当てるためのCSSを宣言する必要はありません。

また、カスタムCSSを一行も記述することなく、完全にカスタム化されたコンポーネントデザインを実装することができます。 たとえば、spaceXpropsであれば、通常以下のCSSを記述する必要がありますが、xstyledなら、1行で済みます。

--x-space-x-reverse: 0;
margin-right: calc({space} * var(--x-space-x-reverse));
margin-left: calc({space} * calc(1 - var(--x-space-x-reverse)));

2. 命名コストがない

これは私としてはかなり大きいメリットだと感じています。 styled-componentでは、それぞれstyleに対して命名する必要があるかと思います。これは開発者にとって脳に無駄なリソースを割くことになるため精神安定上良くないと思っています。 xstyledでは先の例で示したとおり、propsに渡すことでスタイルを当てることができるため命名の必要がありません。

これが解決できるだけでも十分なメリットがあると感じています。

3. presentational componentなのか、styled-componentなのか迷わない

styled-componentを使って開発を行っていると、これがstyled-componentなのか、presentationalなコンポーネントなのかがわからないということが頻繁に起こります。そのたびに、そのコンポーネントを参照してどちらなのかを確認することが必要になります。

命名規則をもたせることで解決できることもあるかもしれないですが、規則があったとしても命名コストはつきまとってきます。

xstyledならどのタグにどのスタイルがあたっているか一目瞭然となり見通しも良くなります。

4. レスポンシブ対応や擬似クラスも容易に対応可能

Musubuでは詳細な企業情報を扱ったり、営業リストを作成する機能を備えているため、モバイル端末をメインで扱っていません。 しかし、あらゆるユーザーを想定し、最低限のレスポンシブ対応を行っています。その際にxstyledは非常に容易に定義できます。

styled-componentの場合、メディアクエリを用いて以下のように定義する必要があるかと思います。

const Wrapper = styled.div`
  display: "block";
  background-color: "#fff";
    
  &:hover {
    background-color: "#000";
  }
    
  @media (min-width: 768px) {
    display: "flex";
  }
`
const ExampleComponent = () => {
  return (
    <Wrapper>
      <div>...</div>
      <div>...</div>
    </Wrapper>
  )
}

xstyledを用いることで簡潔に記述することができます。

// xstyledの場合
const ExampleComponent = () => {
  return (
    <x.div
      display={{
        _: "block",
        md: "flex"
      }}
      bg={{
        _: "#fff",
        hover: "#000"
      }}
    >
      <x.div>...</x.div>
      <x.div>...</x.div>
    </x.div>
  )
}

5. 開発速度の向上

xstyledはTypeScriptで開発されているため、型の恩恵を受けられます。そのため、styled-componentでよくあるtypoによってstyleが付与されないという問題を解決することができます。

また、styled-componentの場合、条件によって付与するスタイルが異なる場合、かなり冗長なコードになると思います。しかし、xstyledはpropsベースのため、条件分岐を簡単に書くことができます。

さらに、特に個人で開発を行う際など、colorやspaceなど全て自分で決めるのは少々面倒なこともあります。xstyledでは、v2以降TailwindCSSに影響を受けているため、デフォルトでいい感じのThemeファイルを用意してくれています。

xstyledのデメリット

ここまでメリットについて述べてきましたが、デメリットについてもいくつか上げたいと思います。

  1. コミュニティが他CSSライブラリと比べ活発ではない
  2. ドキュメントが少ない(Qiita, Zennでの検索ヒット数は合わせて1件)
  3. 複雑な疑似要素などを扱えない

これらの理由により、ある程度の規模のプロダクトでxstyled単体での利用は難しいと感じています。 そのため、styled-componentやemotionと併用して用いることで快適なフロントエンド開発を行えると思います。

Musubuでの活用事例

ここからは、Musubuでのxstyledの活用事例について紹介したいと思います。

theme file

xstyledではデフォルトでいい感じのthemeファイルを用意してくれています。 Musubuではデフォルトのthemeファイルを使わず、オーバーライドすることでデザインシステムを構築しています。

styled-component と xstyledの使い分け

Musubuではxstyledをベースに開発を行っています。しかし、先に述べたとおり、疑似要素を扱えないためbeforenot(:first-child)などを使いたいときにstyled-componentを使っています。 さらに、styled-componentを作成する際に@xstyled/styled-componentからimportすることで、xstyledの恩恵を受けながらstyled-componentで複雑な疑似要素を扱えるようになります。

import styled, { x } from '@xstyled/styled-component'

const Wrapper = styled(x.div)`
  &:before {
    ...
  }
`

const ExampleComponent = () => {
  return (
    <Wrapper w="1200px" bg="gray-100">
      ...
    </Wrapper>
  )
}

これにより最大限見通しを良くした状態でstyleを当てることが可能になります。

最後に

ここまで読んでいただきありがとうございます。Musubuのフロントエンド開発で用いられるCSSフレームワークについて書いてきました。

styled-componentemotionに辛みを感じている方にとって、何か役に立てましたら嬉しいです。

もし今後の技術選定に関わっていきたい方や、もう少し詳しい話を聞いてみたいという方、ぜひカジュアルにお話ししましょう! Baseconnectではエンジニアメンバーも絶賛募集しています。

meety.net

herp.careers