宮水の日記

宮水の日記

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

「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でこの難しさ、恐ろしい... 
以上です!

Typescript exercises やってみた 3問目

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

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

前回の問題

miyamizu.hatenadiary.jp

本日の問題

import chalk from 'chalk';

/*

Intro:

    As we introduced "type" to both User and Admin
    it's now easier to distinguish between them.
    Once object type checking logic was extracted
    into separate functions isUser and isAdmin -
    logPerson function got new type errors.

 UserとAdminの両方に「type」を導入したので、簡単にそれらを区別できるようになりました。
 オブジェクトのtypeをチェックするロジックがisUserとisAdminに分けられると、logPerson関数で新しいタイプのエラーが発生しました。

Exercise:

    Figure out how to help TypeScript understand types in
    this situation and apply necessary fixes.

 TypeScriptが型を理解するために、必要な修正を適用してください。

Run:

    npm run 3

    - OR -

    yarn -s 3

*/

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' }
];

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

function isUser(person: Person) {
    return 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}`);
}

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

console.log();

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

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates

答え

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' }
];

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}`);
}

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

console.log();

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

解説&感想

今回は、isの問題でした。
自分の経験では解けなくて、問題文一番下にある公式ドキュメントのヒントを見ました。
www.typescriptlang.org

TypeScriptが引数で受け取ったの型を推測できない場合、私たちがこうやって書いてTypeScriptを助けてあげる必要もあるんですね。

// このpersonはAdminだよ!
const isAdmin = (person: Person): person is Admin => person.type === 'admin';

公式ドキュメントの同じ項目にinというものもあって、これは逆に"swim"はFishかBirdのプロパティだよ!と教えてあげることができます。
isとはちょっと違うけど、型を限定できるというものです。

function move(pet: Fish | Bird) {
  if ("swim" in pet) {
    return pet.swim();
  }
  return pet.fly();
}

うーん、だんだん難しくなってきました。以上です!

Typescript exercises やってみた 2問目

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

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

前回の問題

miyamizu.hatenadiary.jp


本日の問題

import chalk from 'chalk';

/*

Intro:

    Since we already have some of the additional
    information about our users, it's a good idea
    to output it in a nice way.

 私たちはすでにいくつか情報を追加されたユーザーを持っているので、適切な方法で出力しましょう。

Exercise:

    Fix type errors in logPerson function.

    logPerson function should accept both User and Admin
    and should output relevant information according to
    the input: occupation for User and role for Admin.

 logPerson関数の型エラーを修正してください。
 logPerson関数は、UserとAdminの両方を受け入れ、Userの職業とAdminの役割を出力する必要があります。

Run:

    npm run 2

    - OR -

    yarn -s 2

*/

interface User {
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    name: string;
    age: number;
    role: string;
}

type Person = User | Admin;

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

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

persons.forEach(logPerson);

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-the-in-operator

答え

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'
    }
];

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

persons.forEach(logPerson);

んー、ちょっと微妙なコードだと思うけど、一応動きました。
f:id:kattyan53:20200724163641p:plain

感想

今回は、Type Guardの問題だと思います。

今回の問題では、User型にroleプロパティが、Admin型にoccupationプロパティがないという型エラーが出ていました。
(ちなみに、前回の問題では、roleもoccupationも参照していなかったので、エラーにはなりませんでした。)

そこで、型定義にuser か adminかわかる情報を追加してあげて、データにもtypeプロパティを追加し、
if文でtypeプロパティごとに出力する値を変えるような処理を入れました。
この問題はいろんな書き方ができると思います。

初めは、typeofやinstanceofをそれを使おうと思ったのですが、使えませんでした。

typeofは、stringやnumberやnullといった基本型に対して使えるものです。
instanceofは、interfaceで定義された型に対しては使うことができません。

interfaceのタイプガードをするとしたら、以下のように自分でタイプガードを定義しないといけないようです。(ユーザー定義タイプガードというそうです。)

interface Foo {
  foo: number
}

interface Bar {
  bar: number
}

// ユーザ定義タイプガード
function implementsFoo(arg: any): arg is Foo {
  return arg !== null &&
    typeof arg === "object" &&
    typeof arg.foo === "number"
}

function getNumber(arg: Foo | Bar): number {
  if (implementsFoo(arg)) { // implementsFooをタイプガードに使用
    return arg.foo
  } else {
    return arg.bar
  }
}

※ 参考のQiitaから引用しました

以上です!

参考:TypeScript: interfaceにはinstanceofが使えないので、ユーザ定義タイプガードで対応する - Qiita

「オブジェクト指向設計 実践ガイド」を読みました

今回は「オブジェクト指向設計 実践ガイド」を読みました。

この記事について

この記事では、本の具体的な内容は少なめにし、各章について抽象的にまとめています。
この本に興味のある人が、どんなことが学べるのか軽く理解できるきっかけになればと思います。

なぜ読んだのか

