宮水の日記

宮水の日記

主に書評や資格取得について記事を書いています。

「SCRUM BOOTCAMP THE BOOK」を読みました

みなさんこんにちは!宮水です。
今回は、SCRUM BOOTCAMP THE BOOKを読みました。

なぜ読んだのか

以前、スクラムの考え方の元となっている「アジャイル」については以下の本で勉強し、"計画づくり"が大切だということを学びました。
アジャイルな見積もりと計画づくり - 宮水の日記

ベースの考え方を学んだので、続いてアジャイル開発手法の1つである「スクラム」について学んで行こうと思いました。

対象読者

この本で学べること

  • 基礎編では、スクラムの全体像と決められているルールについて学べます。
  • 実践編では、架空の開発現場を題材に開始時から時系列に沿ってスクラムでどう進めていくのか説明します。

注意すること

  • この本でスクラムの全てがわかるわけではないし、書かれていることを鵜呑みにしない。
  • 自分たちの現場ならどうするか考えながら読む。

スクラムってなに?--基礎編--

出てきた用語だけまとめました。
いつも使用している用語と少し異なるものも時々ありましたが、普段からやっている会議や制作物はこういう目的があったのか!と勉強になりました。
本には、もっと詳しく「イベント毎のロールの役割」や「成果物の役割」が書いてあります。

アジャイル開発とは

アジャイル開発とは、似たような開発手法に共通した価値観と行動原則のこと。

スクラムとは

アジャイル開発手法の1つ。
5つのイベント、3つのロール、3つの制作物など最低限のルールのセットで構成されている。
ルールを実際どのように適用していくかは、自分たちで決める。

5つのイベント

①スプリント ... 計画を実行する単位。短く区切って繰り返す
②スプリントプランニング...スプリントで何を達成するかPOから聞く。開発チームはどのように実剣するか計画を立てる。
③デイリースクラム ... 昨日やったこと、今日やること、困っていることを共有する。
④スプリントレビュー ... スプリントで完成したものをデモする。
⑤スプリントレトロスペクティブ ... うまくいったこと、今後の改善点、アクションプランを作る。

3つのロール

①プロダクトオーナー ... プロダクトのWhatを担当する
②開発チーム ... プロダクトのHowを担当する
スクラムマスター ... スクラムの仕組みがうまく回るようにする人。

3つの制作物

①プロダクトバックログ... 実現したいことをリストにして優先順位をつける
②スプリントバックログ ... プロダクトバックログを具体的な作業に分割する
③インクリメント ... スプリント終了時点で完成していて、正常に動作しているものの単位。完成の定義は、チームで決める。

どうやればうまくいくの?--実践編--

実践編では、やりたそうにしていたからという理由だけでスクラムマスタに任命された「ボク君」や、営業部から移籍してきた「キミちゃん」などの登場人物と一緒にスクラムのやり方を学んでいきます。

Scene 1 プロダクトオーナーは誰だ

スクラムには3つのロールがあります。
プロダクトオーナーは、「何のために何をどういう順番で作るか考える人」
開発チームは「POが実現したいことを実際に作る人」
スクラムマスタは「POと開発チームが開発をうまく進められるようにする人」

ロールは単なる目印であり、誰がリーダーとか偉いとかないというのが印象的でした。
誰がどのロールに適任かどうかは、「ロールが求めていることに熱心に取り組んでくれる人」が理想だそうです。

弊社にはスーパーPOがたくさんいるのですが、みなさん営業出身で、ユーザーファーストを具現化したみたいな方が多くいるので、いつも勉強になっています。

Scene 2 どこに進んでいくの?

この章では、「どういうことを実現するのか」「絶対に達成したいことは何か」を決めるためのフレームワークインセプションデッキ」についての紹介です。

ビジネス上のゴールについての質問「エレベータピッチ」とミッションについての質問「我々はなぜここにいるのか」のスライドを作りましょうという話でした。

Scene 3 いつ頃終わるんだい?

この章では、”見通しの立て方”について解説されていました。
プロダクトバックログというものを使って、欲しい機能を列挙します。
優先順位などの開発順序もスクラムチームで考えます。

Scene 4 正確に見積もれません?!

4章は、見積もりについてです。スクラムチームは、「作業の量」で見積もります。
基準となるタスクを設け、それより大きいか?小さいか?でタスクの作業量を見積もっていきます。

Scene 5 僕なんかでいいですか?

5章は"正確な見積もりをする工夫"についてです。

曖昧なことを確実にしようとして膨大な時間を費やしても確実なものにはならない。スクラムでは推測より確実なものに時間を使う。
という文が印象的でした。

見積もりポーカーの紹介がありました。
見積もりポーカーでは、開発者間の認識合わせに役立ちます。認識のズレが数字のズレとして現れます。
見積もりポーカーは数字を合わせることではなく、対話することに意味があります。

Scene 6 いつ何が手に入るのか?

6章では、ベロシティについて解説されていました。
ベロシティは、1スプリントに消化できたポイントのことで、決定するものではなく計測を続けていくものです。

Scene 7 ちゃんと計画できたかな?

7章は、スプリントプランニングについてです。
前回のベロシティを考慮して、今回のスプリントで実現できそうなタスクを積んでいく作業のことです。

どの段階で「完成した」と言えるのかどうかチームで合意を取っておくこと、「これなら確実に達成できるぞ」と自信が持てるくらい具体的で詳細な計画を作るのがポイントです。

