就職してチームビルディングしてます
ゆるWeb勉強会@札幌 Advent Calendar 2020 2日目の記事です。
エクストリームプログラミングやスクラムなどアジャイル開発に憧れていたSESのエンジニアが、就職して自分達のチームを持つようになり、チームビルディングをやりました。
現状うまくやれていると思っているので、今に至るまでの話をしたいと思います。
就職した
ここ数年フリーランスエンジニアとして大手SIerのシステム開発・保守の現場で働きながら、アジャイル札幌というコミュニティに参加して多くの先輩方に出会い影響を受け、自己研鑽していました。
そこで得た知見や技術をシステム開発・保守の現場で発揮しつつ、ゆるく就職活動をしていました。 そして今年の春、とあるAIベンチャーからオファーを受け、就職しました。
蛇足ですがポートフォリオとしてナンプレのアプリを完成させていたのですが、それも採用担当の方々には見て頂けてたようです。作って良かった。
チームリーダー?になった
入社した際、ベテランソフトウェアエンジニアは私も含めおそらく5〜6名しかいない状況でした。 ソフトウェアの会社ではありますが、主にAIの構想やソフトウェアの要件定義を行っており、開発は外注していました。それを内製に変えていく方針となり、我々が集められたという状況です。ほかには、駆け出しソフトウェアエンジニアやソフトウェア開発に関連する業務に関わってきた方が数名いました。
弊社では、自社プロダクトと、受託開発のプロダクトとがあります。自分は、自社開発のアプリケーションのフロントエンド開発チームのリーダー的役割として働くことになりました。
入社の際に2日間だけ会社へ赴き、チームメンバーと顔合わせをしました。メンバーは駆け出しエンジニアが3名。それ以降私はずっとリモートで働くことになります。とりあえずその場では挨拶だけし、会社を後にしました。
チームのキックオフをした
チームで動き始めるまで2週間ほど間がありました。その間に、だんだんと「このまま始まって大丈夫なのか?」という思いが強まりました。
そこで、メンバーのことを知るためにチームのキックオフをやらせてもらいました。キックオフの内容は以下です。
- みんなが何を目指しているのか教えて!
- どんな働き方してるんですか?
- どんな働き方したいですか?
1. みんなが何を目指しているのか教えて!
ここでは、川口さんの記事を参考にスキルマップを用意しておき、まずは自分の自己紹介をしました。
「自分は素敵なプログラマーになりたいです!リーダーというのはちょっと苦手です。」
そして、みんなが伸ばしたいスキルについても聞きました。
2. どんな働き方してるんですか?
弊社は、ソフトウェア開発の文化が形成されていない状況でした。 エンジニア個人が複数のプロジェクトにアサインされて、マルチタスクをこなしていかなければならない状況です。これはマズいと思っていました。そこで、現状を聞き取り、このままではマズい状況だということを共通認識としました。
とは言え、相手は駆け出しエンジニアですので、これが普通だと思っていたのだと思います。なので実際には「このままだとマズいんだよ。」ということを教えた形になります。
具体的には以下がマズいよと言いました。
- 不得意な分野で苦しむ
- デザインが得意なのに性能改善(メンバーにはデザイナーもいる)
- プログラマなのにデザイン
- プロジェクトの炎上時に個人で対応
- 精神的負担が大きい
常に不安が付きまとう!!
3. どんな働き方したいですか?
僕はこうしたいと伝えました。(理想は一つのプロジェクトを担当したいけどそれは無理)
固定されたチームで、チームの文化やチームワークを育てていきたい。
チームでやるメリットは以下だと伝えました。
- メンバーに頼ろう!
- 得意な人にサクッとやってもらう!
- 得意な人に教えてもらいながらやる!
- 得意な人がいない場合はみんなでやる!
- 精神的負担の軽減
- 不安な状況を作らない!
- 独りに責任を負わせない!
- One for all! All for one!! (一人はチームのために、チームは一つの目的のために)
みんなの同意を得ることができました。
それを実現するために「やってみたい」こと
以下をやってみたいと思っていると伝えました。
- フラクタルスプリント
- 通常1週間~2週間のスプリントを、daily/houryでもやる。
- ペアプログラミング、モブプログラミング
- 画面共有しながら複数人でプログラミングする
- 知見の共有
- コードの共有(誰かが抜けても問題ない)
- チームを固定化
- チームの存在を会社みんなに認識して貰って、チームに対して仕事をおろして貰うのが理想。
会社のみんなにアピールしてみた
チームでやりたいということを、周りの人に言って回りました。 現在の体制を決めている弊社役員の方々の協力も得られ、今後はソフトウェア開発の文化がうまく形成できるのではないかと期待しています。まだまだ時間がかかるとは思います。
実際にチームでの活動を始めた
メンバーには、「実験と改善を繰り返そう」と伝えました。
我々は一応スクラムをやっています。1週間のスプリントでやってます。 が、僕もメンバーもスクラムについて詳しくなく、あまり胸を張れないです。
1週間単位の大きなスプリントの制御は新米スクラムマスターに任せています。もちろん自分も協力してますが。 自分は主に、もっと短いスパンで「実験と改善」を繰り返す文化を形成することを目的に、フラクタルスプリントの真似事をやっています。
フラクタルスプリント(実験してみよう)
「実験してみよう」を合言葉のように使い、チームでの「やり方」を色々と試してみました。
1時間スプリントとYWTしてみた
スプリントというか、「この時間はこの作業をやります」と宣言してペアワークを始めます。 初めの10分でキックオフを行い、40分作業をし、残り10分で振り返りをしてみました。振り返りの方法はYWT(やったこと、わかったこと、次やること)です。チームみんなで振り返るのではなく、一緒に作業したメンバ同士で振り返ります。スプリントとは言わないかもですね。と言うか我々もスプリントという言葉はほとんど使ってません。
感想としては、「とても良かった」です。
- キックオフで作業の目的が明確になり、作業の時間の集中力が増すように思いました。
- 振り返りのYWTでは、なるべく敷居の低い内容を挙げるよう意識してました。論理的な言い方や学術的な言い方はあまりしませんでした(できないので)。ちょっと恥ずかしい感じがしますが、感謝の気持ちを伝えることも意識しました。
- 「このやり方良かったよね。」
- 「ここの設計、うまく機能してたよね。」
- 「なんか、(ペアプロの)ドライバーの交代がうまくいかないね。」
- 「TODO書きながらやってみようか。」
- 「あのときのあの発言がとても助かりました。」
- ちょっと恥ずかしい感じがしますが、感謝の気持ちを伝えることも意識しました。
メンバー間で「なんかYWT良いね」と言う雰囲気になり、続けています。 作業してそのまま「あれ良かった」「あれ良くなかった」と言う話ができると、とても具体的な改善策を決めることができるのが良いと感じてます。
ペアワーク・モブワークしてみた
ペアワーク・モブワークはすぐにメンバーにも受け入れられ浸透しました。メンバーとの仲も良くなりました。そして現在では「基本的には一人で作業しない」という文化が形成されました。
今僕は、毎日誰かとペアプロをしている状況です。とても疲れますが、確実に成果は上がっていると思います。現在、以下の点でメリットを感じています。
- メンバーへの技術と知見の伝達
- 設計(コードの構造)の共有
- 難しいところを一緒にやった場合、その箇所で問題が発生して自分が手を出せないような状況で「君、あそこわかるよね?任せるよ!」と言える。
- 自分が迷った際に意見を貰える(自分は結構迷う)
- この変数名の方が良いと思います
- わかりやすいです・わかりません
- これはクラスにした方が良いと思います
- メンバーとのチームワークが出来上がる
- 都度振り返りを行うので、どんどんやりやすくなる
- 実験して得たチームプラクティスを他のメンバーにも伝達してみんなが良くなっていく
チーム固定で活動してみる
これが一番、実現が難しいところです。自分たちではどうにもならない部分があります。
現状では、メンバー各々が別々のプロジェクトに関わっている状況が続いています。 が、若手メンバーが別のプロジェクトで困った際には、僕がヘルプに入ってペアワークで解決しています。 これを続けていくことで、別のプロジェクトのマネージャーにも、我々がチームであることを認識してもらえると考えています。
今はまだ、「チームで仕事を請け負い、タスクを消化していく」ということはできていませんが、周りにアピールしていることで、協力者は増えています。いつか実現できると思います。
まとめ
始まったばかりのチームですが、チームが密に連携して活動していくことができるようになりました。 今のチームがすごく気に入っていますし、毎日楽しく活動しています。 今後は優秀なソフトウェアエンジニアをもっと増やすべく、邁進していきたいと思います。自分もレベルアップせねば。
読んで頂きありがとうございました!
TypeScriptで型安全な関数合成を行う
型安全な関数合成関数を作成したのでメモします。
関数合成を行う関数compose
const defaultFunction = (p: any) => p; const compose = < F extends (...args: any[]) => T0, T0 = ReturnType<F>, T1 = T0, T2 = T1, T3 = T2, T4 = T3, T5 = T4, T6 = T5, T7 = T6, T8 = T7, T9 = T8 >( f0: F, f1: (p: T0) => T1 = defaultFunction, f2: (p: T1) => T2 = defaultFunction, f3: (p: T2) => T3 = defaultFunction, f4: (p: T3) => T4 = defaultFunction, f5: (p: T4) => T5 = defaultFunction, f6: (p: T5) => T6 = defaultFunction, f7: (p: T6) => T7 = defaultFunction, f8: (p: T7) => T8 = defaultFunction, f9: (p: T8) => T9 = defaultFunction ) => (...p: Parameters<F>) => f9(f8(f7(f6(f5(f4(f3(f2(f1(f0(...p))))))))));
関数10個までを合成します。
composeの使用
例えばFizzBuzzを以下のように作ることができます。 Fizz Buzz - Wikipedia
function createFizzBuzz( fizzBuzzString: string, ...nums: number[] ): (input: number | string) => number | string { return (input: number | string) => typeof input === 'number' && nums.every((num) => input % num === 0) ? fizzBuzzString : input; } const toFizz = createFizzBuzz('Fizz', 3); const toBuzz = createFizzBuzz('Buzz', 5); const toFizzBuzz = createFizzBuzz('FizzBuzz', 3, 5); function toString(input: number | string): string { return input.toString(); } const fizzbuzz = compose(toFizzBuzz, toFizz, toBuzz, toString); describe('FizzBuzz', () => { test('2 -> 2', () => expect(fizzbuzz(2)).toEqual('2')); test('3 -> Fizz', () => expect(fizzbuzz(3)).toEqual('Fizz')); test('4 -> 4', () => expect(fizzbuzz(4)).toEqual('4')); test('5 -> Buzz', () => expect(fizzbuzz(5)).toEqual('Buzz')); test('6 -> Fizz', () => expect(fizzbuzz(6)).toEqual('Fizz')); test('10 -> Buzz', () => expect(fizzbuzz(10)).toEqual('Buzz')); test('15 -> FizzBuzz', () => expect(fizzbuzz(15)).toEqual('FizzBuzz')); test('30 -> FizzBuzz', () => expect(fizzbuzz(30)).toEqual('FizzBuzz')); });
生成した関数の型
composeで作成した関数は、型が特定されています。
composeに指定した第一引数の関数の引数の型が、composeで生成した関数の引数の型になります。 また、composeに指定した最後の関数の戻り値の型が、composeで生成した関数の戻り値の型になります。
生成したfizzbuzz関数の引数の型が「string | number」なのがイマイチですね。 以下のようにnumberを引数に取る関数を第一引数にすると良いです。
引数に渡す関数も型安全
例えば、以下のようなcomposeの引数を
const fizzbuzz = compose(inputNumber, toBuzz, toString);
以下のように変えると型エラーになります。
const fizzbuzz = compose(inputNumber, toBuzz, inputNumber, toString);
これは、toBuzzの戻り値の型がstring | number
であるのに対し、inputNumberの引数の型がnumber
であるためです。
compose関数の解説
const defaultFunction = (p: any) => p; // ① const compose = < F extends (...args: any[]) => T0, // ② T0 = ReturnType<F>, // ③ T1 = T0, // ④ T2 = T1, T3 = T2, T4 = T3, T5 = T4, T6 = T5, T7 = T6, T8 = T7, T9 = T8 >( // ⑤ f0: F, f1: (p: T0) => T1 = defaultFunction, f2: (p: T1) => T2 = defaultFunction, f3: (p: T2) => T3 = defaultFunction, f4: (p: T3) => T4 = defaultFunction, f5: (p: T4) => T5 = defaultFunction, f6: (p: T5) => T6 = defaultFunction, f7: (p: T6) => T7 = defaultFunction, f8: (p: T7) => T8 = defaultFunction, f9: (p: T8) => T9 = defaultFunction ) => (...p: Parameters<F>) => f9(f8(f7(f6(f5(f4(f3(f2(f1(f0(...p)))))))))); // ⑥
多くの型引数を使用しています。これらは、compose関数に引数を指定した際に、次に指定する関数の型やcompose関数で生成する関数の型を動的に導き出すためにあります。
①defaultFunction
const defaultFunction = (p: any) => p;
defaultFunctionは、受けた引数をそのまま返却する関数です。
composeに指定する関数は1〜10個(f0〜f9まで)指定でき、指定する関数が10個に満たない場合にdefaultFunctionが使用されます。
②ひとつめの型引数 F
F extends (...args: any[]) => T0
何らかの引数を受け取り何らかの返却値:T0を返す関数を継承した型 F を定義しています。 F はcompose関数の第一実引数の型となっており、実際にcompose関数に第一引数を渡したタイミングで型が確定します。
③T0
T0 = ReturnType<F>
関数Fの戻り値の型をT0と定義しています。 実際にcompose関数に第一引数を渡したタイミングで型が確定します。
④T1〜T9
T1 = T0, T2 = T1, T3 = T2, T4 = T3, T5 = T4, T6 = T5, T7 = T6, T8 = T7, T9 = T8
compose関数の引数となる関数の戻り値、兼引数の型をそれぞれ定義しています。 T1〜T9は、実際にcompose関数に第二引数以降を指定することで型が確定します。
デフォルトで、T1はT0と同一の型、T2はT1と同一の型、・・・というように定義しています。 デフォルトの型は、compose関数を使用する際に指定する関数f1〜f9のうちいくつかが省略された場合に適用されます。
⑤実際の引数
f0: F, f1: (p: T0) => T1 = defaultFunction, f2: (p: T1) => T2 = defaultFunction, f3: (p: T2) => T3 = defaultFunction, f4: (p: T3) => T4 = defaultFunction, f5: (p: T4) => T5 = defaultFunction, f6: (p: T5) => T6 = defaultFunction, f7: (p: T6) => T7 = defaultFunction, f8: (p: T7) => T8 = defaultFunction, f9: (p: T8) => T9 = defaultFunction
f0
f0はFの型の関数を指定する必要があります。 と言っても、Fは如何なる関数も許容する型ですので、関数なら何でもOKです。
f0を指定すると、関数Fの型が確定します。
例えば、f0に上述のinputNumberを指定した場合、inputNumberは(input:number)=>number
型であるため、この型がFということになります。
f1〜f9
f1はf1: (p: T0) => T1
のように定義しています。
これは、「T0の型の引数pを受け取り、T1の型の引数を返す関数」を意味しています。
T0は第一引数の関数f0のReturnTypeです。つまり、第一引数の関数f0の戻り値がstringなら、第二引数の関数f1の引数はstringで確定します。
例えば、f0に上述のinputNumberを指定した場合、inputNumberの戻り値の型であるnumberがf1の引数である必要があります。この時点ではf1の戻り値は何でも良いです。
f2以降も同様の考え方です。
このように、composeの型引数と実引数は、前の引数の型によって後の引数の型を決定していく構造となっています。
引数を省略した場合
f0以外の引数は省略可能となっており、デフォルトでdefaultFunctionが適用されます。
defaultFunctionは、受け取った引数をそのまま返却する関数です。 defaultFunctionを使用した場合、引数と戻り値の型が変わりません。 そのため、f1〜f9が途中から省略された場合には、最後に指定した関数の戻り値が、compose関数で生成した関数の戻り値の型T9として確定します。
⑥compose実行部分
// 引数部分省略・・・ ) => (...p: Parameters<F>) => f9(f8(f7(f6(f5(f4(f3(f2(f1(f0(...p))))))))));
compose関数を実行すると、(...p: Parameters<F>) => f9(f8(f7(f6(f5(f4(f3(f2(f1(f0(...p))))))))))
の関数を生成し返却します。
生成する関数の引数はF関数の引数と同じとなります。 生成する関数は、関数f0にpを渡し、その結果を関数f1に渡し、その結果を・・・という具合にf0〜f9を全て実行する内容となります。
以上。