私は今までオブジェクト指向Javaで少し学んだきり、あんまり勉強してきませんでした。改めてRubyオブジェクト指向を学んで、Rubyらしいオブジェクト指向を身につけて実務に活かそうと思いました。
特に「変更に強いコード」が書けるようになりたいです。変更に強いコードとはどんなコードなのか、どういう風に書くのか学んでいこうと思います。

この本の対象者

この本で学べること

  • オブジェクト指向ソフトウェアの設計
  • 今日の生産性が翌月も翌年も持続するようなソフトウェアをいかに構成するか
  • 将来に適応できるコードの書き方

第1章 オブジェクト指向設計

1章では、手短にオブジェクト指向プログラミングの概要が説明されています。
設計原則やデザインパターンにも軽く触れ、設計が大切な理由や設計が失敗してしまう理由について解説されていました。

第2章 単一責任のクラスを設計する

この章では、自転車について様々な計算するアプリケーションを使って、単一責任のクラスの考え方を学びます。

自転車のギアの比を計算するだけのクラスが完成した。

ギアのインチも知りたくなったが、引数が変わってしまう。

attr_readerを使ってインスタンス変数を隠蔽し、データの参照を"振る舞い"へと変える。

計算の都合上、続いて直径が必要になるが、データ構造(配列の要素の順番)に依存してしまう。

Structを使って、クラスを定義することなく属性を一箇所に束ねる。

最後に自転車の車輪の円周を計算したいという要望がくる。Wheelクラスを作るときがきた!

今まで変更に強いクラスを作ってたから、変更も簡単だね!めでたしめでたし

という流れです。
クラスを"簡単に変更できる"とはどういう意味か?クラスにさせる振る舞いはどのように決めるべきか?などが理解できました。
本書では、コードを用いて解説されています。

第3章 依存関係を管理する

この章では、2章で取り扱ったGear(ギア)とWheel(ホイール)のコードを使って、依存関係の原因と分離の仕方について解説されます。

最初のコードは、Wheelクラスに変更があった場合、Gearも大きく影響してしまう状態

Gearクラスの中でWheelインスタンスを生成するのをやめ、Gearインスタンスを生成するときに生成したWheelインスタンスを引数として渡す。(と疎結合になる)

引数の順番への依存もよくないので、Hashを使用する。明示的にデフォルト値を設けるのもあり。

WheelがGearに依存しているコードではなく、GearがWheelに依存するコードも書ける。
するとWheel.newという具象的なコードに依存していたGearが、抽象的なものに依存するようになったよ。抽象的なものに依存することはいいこと。
静的型付け言語ではインターフェース、Rubyみたいな動的型付け言語ではダックタイピングって言うけど、使いすぎに注意してね。

まとめると、疎結合なオブジェクトは変更に対応しやすいし再利用しやすいし最高!

「このクラスを直したらあのクラスを直さないといけない」という状況を極力減らすことが大切です。

第4章 柔軟なインターフェースをつくる

4章では、オブジェクト間でどのように「柔軟なインターフェース」を作成するのか取り扱います。

自転車旅行会社のアプリケーションを題材に、シーケンス図の使い方とデメテルの法則について学べました。
デメテルの法則は、簡単にいうと「オブジェクトは自分のことを知るべきで、他のことは知りすぎないようにしよう」という法則です。
デメテルの法則は、ドットは1個までが望ましいという言い方もします。Rubyでは、delegate.rbとforwardable.rbがあります。

例えば、

customer.bicycle.wheel.rotate

のようなメッセージチェーンは、設計者の設計思想は既知のオブジェクトに影響を受けすぎています。
customerは自転車のホイールが回転することを知らなくても大丈夫です。customerは、自転車に乗ることができるとわかれば十分だということです。

customer.ride

第5章 ダックタイピングでコストを削減する

第5章では、4章に続き自転車旅行会社のコードでダックタイピングを扱う例が解説されています。

Tripクラスに定義された、旅の準備をする"prepareメソッド"は、準備をする人(クラス)ごとに準備の内容を異なるものにしたいです。

※ 日本語でコードを表しています

def prepare(preparers)
preparers.each { |preparer|
if preparer == Mechanic
自転車を用意する
elsif preparer == TripCoordinator
食料を買う
elsif preparer == Drivar
自転車の水のタンクを用意する
end
}
end

しかし、これだと各クラスへの依存が大きいです。そこで、ダックタイピングの出番です。
prepareメソッドが何をしたいかというと、ズバリ"旅の準備"。
コードを以下のように書き換えます。こうすると、新しく準備をする人(preparer)が増えてもTripのprepareメソッドに変更はありません。

def prepare(preparers)
preparers.each { |preparer|
preparer.prepare_trip(self)
}
end

Mechanicクラス
def prepare_trip(trip)
自転車を用意する
end

TripCoordinatorクラス
def prepare_trip(trip)
食料を買う
end

Drivarクラス
def prepare_trip(trip)
 自転車の水のタンクを用意する
end

このように、ダックタイピングを使えばオブジェクトが"何であるか"ではなく"何をするか"によって定義される、仮想の型を作ることができます。
ダックタイピングについてもう少し知りたくなったので、追加で ダック・タイピングがダメな理由 | GWT Center という記事も読みました。メリットデメリットが書かれていてとてもわかりやすかったです。