Scene 8 スプリントは順調かな?

8章はデイリースクラムについてです。
一日1回、同じ時間に昨日やったこと、今日やったこと、困っていることを共有します。

デイリースクラムは進捗共有会ではなく、問題があればすぐにスプリントの残りの進め方を見直したり、何か対策したりするためにあります。
見積もりも、予想なので何か問題があった場合必要に応じて更新するべきです。

Scene 9 これって間に合うのかな?

スプリントの中で問題になりそうなことを見つけたり、全員のタスクの状況を把握するためにタスクボードを使う。
それぞれのタスクが未着手、着手、完了などそれに合わせて貼っておきます。

Scene 10 だいたい終わってまーす!!

「要望通りに作った」というのは人によって異なることがあるので、実際に出来上がったものをデモして目で見ることが大切です。

上述したように、ここでも「完成の定義」が大切になってきます。
みんなで決めた完成の定義を満たしているかどうか、デモによって明らかにします。

Scene 11 もう1日あれば...

タスクを積んで、スプリント内に終わらず、あと1日あれば終わるんだけど...というケースはよくあります。

そういった場合でも、スクラムではスプリントの期間を変えたりしません。理由は、スクラムチームの実力を正しく測り、納期を予測しやすくするためです。

Scene 12 少し早く終わったぞ!

逆に、予定していたタスクがスプリント期間が終わる前に消化できたとします。
その場合は、次に予定しているタスクをプロダクトバックログから取り、開発を進めていきます。
そのために、プロダクトバックログは開発順・優先順位が高い順に並んでいます。

Scene 13 全員揃ってないけど...

スクラムには、3つのイベントがあります。
スクラムチームは、全員がイベントの目的を理解し、当事者意識を持って"自分達だけで"イベントを進められるようにする必要があります。

スクラムがうまく進んでいないうちは、だんだんイベントに出席しなくなったり、垂れてくることもありますが、そのためにスクラムマスタがいます。スクラムマスタは、スクラムチームの課題に気づかせてあげたり、課題に対してアドバイスをしたりします。

Scene 14 もっと早くできないの?

ベロシティが上がっていると、良い兆候に見えます。
しかし、安定しているベロシティがいいベロシティです。なぜかというと、見積もりもタスクの消化もうまくいっている証拠だからです。

Scene 15 え? POがいないの!?

POには、必ずイベントに参加してもらいましょう。
特に、出来上がったものは必ずPOにレビューしてもらい、フィードバックを得ましょう。
スクラムマスタが、POの支援をしても良いです。

Scene 16 うまく伝わってるのかな?

ロールが違うと、実現したいもののアプローチも違います。
POはユーザー目線が、開発者はどうやって実現するかを考えるのが得意だからです。

開発者に、どういったものを作って欲しいのかうまく伝えるのに、ユーザーストーリーというものがあります。

Scene 17 何かがおかしいぞ!?

スクラムを進めていく上で、チームが何か問題を抱えている気がする...という場合。
本書では、みんながプランニングで決めた期限を守るためにものすごく残業しているとか、完成しそうにないことに薄々気づいているけど、誰も気に留めないなどが問題の兆候としてありました。

そこで、スクラムマスタが「本当に大丈夫か」声かけをしてあげます。問題に気づかせてあげるのも、スクラムマスタの重要な役割です。
そうすると「前に書いたコードが汚すぎて修正が大変」という具体的な問題点が浮かび上がってきました。

Scene 18 すぐに解決できないよ...

スクラムチームは、問題になりそうなことやうまくいってないことに素早く対処して開発を進めていきます。
問題が起きたら、早めに対処するために、タスクボードに問題に対するDoToリストを作り、優先順位をつけて片付けていく方法があります。

Scene 19 今後のことがわからない!?

19章では、プロダクトバックログのお手入れについてです。
プロダクトバックログでは、最初に取り決めた機能や、スプリントレビューでレビューされた意見や、バグ修正など色々なタスクを貯めておくところですが、定期的にお手入れしないと、優先順位がわからなくなってしまいます。

最初のうちは、バックログを定期的にお手入れするイベントを用意すると良いようです。

Scene 20 本当に着手してもいいのかな?

20章は、手戻りを少なくする方法についてです。

作りおえて、スプリントレビューのときに「何か思っていたのと違う!」となってしまっては、もったいないです。そうならないためにも、タスクに着手する前から、仕様を明確にし、実現したいことを深く理解するべきです。

受託で働いていたときは「とりあえず動くものを作らないとお客さんもイメージが湧かない」ということがありました。そういうときは、紙でもパワーポイントでもいいので、プロトタイプを作ると良いそうです。

Scene 21 あれ!? 間に合わない...

ゴールは常に動いていくもの。
リリースが間に合わない場合、品質・予算・期間・スコープの4つのうち、期間の調整かスコープの調整を行おう。

「どう実現するか」考え直すのもスコープの調整といえます。

Scene 22 この作業は苦手です...

22章は、スキルマップについてです。
スクラムチームは自分たちで問題を解決できるし、自分たちで次にやるべきことを見つけられる独立したチームでありますが、個人個人は得手不得手があります。
そこで、スキルマップを作って、開発チームの得意なこと、苦手なことを把握し、チームで協力して開発を進めていくのがいいスクラムチームです。

Scene 23 それぐらいはできるよね?

スプリントゴールの達成へのコミットメントを表明するのは、開発チームであり、他の人が口を挟んではいけません。
一方で開発チームは、責任を持って約束できるようになることが大切です。

Scene 24 やり残したことはないかい?

24章は、リリースについてです。
以前みんなで決めた完成の定義を満たしていても、リリースの定義を満たしてなければリリースはできません。

そこで、リリーススプリントといって、リリースのために必要なタスクを洗い出すためのスプリントがあります。ただし、本来はリリーススプリントがなくてもいいようにするのが理想です。

Scene 25 ここからが始まりさ!?

実際の現場はもっと複雑で大変です。
スクラムを導入したからといって、うまくいくわけではありません。
スクラムは、

  • 開発でうまくいっていないことを特定しやすい
  • 問題を解消する機会がある
  • うまく進めるためにやり方を変えられる機会がある
  • 多少やり方を変えても、影響が少ないようになっている

のが特徴です。これを活用できるかどうかは、スクラムチーム次第です。

感想

今回は、SCRUM BOOTCAMP THE BOOKを読みました。
以前「アジャイルな見積もりと計画づくり」を読んでいたものの、スクラムについて絞った本を読んだのは初めてでした。
上記の本より、少し解像度高くスクラムの用語や進め方の基本の型を学ぶことができて、よかったです。

現在私はスクラムで開発をしているチームにいるのですが、すごく良いスクラムチームに所属させていただいているんだなと改めて思いました。
振り返りのときは毎回よかったことも良くなかったこともしっかり振り返られているし、毎スプリントで改善点を出せたり、このまま良いスクラムチームを継続できるように私も頑張りたいです。

本書には、スクラムで困ることあるあるが漫画も交えてストーリー形式で書いてあってわかりやすくて面白かったです。
場面(Scene)ごとに並べてあるので、スクラムを進めていて「困ったな?」と思ったら読み返すと良さそうです。
時々わからなくなったときに、初心に返るためにまた適宜読み直したいと思います。

以上です。ここまで読んでくださりありがとうございました。

Typescript exercises やってみた 8問目

みなさんこんにちは。宮水です。
今日は、TypeScript エクササイズの8に取り組んでみました。

こちらのリポジトリをforkして、cloneして取り組みます。
rootディレクトリで、yarn installしてから問題文にあるRun this exerciseのコマンドを叩くと、答え合わせができます。
github.com

本日の問題

/*

Intro:

    PowerUsers idea was bad. Once those users got
    extended permissions, they started bullying others
    and we lost a lot of great users.
    As a response we spent all the remaining money
    on the marketing and got even more users.
    We need to start preparing to move everything to a
    real database. For now we just do some mocks.

    The server API format was decided to be the following:

    In case of success: { status: 'success', data: RESPONSE_DATA }
    In case of error: { status: 'error', error: ERROR_MESSAGE }

    The API engineer started creating types for this API and
    quickly figured out that the amount of types needs to be
    created is too big.

 PowerUsersのアイデアはダメでした。
 それらのユーザーが拡張された権限を取得すると、他のユーザーをいじめ始め、多くの優れたユーザーを失いました。
 対して、残りのすべてのお金をマーケティングに費やし、さらに多くのユーザーを獲得しました。
 すべてを実際のデータベースに移動する準備を始める必要があります。とりあえず、モックをいくつかやってみます。
 
    成功した場合: { status: 'success', data: RESPONSE_DATA }
    エラーの場合: { status: 'error', error: ERROR_MESSAGE }

  APIエンジニアは、このAPIのタイプの作成を開始し、作成する必要があるタイプの数が多すぎることをすぐに理解しました。

Exercise:

    Remove UsersApiResponse and AdminsApiResponse types
    and use generics in order to specify API response formats
    for each of the functions.

 UserのAPI応答とAdminのApiResponseタイプを削除し、ジェネリックを使用して、各関数のAPI応答形式を指定します。

Run:

    npm run 8

    - OR -

    yarn -s 8

*/

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

type Person = User | Admin;