第6章 継承によって振る舞いを獲得する

この章では、クラスによる継承のテクニックを学びます。

自分が様々な自転車を扱う会社に勤めているという設定で、最初に自転車クラスを作るところから始めます。
マウンテンバイクとロードバイクというサブクラスを作ったり、リカンベント(首や腰に優しい自転車)自転車を追加したときに既存の自転車クラスが壊れることを体験しながら、どのように継承を扱っていくのか学びます。
superを使うより、親クラスのメソッドを子クラスでオーバーライドしたほうがいいということも学べました。(テンプレートメソッドパターンというそうです)
Rubyの親クラスと子クラスのメソッドが呼び出される順番について復習にもなりました。

第7章 モジュールでロールの振る舞いを共有する

6章では、継承を使って共通の処理を親クラスに移動させるやり方を学びました。一方で7章では、モジュールを使ってオブジェクトの役割を共有する方法を学びます。

本章では、整備士と自転車と自動車、そしてスケジュールクラスが登場します。
はじめに、整備士と自転車と自動車にはそれぞれ休みや、稼働可能かなどの"スケジュール"をスケジュールクラスに聞くようになっています。
しかし、スケジュールは"オブジェクト自身"が知っているべきなので、スケジュールをクラスではなくモジュール化して、整備士と自転車と自動車クラスでincludeしてオブジェクト自身にスケジュールを聞けるように改修していきます。
継承と違って、モジュールにするとどんなクラスにもメソッドを追加できるので便利ですね。

モジュールが追加された際のメソッド探索の順番にも少し触れられており、勉強になりました。

最後に、 モジュールを使うとRubyらしい便利なコードが書ける反面、継承を使った方がいいのか?モジュールを使った方がいいのか?迷うことがあります。そこで、7章の後半ではモジュールを使ったアンチパターンも紹介されていました。

第8章 コンポジションでオブジェクトを組み合わせる

8章の前半では、自転車をコンポーズしていき、コンポジションによってオブジェクトを組み立てるテクニックを説明されます。
コンポジションとは、組み合わされた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為です。
タイヤやサドル、ギアなど一つ一つは部品ですが、組み合わせることで一つの自転車となります。
今回も自転車を例にし、コンポジションについて理解します。

同じく8章の後半では、コンポジション、継承、ダックタイプによる役割の共有からいずれかを選ぶためのガイドラインが示されます。

継承について (整数は数字だ。浮動小数点数は数字だ)

例えばRubyのNumericクラスを継承するIntegerとFloatは二つとも、根本的には"数字"です。
「is-a」の関係のときには、継承を使う方が見通しがよく、合理的と言えます。
しかし、継承は間違ったケースに適用した場合に最悪です。簡単に振る舞いを追加できなくなってしまいます。

コンポジション(自転車はパーツを持っている)

コンポジションを使うと、小さなオブジェクトがたくさんできます。一つ一つが独立しているので、変更が容易にできます。
「has-a」の関係にはコンポジションを使うと良いでしょう。(Bicycle hava-a parts)
一方で、オブジェクト同士を組み合わせたときに、うまくいくかどうかは保証できません。書いた人の設計の腕次第になります。

ダックタイピング(準備は整備士のように振る舞う)

ダックタイピングは、「behaves-like-a」関係のときに使うのが好ましいです。
「〇〇は△△のように振る舞う」に当てはまる場合に使用します。
5章でやったコードを例にすると、準備は整備士のように振る舞う、準備は運転手のように振る舞う、などです。
ちょっと違和感ありますが、なんとなく意味は伝わります。

第9章 費用対効果の高いテストを設計する

最後は、テストについてです。意味のあるテストの書き方が学べます。
ここまで、オブジェクト指向を学ぶことにより"変更に強い"コードの書き方を学びました。ここで出てくるのが、リファクタリングです。

良いテストは、コードへの変更によってテストの書き直しが強制されないように書かれています。また、テストは唯一信用できる設計の仕様書となります。
privateメソッドのテストはしない、リスコフの置換原則、など知らないことがたくさん書いてあって、参考になりました。

まとめ

新しいオブジェクトを設計する際に変更に強いコードを書くための考え方を学ぶことができました。

今後は以下のような観点に気をつけながら設計していこうと思いました。

  • このオブジェクトの責任はなんだろう?
  • 依存関係はどうなっているだろう?
  • オブジェクトはどんな振る舞いをするべきか?
  • 振る舞いによっては、共有するべきか?(モジュールを使うべきか?)
  • 継承、ダック、コンポジションの使い分け
  • どんなテストを書くべきか(受信メッセージ/ 役割/ プライベートメソッド/ 送信メッセージ/ ダックタイプ/ 継承/ 振る舞い)

全体的に例が自転車で一貫していて、コードもわかりやすかったです。
翻訳にちょっと癖がありますが、読み進めていけば気にならなくなりました。

とてもおすすめなので、ぜひ読んでみてください。