const admins: Admin[] = [
    { type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
    { type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' }
];

const users: User[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' }
];

type AdminsApiResponse = (
    {
        status: 'success';
        data: Admin[];
    } |
    {
        status: 'error';
        error: string;
    }
);

function requestAdmins(callback: (response: AdminsApiResponse) => void) {
    callback({
        status: 'success',
        data: admins
    });
}

type UsersApiResponse = (
    {
        status: 'success';
        data: User[];
    } |
    {
        status: 'error';
        error: string;
    }
);

function requestUsers(callback: (response: UsersApiResponse) => void) {
    callback({
        status: 'success',
        data: users
    });
}

function requestCurrentServerTime(callback: (response: unknown) => void) {
    callback({
        status: 'success',
        data: Date.now()
    });
}

function requestCoffeeMachineQueueLength(callback: (response: unknown) => void) {
    callback({
        status: 'error',
        error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.'
    });
}

function logPerson(person: Person) {
    console.log(
        ` - ${chalk.green(person.name)}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
    );
}

function startTheApp(callback: (error: Error | null) => void) {
    requestAdmins((adminsResponse) => {
        console.log(chalk.yellow('Admins:'));
        if (adminsResponse.status === 'success') {
            adminsResponse.data.forEach(logPerson);
        } else {
            return callback(new Error(adminsResponse.error));
        }

        console.log();

        requestUsers((usersResponse) => {
            console.log(chalk.yellow('Users:'));
            if (usersResponse.status === 'success') {
                usersResponse.data.forEach(logPerson);
            } else {
                return callback(new Error(usersResponse.error));
            }

            console.log();

            requestCurrentServerTime((serverTimeResponse) => {
                console.log(chalk.yellow('Server time:'));
                if (serverTimeResponse.status === 'success') {
                    console.log(`   ${new Date(serverTimeResponse.data).toLocaleString()}`);
                } else {
                    return callback(new Error(serverTimeResponse.error));
                }

                console.log();

                requestCoffeeMachineQueueLength((coffeeMachineQueueLengthResponse) => {
                    console.log(chalk.yellow('Coffee machine queue length:'));
                    if (coffeeMachineQueueLengthResponse.status === 'success') {
                        console.log(`   ${coffeeMachineQueueLengthResponse.data}`);
                    } else {
                        return callback(new Error(coffeeMachineQueueLengthResponse.error));
                    }

                    callback(null);
                });
            });
        });
    });
}

startTheApp((e: Error | null) => {
    console.log();
    if (e) {
        console.log(`Error: "${e.message}", but it's fine, sometimes errors are inevitable.`)
    } else {
        console.log('Success!');
    }
});

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/generics.html

答え

以下の5行を関数の上ら辺に追加しました。

type ApiResponse<T> = { status: 'success'; data: T; } | { status: 'error'; error: string; };

type ApiResponseOfAdmin = ApiResponse<Admin[]>;
type ApiResponseOfUser = ApiResponse<User[]>;

type CurrentServerTimeApi = ApiResponse<number>;
type CoffeeMachineQueueLengthApi = ApiResponse<number>;

感想

今回も問題を理解するのにめっちゃ時間かかったんですけど、とにかく`requestCurrentServerTime`関数と`requestCoffeeMachineQueueLength`関数
のunknown型をAdmin用でもUser用でもいいようにジェネリックを使って対応させてくださいという問題でした。

ジェネリックについては、以下のやめ太郎さんの記事が大変わかりやすいです。
4歳娘「パパ、具体的な名前をつけないで?」 - Qiita

今回は、Tの部分がAdminでもUserでもnumberでもOKな、ApiResponseという型を作りました。

type ApiResponse<T> = { status: 'success'; data: T; } | { status: 'error'; error: string; }

ApiResponseOfAdminとか命名イケテなさすぎますねwwww 英語頑張ります😭
ふぅー8問目もめちゃくちゃ難しかった😭 以上です!

Typescript exercises やってみた 7問目

みなさんこんにちは。宮水です。
今日は、TypeScript エクササイズの7に取り組んでみました。
翻訳はGoogle翻訳にぶち込みました。

こちらのリポジトリをforkして、cloneして取り組みます。
rootディレクトリで、yarn installしてから問題文にあるRun this exerciseのコマンドを叩くと、答え合わせができます。すごい!!
github.com

本日の問題

import chalk from 'chalk';

/*

Intro:

    Project grew and we ended up in a situation with
    some users starting to have more influence.
    Therefore, we decided to create a new person type
    called PowerUser which is supposed to combine
    everything User and Admin have.

  プロジェクトが成長し、一部のユーザーがより大きな影響力を持つようになる状況になりました。
  そのため、UserとAdminが持つすべてのものを組み合わせることになっているPowerUserという新しい人物タイプを作成することにしました。

Higher difficulty exercise:

    Define type PowerUser which should have all fields
    from both User and Admin (except for type),
    and also have type 'powerUser' without duplicating
    all the fields in the code.

  UserとAdminの両方からのすべてのフィールドが必要なタイプPowerUserを定義します(typeを除く)。
  また、コード内のすべてのフィールドを複製することなく、タイプ 'powerUser'を持ちます。

Run:

    npm run 7

    - OR -

    yarn -s 7

*/

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

type PowerUser = unknown;

type Person = User | Admin | PowerUser;

const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    { type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
    { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
    { type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' },
    {
        type: 'powerUser',
        name: 'Nikki Stone',
        age: 45,
        role: 'Moderator',
        occupation: 'Cat groomer'
    }
];

function isAdmin(person: Person): person is Admin {
    return person.type === 'admin';
}

function isUser(person: Person): person is User {
    return person.type === 'user';
}

function isPowerUser(person: Person): person is PowerUser {
    return person.type === 'powerUser';
}

function logPerson(person: Person) {
    let additionalInformation: string = '';
    if (isAdmin(person)) {
        additionalInformation = person.role;
    }
    if (isUser(person)) {
        additionalInformation = person.occupation;
    }
    if (isPowerUser(person)) {
        additionalInformation = `${person.role}, ${person.occupation}`;
    }
    console.log(`${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`);
}

console.log(chalk.yellow('Admins:'));
persons.filter(isAdmin).forEach(logPerson);

console.log();

console.log(chalk.yellow('Users:'));
persons.filter(isUser).forEach(logPerson);

console.log();

console.log(chalk.yellow('Power users:'));
persons.filter(isPowerUser).forEach(logPerson);

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/utility-types.html

答え

まず問題の意味を読み解くのに時間がかかりました😇
interface powerUserは作らずに、UserもAdminの特性をもつpowerUser型を作りましょうという意味でした。
つまり、UserだけがもつoccupationもAdminだけがもつroleも持ちます。

  // ココニカイテアッタヨ
    {
        type: 'powerUser',
        name: 'Nikki Stone',
        age: 45,
        role: 'Moderator',
        occupation: 'Cat groomer'
    }

今回のヒントはこちらです。
TypeScript: Handbook - Utility Types

TypeScript provides several utility types to facilitate common type transformations. These utilities are available globally.

ユーティリティ型は、一般的な型変換を容易にしてくれるTypeScriptが用意してくれた型のことだそうです。

今回は、Omitだけ馴染みがあったので、Omitを使いました。

Userのtypeを取り除く & Adminのtypeを取り除く & type: powerUserを追加するという意味の型を作りました。

type PowerUser = Omit<User, 'type'> & Omit<Admin, 'type'> & {type: 'powerUser'};

多分いい感じだと思います。
f:id:kattyan53:20200805084901p:plain

他に回答あったらご教示いただけると嬉しいです!以上です!

「Atomic Design 〜堅牢で使いやすいUIを効率よく設計する〜 」を読みました

こんにちは。宮水です。

今回は、Atomic Design 〜堅牢で使いやすいUIを効率よく設計する〜 を読みました。

本記事について

この記事では、本の内容の具体的な部分には触れず、各章を抽象的にまとめています。

この本を読んだ理由

弊社では、フロントの開発はReactでAtomic Designを用いて開発しています。
普段ReactやTypeScriptの勉強に追われてしまって、新規開発のときにUIや今作っているコンポーネントをどこに書くべきか?などの理解を曖昧にしています。
デザイナーさんから上がってきたデザインを見たときにコンポーネント・ベースの設計がシュッとできるようになりたいので、この本で学ぶことにしました。

対象読者

  • ユーザーが使いやすいサービスをより少ない工数で作りたい
  • UI開発を効率的に行いたい Webフロントエンジニア

この本で学べること

第1章 UI設計における現状の問題を振り返る

1章では、タイトルの通りUI設計における問題について振り返ります。

  • UIが良いとユーザーがどう感じるのか。
  • 逆にUIが悪いと、ユーザーはどんなことを感じるのか。
  • 直感的なUIの定義
  • CSSの取り扱いの難しさ
  • デザイナーが存在しない現場
  • SPAの普及について

問題点については、共感できるものが多く的確に言語化していただいてました。
私自身、CSSの取り扱いの難しさや、SPAの考え方を理解するのにとても時間がかかりました。

第2章 コンポーネント・ベースのUI設計

2章では、コンポーネント・ベースでUIを開発するメリットやコンポーネント設計について書かれていました。

メリットとして、

などが挙げられていました。

コンポーネント設計のポイントのところで印象に残ったのが、「単一責任の原則」(1コンポーネントが持つ責任は1つにする)と「関心の分離」(担当する機能を目的別に分類すること)です。
どの単位でコンポーネントを分けるか迷うので、一つ指標になると思いました。

また、「HTMLやCSS, JavaScriptコンポーネント化が苦手で、それを手助けしてくれるのがReact」という話は二年前くらいに読みたい文章でした。

第3章 Atomic DesignによるUIコンポーネント設計

Atomic Designは、デザインフレームワークで、UIデザインの関心ごとを階層化します。

Templatesは画面全体のレイアウト、Organismsはユーザーの行動を促すコンテンツ、Moleculesは行動を阻害しない操作性、Atoms はデザインの統一性だそうです。デザイナー目線で考えたことがなかったので、この分類の仕方は目から鱗でした。
特にMoleculesとOrganismsの違いや、Templates とPageの違いはかなり曖昧にしていたので、知見が溜まりました。
本書にとても詳しく具体例が載っているので、ぜひ読んでみてください。

ちなみにどうして化学用語と開発用語が混ざっているのかちょっと気になっていたのですが、以下のような豆知識もコラムとして記載されていました。

Atoms、Molecules、Organisms、と化学用語が続いた後で、Templates、Pagesと通常の開発用語が登場することです。ここには、「開発者だけで使う用語」と「開発者以外に対しても使う用語」という区別が表現されています。つまり、化学用語のAtoms、Molecules、Organismsというコンポーネントの概念は開発者だけが認識していればよいものであり、一般的な用語のTemplatesとPagesについては、開発者以外の経営者やクライアントと話す際にも説明する必要がある概念だという意味が込められています。

へぇ〜。

第4章 UIコンポーネント設計の実践

4章は、実践編です。あるページのデザインが与えられ、実際にAtomicDesignで分割していきます。

  • Atoms、Molecules、Organisms、Templates、Pagesに実際に分割する
  • 分割する際のポイントや基準の解説

 - Atomsは抽象度が高いコンポーネントにするよ
 - ロジックと表示に関する責任は分離するよ(諸説あり)
 など

などなど、実際に実務で使える知識が満載でした。

エンジニアには馴染みのない、アプリケーションのデザインの統一性の話など、デザイン観点での解説もとても有意義だと思いました。
一方で、SFCやHOCに関する話題が載っていたので、Reactに関する情報は少し古いと感じました。

第5章 UIコンポーネントのテスト

5章は、コンポーネントのテストについてです。
フロントのテストに関する、テストのポイントや用語がたくさん載っていて、とても勉強になりました。

  • UIが適切に分かれていれば、テストが簡単になる
  • Enzymeを使ってインタラクションテストをする

  インタラクションとは、ユーザーのアクションを適切にUIが処理しているかどうかのテスト。

 視力や聴力の違いのような身体的特徴による差異だけではなく、ユーザーが一時的に片手でスマートフォンで利用している場面やネットワーク帯域が狭い場所でも利用している場面などでも利用しやすくすること。

第6章 現場におけるコンポーネント・ベース開発のポイント

6章では、エンジニア以外の人を巻き込んだAtomic Designの使い方について説明されていました。

  • エンジニアとデザイナーで問題解決の方法が違う
  • そのすれ違いをフレームワークを使ってどのように解決するのか
  • コンポーネントリストを作ってエンジニア以外の人も触れる環境を作る

などです。

感想

この本を読んで一番よかったなと思ったのは、デザイナーさん視点の話が多く書かれていたことです。
デザイナーさんの問題解決がエンジニアとは異なることや、どんなことを考えてデザインをされているのか少し理解できました。

「Atomic Designって、Atoms、Molecules、Organisms、Templates、Pagesにいい感じに分割して再利用が楽になるんでしょ?」くらいの認識しかなかったのですが、このフレームワークを使うメリットを知ることができました。
特に、先ほども書いたのですが、MoleculesとOrganisms、TemplatesとPagesは分割を曖昧にしていたので、実践的に学ぶことができてよかったです。

これからAtomic Designを使用したReactの開発をされる方には、オススメの1冊です!

ここまで読んでくださりありがとうございました。

Typescript exercises やってみた 6問目

みなさんこんにちは。宮水です。
今日は、TypeScript エクササイズの6に取り組んでみました。
翻訳はめんどくさくなったので、Google翻訳に突っ込みました。

こちらのリポジトリをforkして、cloneして取り組みます。
rootディレクトリで、yarn installしてから問題文にあるRun this exerciseのコマンドを叩くと、答え合わせができます。すごい!!
github.com

本日の問題

import chalk from 'chalk';

/*

Intro:

    Filtering was completely removed from the project.
    It turned out that this feature was just not needed
    for the end-user and we spent a lot of time just because
    our office manager told us to do so. Next time we should
    instead listen to the product management.
 
 フィルタリングはプロジェクトから完全に削除されました。
    この機能はエンドユーザーには必要ないだけで、オフィスマネージャーから指示されただけで多くの時間が費やされたことがわかりました。次回は代わりに製品管理に耳を傾ける必要があります。

    Anyway we have a new plan. CEO's friend Nick told us
    that if we randomly swap user names from time to time
    in the community, it would be very funny and the project
    would definitely succeed!

    とにかく新しい計画があります。 CEOの友人であるニックは、コミュニティで時々ユーザー名をランダムに入れ替えると非常に面白くなり、プロジェクトは確実に成功すると語っています

Exercise:

    Implement swap which receives 2 persons and returns them in
    the reverse order. The function itself is already
    there, actually. We just need to provide it with proper types.
    Also this function shouldn't necessary be limited to just
    Person types, lets type it so that it works with any two types
    specified.

    2人を受け取り、逆の順序で返すスワップを実装します。関数自体は、実際にはすでに存在しています。適切なタイプを提供する必要があるだけです。
    また、この機能は必ずしも必要ではありません。
    人物のタイプを入力して、指定した2つのタイプで機能するようにタイプします。

Run:

    npm run 6

    - OR -

    yarn -s 6

*/

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

function logUser(user: User) {
    const pos = users.indexOf(user) + 1;
    console.log(` - #${pos} User: ${chalk.green(user.name)}, ${user.age}, ${user.occupation}`);
}

function logAdmin(admin: Admin) {
    const pos = admins.indexOf(admin) + 1;
    console.log(` - #${pos} Admin: ${chalk.green(admin.name)}, ${admin.age}, ${admin.role}`);
}

const admins: Admin[] = [
    {
        type: 'admin',
        name: 'Will Bruces',
        age: 30,
        role: 'Overseer'
    },
    {
        type: 'admin',
        name: 'Steve',
        age: 40,
        role: 'Steve'
    }
];

const users: User[] = [
    {
        type: 'user',
        name: 'Moses',
        age: 70,
        occupation: 'Desert guide'
    },
    {
        type: 'user',
        name: 'Superman',
        age: 28,
        occupation: 'Ordinary person'
    }
];

function swap(v1, v2) {
    return [v2, v1];
}

function test1() {
    console.log(chalk.yellow('test1:'));
    const [secondUser, firstAdmin] = swap(admins[0], users[1]);
    logUser(secondUser);
    logAdmin(firstAdmin);
}

function test2() {
    console.log(chalk.yellow('test2:'));
    const [secondAdmin, firstUser] = swap(users[0], admins[1]);
    logAdmin(secondAdmin);
    logUser(firstUser);
}

function test3() {
    console.log(chalk.yellow('test3:'));
    const [secondUser, firstUser] = swap(users[0], users[1]);
    logUser(secondUser);
    logUser(firstUser);
}

function test4() {
    console.log(chalk.yellow('test4:'));
    const [firstAdmin, secondAdmin] = swap(admins[1], admins[0]);
    logAdmin(firstAdmin);
    logAdmin(secondAdmin);
}

function test5() {
    console.log(chalk.yellow('test5:'));
    const [stringValue, numericValue] = swap(123, 'Hello World');
    console.log(` - String: ${stringValue}`);
    console.log(` - Numeric: ${numericValue}`);
}

[test1, test2, test3, test4, test5].forEach((test) => test());

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/basic-types.html#tuple
// https://www.typescriptlang.org/docs/handbook/generics.html

答え

function swap<A, B>(v1: A, v2: B): [B,A] {
    return [v2, v1];
}

感想

今回は、swap関数に型をつけるという問題でした。
v1にもv2にもUserかAdminが入る可能性があるので、困りました...。
f:id:kattyan53:20200729211309p:plain


そんなときは、ジェネリクスですね!!
4歳娘「パパ、具体的な名前をつけないで?」 - Qiita

swap< A, B > は関数の型
v1: A, v2: Bは引数の型
[B,A]は返り値の型です。

function swap<A, B>(v1: A, v2: B): [B,A] {
    return [v2, v1];
}

A, B→B,Aで型を反対にすることも表現できるとは、初めて知りました!


以上です!

Typescript exercises やってみた 5問目

みなさんこんにちは。宮水です。
今日は、TypeScript エクササイズの5に取り組んでみました。
英語も苦手なので、翻訳も自分でしてみました。

こちらのリポジトリをforkして、cloneして取り組みます。
rootディレクトリで、yarn installしてから問題文にあるRun this exerciseのコマンドを叩くと、答え合わせができます。
github.com

本日の問題

import chalk from 'chalk';

/*

Intro:

    Filtering requirements have grown. We need to be
    able to filter any kind of Persons.

    フィルタリングの要件が増えました。
    あらゆる種類のPersonをフィルタリングできるようにする必要があります。

Exercise:

    Fix typing for the filterPersons so that it can filter users
    and return User[] when personType='user' and return Admin[]
    when personType='admin'. Also filterPersons should accept
    partial User/Admin type according to the personType.

    filterPersons の入力を修正して、ユーザーをフィルタリングし、personType = 'user'のときにUser[]を返し、personType = 'admin'のときにAdmin[]を返すようにしてください。
    また、filterPersonsは、personTypeに応じて部分的なUser/Adminタイプを受け入れる必要があります。

Higher difficulty bonus exercise:

    Implement a function `getObjectKeys()` which returns proper type
    for any argument given, so that you don't need to cast it.

    let criteriaKeys = Object.keys(criteria) as (keyof User)[];
    -->
    let criteriaKeys = getObjectKeys(criteria);

Run:

    npm run 5

    - OR -

    yarn -s 5

*/

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

type Person = User | Admin;

const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    { type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
    { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
    { type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' },
    { type: 'user', name: 'Wilson', age: 23, occupation: 'Ball' },
    { type: 'admin', name: 'Agent Smith', age: 23, role: 'Anti-virus engineer' }
];

function logPerson(person: Person) {
    console.log(
        ` - ${chalk.green(person.name)}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
    );
}

function filterPersons(persons: Person[], personType: string, criteria: unknown): unknown[] {
    return persons
        .filter((person) => person.type === personType)
        .filter((person) => {
            let criteriaKeys = Object.keys(criteria) as (keyof Person)[];
            return criteriaKeys.every((fieldName) => {
                return person[fieldName] === criteria[fieldName];
            });
        });
}

let usersOfAge23: User[] = filterPersons(persons, 'user', { age: 23 });
let adminsOfAge23: Admin[] = filterPersons(persons, 'admin', { age: 23 });

console.log(chalk.yellow('Users of age 23:'));
usersOfAge23.forEach(logPerson);

console.log();

console.log(chalk.yellow('Admins of age 23:'));
adminsOfAge23.forEach(logPerson);

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/functions.html#overloads

私の答え

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

type Person = User | Admin;

const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    { type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
    { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
    { type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' },
    { type: 'user', name: 'Wilson', age: 23, occupation: 'Ball' },
    { type: 'admin', name: 'Agent Smith', age: 23, role: 'Anti-virus engineer' }
];

function logPerson(person: Person) {
    console.log(
        ` - ${chalk.green(person.name)}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
    );
}


function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Admin>): Admin[];
function filterPersons(persons: Person[], personType: 'user', criteria: Partial<User>): User[];

function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {
    return persons
        .filter((person) => person.type === personType)
        .filter((person) => {
            let criteriaKeys = Object.keys(criteria) as (keyof Person)[];
            return criteriaKeys.every((fieldName) => {
                return person[fieldName] === criteria[fieldName];
            });
        });
}

let usersOfAge23: User[] = filterPersons(persons, 'user', { age: 23 });
let adminsOfAge23: Admin[] = filterPersons(persons, 'admin', { age: 23 });

console.log(chalk.yellow('Users of age 23:'));
usersOfAge23.forEach(logPerson);

console.log();

console.log(chalk.yellow('Admins of age 23:'));
adminsOfAge23.forEach(logPerson);

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/functions.html#overloads

感想

今回は、オーバーロードの問題でした。
同じ名前のメソッドでも、引数が違うか、引数の型が違えば定義できるというものです。
中身も同じ関数を書かないといけないと思ったのですが、型だけオーバーロードしてくれるんですね。

コンパイラーが型チェックできるからOKって意味だと思う...)

The answer is to supply multiple function types for the same function as a list of overloads. This list is what the compiler will use to resolve function calls. Let’s create a list of overloads that describe what our pickCard accepts and what it returns.

昨日、同じチームの先輩から助言を受けて、以下の部分は、

function filterPersons(persons: Person[], personType: 'admin', criteria: {[P in keyof Admin]?: Admin[P];}): Admin[];
function filterPersons(persons: Person[], personType: 'user', criteria: {[P in keyof User]?: User[P];}): User[];

function filterPersons(persons: Person[], personType: string, criteria: {[P in keyof Person]?: Person[P];}): Person[] {

Partial<型>で置き換えられるとと教えていただいて、使ってみました。

function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Admin>): Admin[];
function filterPersons(persons: Person[], personType: 'user', criteria: Partial<User>): User[];

function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {

以上です!

Typescript exercises やってみた 4問目

みなさんこんにちは。宮水です。
今日は、TypeScript エクササイズの4に取り組んでみました。
英語も苦手なので、翻訳も自分でしてみました。

こちらのリポジトリをforkして、cloneして取り組みます。
rootディレクトリで、yarn installしてから問題文にあるRun this exerciseのコマンドを叩くと、答え合わせができます。すごい!!
github.com

前回の問題

miyamizu.hatenadiary.jp

本日の問題

import chalk from 'chalk';

/*

Intro:

    Time to filter the data! In order to be flexible
    we filter users using a number of criteria and
    return only those matching all of the criteria.
    We don't need Admins yet, we only filter Users.

 データをフィルタリングする時間です!柔軟にするために、いくつかの基準を使用してユーザーをフィルタリングし、すべての基準に一致するユーザーのみを返します。まだ管理者は必要ありません。ユーザーのみをフィルタリングします。

Exercise:

    Without duplicating type structures, modify
    filterUsers function definition so that we can
    pass only those criteria which are needed,
    and not the whole User information as it is
    required now according to typing.

 Type構造を複製せずに、filterUsers関数の定義を変更し、ユーザーの情報全体ではなく必要な基準のみ渡すことができるようにします。

Higher difficulty bonus exercise:
    Exclude "type" from filter criterias.


めっちゃむずいエクササイズ:
    フィルター基準から「タイプ」を除外します。

Run:

    npm run 4

    - OR -

    yarn -s 4

*/

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

type Person = User | Admin;

const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    {
        type: 'admin',
        name: 'Jane Doe',
        age: 32,
        role: 'Administrator'
    },
    {
        type: 'user',
        name: 'Kate Müller',
        age: 23,
        occupation: 'Astronaut'
    },
    {
        type: 'admin',
        name: 'Bruce Willis',
        age: 64,
        role: 'World saver'
    },
    {
        type: 'user',
        name: 'Wilson',
        age: 23,
        occupation: 'Ball'
    },
    {
        type: 'admin',
        name: 'Agent Smith',
        age: 23,
        role: 'Administrator'
    }
];

const isAdmin = (person: Person): person is Admin => person.type === 'admin';
const isUser = (person: Person): person is User => person.type === 'user';

function logPerson(person: Person) {
    let additionalInformation: string = '';
    if (isAdmin(person)) {
        additionalInformation = person.role;
    }
    if (isUser(person)) {
        additionalInformation = person.occupation;
    }
    console.log(` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`);
}

function filterUsers(persons: Person[], criteria: User): User[] {
    return persons.filter(isUser).filter((user) => {
        let criteriaKeys = Object.keys(criteria) as (keyof User)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

console.log(chalk.yellow('Users of age 23:'));

filterUsers(
    persons,
    {
        age: 23
    }
).forEach(logPerson);

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#predefined-conditional-types

"私の"答え

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

type Person = User | Admin;

const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    {
        type: 'admin',
        name: 'Jane Doe',
        age: 32,
        role: 'Administrator'
    },
    {
        type: 'user',
        name: 'Kate Müller',
        age: 23,
        occupation: 'Astronaut'
    },
    {
        type: 'admin',
        name: 'Bruce Willis',
        age: 64,
        role: 'World saver'
    },
    {
        type: 'user',
        name: 'Wilson',
        age: 23,
        occupation: 'Ball'
    },
    {
        type: 'admin',
        name: 'Agent Smith',
        age: 23,
        role: 'Administrator'
    }
];

const isAdmin = (person: Person): person is Admin => person.type === 'admin';
const isUser = (person: Person): person is User => person.type === 'user';

function logPerson(person: Person) {
    let additionalInformation: string = '';
    if (isAdmin(person)) {
        additionalInformation = person.role;
    }
    if (isUser(person)) {
        additionalInformation = person.occupation;
    }
    console.log(` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`);
}

function filterUsers(persons: Person[], criteria: {
    [P in keyof User]?: User[P];
  }): User[] {
    return persons.filter(isUser).filter((user) => {
        let criteriaKeys = Object.keys(criteria) as (keyof User)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

console.log(chalk.yellow('Users of age 23:'));

filterUsers(
    persons,
    {
        age: 23
    }
).forEach(logPerson);

解説&感想

正直、今回は自信ないです...。

今回は、User型なのにage以外のプロパティ忘れてるで!というエラーでした。
f:id:kattyan53:20200726020451p:plain

しかし、検索条件なので必要なプロパティだけ渡して使いたいとのことです。

filterUsers関数の定義を変更し、

とあるので、変更するのはfilterUsers関数の定義。
公式ドキュメントのMapped typesをヒントに、以下のコードを読んで、次のように書きました。

// Use this:
type PartialWithNewMember<T> = {
  [P in keyof T]?: T[P];
} & { newMember: boolean }
function filterUsers(persons: Person[], criteria: {
    [P in keyof User]?: User[P];
  }): User[] {
    return persons.filter(isUser).filter((user) => {
        let criteriaKeys = Object.keys(criteria) as (keyof User)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

これで、既存のUser typeのkey情報を再利用しつつ、Pという新しいtypeを定義したつもりです。


4でこの難しさ、恐ろしい... 
以上です!