パステル色な日々

気ままに綴るブログ

UIから考えるモデリング in Elm

Modelは大切

皆さんは何を考えながらモデリングしてますか?まずはElm Architectureを見てみましょう。

elm_architecture
Elm Architecture

ViewはModelに依存しUpdateもModelに依存しています。したがってModelの影響の大きさは言わずもがなです。

ちょっとしたModelの修正も影響範囲が多くて溜息をつくことは少なくありません。影響は少ないほうがいいですよね。

今回はモデリングについて考えてみましょう。

UIから考える

今回はこの動画にある方法を使ってモデリングをやってみようと思います。

youtu.be

リチャードさんはまずはUIから考えようと言っています。インライン編集可能なブログ記事のページを考えてみましょう。

blog_article_page
ブログ記事のページ

TitleとStoryという本文で構成されたブログ記事のページです。記事の作成者はインライン編集ができます。

ペンのアイコンをクリックすると右のページようなインライン編集状態になります。Saveボタンをクリックすると左のページのTitleが編集したものに置換されます。

みなさんもこのようなページをモデリングしてみてください。それでは次に私がモデリングしてみます。

type Article
    = Article { title : Title, story : Story } Property


type Property
    = ReadOnly
    | WriteTitle Title


type alias Title =
    String


type alias Story =
    String


-- MODEL

type alias Model =
    { article : Article
    }

Titleのインライン編集に着目してモデリングしてみました。皆さんのモデリングと見比べてどうでしたか?

それでは次のモデリングと見比べてみましょう。

type Property
    = ReadOnly
    | WriteTitle Title


type alias Title = String


type alias Story = String

-- MODEL

type alias Model =
    { title : Title -- READ/WRITE
    , story : Story -- READ/WRITE
    , property : Property -- READ/WRITE
    }

前者と後者は何が違うのでしょうか?後者はレコードの要素をいつでも操作することができます。

そのためPropertyがReadOnlyであってもtitleとstoryは変更できます。もちろんそのようにしないロジックを構築すれば防ぐことができます。

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ToggleEditTitle ->
            ( { model | property = WriteTitle model.title }, Cmd.none )

        SetTitle title ->
            case model.property of
                WriteTitle _ ->
                    ( { model | property = WriteTitle title }, Cmd.none )

                ReadOnly ->
                    ( model, Cmd.none )

        SaveTitle ->
            case model.property of
                WriteTitle title ->
                    ( { model | title = title, property = ReadOnly }, Cmd.none )

                ReadOnly ->
                    ( model, Cmd.none )

それでは前者の場合はどうでしょう。

module Article exposing (Article, empty, mapTitle, toggleTitle)


type Article
    = Article { title : Title, story : Story } Property


type Property
    = ReadOnly
    | WriteTitle Title


type alias Title =
    String


type alias Story =
    String


empty : Article
empty =
    Article { title = "", story = "" } ReadOnly


toggleTitle : Article -> Article
toggleTitle (Article edits property) =
    case property of
        ReadOnly ->
            WriteTitle edits.title
                |> Article edits

        WriteTitle title ->
            ReadOnly
                |> Article { edits | title = title }


mapTitle : (Title -> Title) -> Article -> Article
mapTitle modifier ((Article edits property) as article) =
    case property of
        ReadOnly ->
            article

        WriteTitle title ->
            WriteTitle (modifier title)
                |> Article edits
module Page.Article exposing (..)

import Article exposing (Article)


-- MODEL

type alias Model =
    { article : Article
    }

-- UPDATE

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ToggleEditTitle ->
            ( { model | article = Article.toggleTitle model.article }, Cmd.none )

        SetTitle title ->
            ( { model | article = Article.mapTitle (always title) model.article }, Cmd.none )

        SaveTitle ->
            ( { model | article = Article.toggleTitle model.article }, Cmd.none )

いかがでしょうか。モジュールを2つに分けてみました。

これならArticleはReadOnlyのときにtitleを変更できません。なぜならそのような関数がArticleモジュールから公開されてないからです。

また前述のパターンと異なり、WriteTitleコンストラクタも公開していません。このためArticleモジュール内部にArticleの実態は隠蔽されています。

この方法をOpaque Typeと呼びます。テクニックの一つですが動画では必須とまで言われています。

これで、Articleモジュールを編集してもUpdateやViewに影響が出ないように作ることができました。ネストが浅くなり多少見やすくなってないでしょうか。

また、title編集時の状態を WriteTitle Title と定義しているのもポイントです。

type alias Model =
    { title : Title
    , story : Story
    , editableTitle : Maybe Title
    , editableStory : Maybe Story
    }

少し定義を変えてみました。それではこの状態はとり得るのでしょうか。

type alias Model =
    { title : Title
    , story : Story
    , editableTitle : Just Title
    , editableStory : Just Story
    }

インラインで編集可能なのは一箇所だけとします。するとeditableTitleとeditableStoryの両方がこのような状態を取ることはありえません。

このようなアプリケーションにありえない状態を作り出すことが可能なモデリングを避けることでバグの少ないアプリケーションを構築できます。不可能な状態はモデリングで防ぐことができます。

youtu.be

サマリ

  • UIから考えることでモデリングができる
  • Oppaque Typeで変更に強いモデルを作ることができる
  • 不可能な状態 (Impossible state) を排除することができる

所感

またもや動画からモデリングの方法の一部を紹介しました。Elmでアプリケーション開発をしているとモデルを変更する機会は多々訪れます。

そのたびにModel, Update, Viewのすべてを変更するのはとても大変です。今回の方法を用いてなるだけ変更の影響がもれないように気をつけてみてください。

しかし、モデリングのポイントはこれだけではありません。UI StateとDomainをどのようにモデリングするのかという話題もあります。

フロントエンドでDDDをしようものならもっと複雑になることでしょう。一概にこれが正解というわけではないので皆さんの課題に沿った最善の方法を模索していきましょう。

そしてどんどん私に教えてください。 (違

Elmのモジュールを作るときはどういうとき

モジュールを作るとき

youtu.be

いきなりですがこの動画を見てください。この動画はElmの作者のEvanさんによる発表です。

まず見て驚くのはElmの場合、一つのファイルに対してプログラムを育てていくことではないでしょうか。通常システムを作るときには責務を切り分けて責務ごとにモジュールを作成することが結果的にファイル分割につながっていました。

Elmでも同様にElm ArchitectureによりModel, Update, Subscribe, Viewにシステムが分割されています。しかし単一の責務ごとにモジュールを作成することなく一つのファイルにアプリケーションを構築していきます。

この発表いわく通常それは400から1000行くらいのコードまで成長するそうです。そのあたりからモジュール作成の兆しが訪れることが述べられています。

Modelの構築の仕方に習っていくと新しいCustom Typeを作る機会が訪れます。まずはそのCustom Typeを作成して、そのCustom Typeにまつわる関数を作ります。

こうしてできたCustom Typeとそれにまつわる関数を別モジュールに切り出します。これがElmにおけるモジュール作成のタイミングと作成方法です。

このことはドキュメントにまとまっています。

Modules · An Introduction to Elm

モジュールの変更による影響範囲を閉じ込める

youtu.be

またいきなりですがこの動画を見てください。この動画はEvanさんの同僚NoRedInkで働くRichardさんの発表です。

動画で述べられている通りModelに依存しているシステムは多いのでModelの変更は影響が多岐にわたることが多いです。これをModel内の変更に留めるにはどうするといいでしょうか?

積極的にOpaque Typeを使うことを主張しています。Opaque Typeについては下記の記事が参考になります。

Advanced Types in Elm - Opaque Types – Charlie Koster – Medium

Elm PackagesのAPI Documentにも記述があります。

Design Guidelines

適切なインターフェース (関数) を作成し、exposingで公開するものを絞り込むことで変更をモジュールに閉じ込めることができます。

module Post exposing (Post, fromDoc)

type Post
  = Post doc
  
  
fromDoc : doc -> Post
fromDoc =
  Post

やりすぎないこと

Evanさんはやりすぎないことを注意としています。Custom Typeによるモジュール化を進めると最初のうちから再利用を意識して同じロジックをまとめたがります。

そんなときには一呼吸おいて問題が起きてから対処するようにしましょう。例えばページによって同じCustom Typeでもデータ構造が異なることがあります。

module PageX exposing (..)

type Doc
  = Doc Title Content

type alias Title = String

type alias Content = String
module PageY exposing (..)

type Doc
  = Doc
    { title : String
    , content : String
    , description : String
    }

早い段階から共通化を見越して過剰に手を加えるのではなく実際に重複するロジックが出てくるまで待ってみましょう。

共通したデータ構造があるのならコードを改善してシェアしましょう。このようにElmでは作っていく最中に発見してリファクタリングを積極的に繰り返すことを勧めているように感じます。

それは強力な型と言語設計によりリファクタリングをしやすい環境が整っているからでしょう。

サマリ

  • Elmでは必要な時が来るまでモノリシックなモジュールでアプリケーションを育てていく
  • カスタムタイプが増えてヘルパー関数ができたときがモジュールを作るサイン
  • モジュールはカスタムタイプに対する責任を持つ
  • Opaque Typeを積極的に使い影響範囲をなるべくモジュール内に限定する
  • 過度なモジュール化はすべきではないリファクタリングすることを念頭に置く

所感

今回はElmのモジュールについて記事にしました。Elmではこのような思想がありますが、一つのファイルで1000行もコードを書いていてはチーム開発をしているとコンフリクトが多発しそうですね。

また、モジュールの作り方にこのような指針がないとチームで開発ではこの関数はどのモジュールに属するのがいいのか迷ってしまうシーンがあります。

JavaScriptとは異なるモジュールの作成プロセスですが、Elmではこのようなプロセスをおすすめしていることを紹介しました。

Elmはどうやって超小さいアセットを実現したのだろう

Elmはとても小さいアセットを作ることができる

We had a community member working on a 49,315 line application try the new version and he got 114kb after compilation, minification, and gzip. That is in the ballpark of the 100kb produced by the 2000 lines in the Vue implementation!

blog/small-assets-without-the-headache

リンク先の画像が示すように、Elmはリリースされた0.19で十分に小さいJavaScriptファイルを生成することができるようになりました。RealWorldアプリケーションでの検証結果ですが、ひと目で主要なフロントエンドのライブラリ利用時よりも小さくなっている事がわかります。

0.19リリース時に公開されたこのブログ記事を見て僕は「すごいなー」と感心しました。一体どうやってこの小さなファイルを生成することができるようになったのでしょう。

デッドコードを削除する

僕はしばしば課題に直面すると同じような課題に対してアプローチしているライブラリがないか探す事があります。JavaScriptにはNPMという素晴らしいシステムがあるので、簡単にプロジェクトにライブラリを導入できます。

しかし、ライブラリのコードをすべて利用することは殆どないのです。ライブラリが公開しているAPIはどれも魅力的なものですが課題を突破するためにはいくつかのAPIで事足りる事が多いからです。

デッドコードはこうしたときに生まれます。利用してないコードがあってもライブラリをインストールした以上、ビルド時には無視できない存在となっています。もちろんライブラリだけではなくプロジェクト内のコードだって同じように考えることができるでしょう。

巨大なプロジェクトでは利用されてないコードが生まれることを見落としていることもあるかもしれません。webpackに代表されるようにツリーシェイキングによってこのようなデッドコードは削ぎ落とされることになりますが、完璧に削ぎ落とせてないそうです。

どうやらwebpackはモジュール単位でルーチンを走らせており、モジュールの粒度が大きくなればなるほど利用してないコードが紛れ込む可能性が高くなります。直接APIを呼んでない場合でも別のルーチンから呼ばれている場合もあるためライブラリが増えれば増えるほどデッドコードは増加する傾向にあります。

対策としてlodashに代表されるように関数ごとにモジュールを分ける方法があるものの、JavaScriptではどのモジュールでも実行時に関数の変更ができるので今呼ばれてないからと言って簡単に削除することはできないのが実情です。

しかし、Elmの関数は実行時に再定義と削除ができないため、メイン関数から到達可能であれば残して到達できなければ削除することができます。これがElmがアセットを小さく保つことができた1つ目の理由です。

また、Elmの場合はNPMとは異なり、package.elm-lang.orgでライブラリを公開しており、ここで公開されているライブラリはすべてElmで書かれているので安心して同じように削除できるのです。

レコードフィールドをリネームする

Elmにおけるレコードとは次のようなものです。

student =
  { name = "Haruka"
  , age = 17
  }

フィールドとはnameageを指していいます。これを参照する場合はstudent.nameとなりますが、0.19ではコンパイルの前後でs.nとリネームされるのです。

驚くべきことにこれで5% ~ 10%縮小するそうです。やはりこれもライブラリを含めたエコシステム全体で機能できます。

サマリ

  • Elmは0.19で主要なライブラリのどれよりも小さいJSファイルを生成する事ができる
    • デッドコードを削除する
    • レコードフィールドをリネームする

所感

非常に小さなアセットの生成が可能になった背景には言語の特性とその特性沿ったエコシステムがありました。一方でElm 0.19には多くの破壊的変更が含まれています。

compiler/0.19.md at master · elm/compiler · GitHub

コンパイラの最適化にはこのような変更を盛り込むしかなかったのでしょうか。Elmは純粋な関数型プログラミングの土壌を用意してくれていますが、JavaScriptに変換する際に問題を作り出してしまうこともあったのではないでしょうか。

変更の裏にはこうした課題を突破できないのでやむを得ず適用した変更したものもあるかもしれません。世界観を一貫して守りきる思想の強い言語という雰囲気を感じ取ることができました。それにしても破壊的変更が多くて大変です。

Elmのアプリケーションコードを読んでみよう

ElmアプリケーションはElmアーキテクチャーに沿って構築していきます。構築したアプリケーションコードはElmのコアコード上でアプリケーションとして振る舞います。

Elmのコアコードがどんなことをしているのか気になったので、今回はElmアプリケーションが起動するまでにどういうことをやっているのかを追っていきたいと思います。

起動呼び出し

Elmアプリケーションを起動するには以下の呼び出しを最初に行います。

var app = Elm.Main.fullscreen()

他にもいろいろ起動するために呼び出せる関数はありますが、とりあえずこちらから考えます。見て分かる通り Elm.Main オブジェクトを操作しているので、まずはこれがどういうオブジェクトなのか知ることから始めていきましょう。

Elm.Main

var Elm = {};
Elm['Main'] = Elm['Main'] || {};
if (typeof _user$project$Main$main !== 'undefined') {
    _user$project$Main$main(Elm['Main'], 'Main', undefined);
}

非常にシンプルなコードですね。_user$project$Main$main をみると、どうやらコンパイラモジュール名$関数名 という名前付けをしているようです。

Elm['Main'] にどういう変更が加えられるのか見ていきましょう。

makeProgram

function makeProgram(flagChecker)
{
    return F2(function(debugWrap, impl)
    {
        return function(flagDecoder)
        {
            return function(object, moduleName, debugMetadata) // == _user$project$Main$main
            {
                var checker = flagChecker(flagDecoder, moduleName);
                if (typeof debugMetadata === 'undefined')
                {
                    normalSetup(impl, object, moduleName, checker);
                }
                else
                {
                    debugSetup(A2(debugWrap, debugMetadata, impl), object, moduleName, checker);
                }
            };
        };
    });
}

_user$project$Main$mainobject, moduleName, debugMetadata を引数に取る無名関数みたいです。flagCheckercheckNoFlagscheckYesFlags が用意されています。

F2 は関数を受け取ってカリー化して返します。逆に A2 はカリー化された関数に引数を渡して評価する関数です。 debugWrap_elm_lang$virtual_dom$VirtualDom_Debug$wrap です。

implHtml.program に渡すおなじみのレコードです。見てわかるように Elm['Main']debugMetadata の有無で呼び出す関数が異なります。

今回は undefined でした。Html.program はこの関数の実行結果で実装が定義されます。

normalSetup

function normalSetup(impl, object, moduleName, flagChecker)
{
    object['embed'] = function embed(node, flags)
    {
        while (node.lastChild)
        {
            node.removeChild(node.lastChild);
        }

        return _elm_lang$core$Native_Platform.initialize(
            flagChecker(impl.init, flags, node),
            impl.update,
            impl.subscriptions,
            normalRenderer(node, impl.view)
        );
    };

    object['fullscreen'] = function fullscreen(flags)
    {
        return _elm_lang$core$Native_Platform.initialize(
            flagChecker(impl.init, flags, document.body),
            impl.update,
            impl.subscriptions,
            normalRenderer(document.body, impl.view)
        );
    };
}

ここで Elm['Main'] に変更が加えられます。これ以上、 Elm['Main'] に変更は加えてなさそうです。

initialize

function initialize(init, update, subscriptions, renderer)
{
    // ambient state
    var managers = {};
    var updateView;

    // init and update state in main process
    var initApp = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
        var model = init._0;
        updateView = renderer(enqueue, model);
        var cmds = init._1;
        var subs = subscriptions(model);
        dispatchEffects(managers, cmds, subs);
        callback(_elm_lang$core$Native_Scheduler.succeed(model));
    });

    function onMessage(msg, model)
    {
        return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
            var results = A2(update, msg, model);
            model = results._0;
            updateView(model);
            var cmds = results._1;
            var subs = subscriptions(model);
            dispatchEffects(managers, cmds, subs);
            callback(_elm_lang$core$Native_Scheduler.succeed(model));
        });
    }

    var mainProcess = spawnLoop(initApp, onMessage);

    function enqueue(msg)
    {
        _elm_lang$core$Native_Scheduler.rawSend(mainProcess, msg);
    }

    var ports = setupEffects(managers, enqueue);

    return ports ? { ports: ports } : {};
}

続けて fullscreen 呼び出しによる起動に至るまでの処理を眺めていきます。この関数はかなり重要そうです。

_elm_lang$core$Native_Scheduler.nativeBinding に渡されている関数はどちらも同じような内容です。おそらく view を更新して effectManagercmdssubs を渡して変更を通知しています。

_elm_lang$core$Native_Scheduler.nativeBinding を始めとする _elm_lang$core$Native_Scheduler モジュールの関数はElmアプリケーションのイベントループに大いに関係しています。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

nativeBinding が返すオブジェクトは後にenqueueされて処理されます。initApp は初期化に必要な処理、onMessage はメッセージを受け取った時に呼び出す処理だと推測できます。

setupEffectsdispatchEffects のための準備ではないでしょうか。もしかするとEffectManager関係の処理かもしれないです。ここで返しているオブジェクトは app に代入されますが利用してないですね。

spawnLoop

function spawnLoop(init, onMessage)
{
    var andThen = _elm_lang$core$Native_Scheduler.andThen;

    function loop(state)
    {
        var handleMsg = _elm_lang$core$Native_Scheduler.receive(function(msg) {
            return onMessage(msg, state);
        });
        return A2(andThen, loop, handleMsg);
    }

    var task = A2(andThen, loop, init);

    return _elm_lang$core$Native_Scheduler.rawSpawn(task);
}

一見するとこんがらがりそうなんですが、return 文に注目すると taskenqueue しているのがわかります。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

この task_elm_lang$core$Native_Scheduler.nativeBinding の仲間です。これは _elm_lang$core$Native_Scheduler.andThen で処理がラップされています。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

コールバック関数の loophandleMsg という task を設定して再度 andThen でラップしています。_elm_lang$core$Native_Scheduler.receive_elm_lang$core$Native_Scheduler.nativeBinding の仲間です。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

rawSpawn

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

processtask をラップしています。 processenqueue します。

work

キューに溜まった process を処理します。取り出して process.root があれば step 関数に渡します。process がなくなると活動を停止します。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

step

enqueue された processstep 関数で処理されます

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

enqueue された task をおさらいします。

process(andThen(loop, nativeBinding(task)))

順に処理していきますが、 loop が実行される気配がありません。 loopprocess.stack.callback に積まれたままです。

ちょっと謎ですね。 loop はいつ呼ばれるのでしょうか。

nativeBinding(task) の処理では登録していた関数を呼び出しています。ここで呼び出しているのは初期化処理をする関数です。

 var initApp = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
        var model = init._0;
        updateView = renderer(enqueue, model);
        var cmds = init._1;
        var subs = subscriptions(model);
        dispatchEffects(managers, cmds, subs);
        callback(_elm_lang$core$Native_Scheduler.succeed(model));
    });

renderernormalRenderer(document.body, impl.view) の実行結果です。

function normalRenderer(parentNode, view)
{
    return function(tagger, initialModel)
    {
        var eventNode = { tagger: tagger, parent: undefined };
        var initialVirtualNode = view(initialModel);
        var domNode = render(initialVirtualNode, eventNode);
        parentNode.appendChild(domNode);
        return makeStepper(domNode, view, initialVirtualNode, eventNode);
    };
}

normalRenderer では初期DOMの生成を行い、stepper 関数の作成を行います。

function makeStepper(domNode, view, initialVirtualNode, eventNode)
{
    var state = 'NO_REQUEST';
    var currNode = initialVirtualNode;
    var nextModel;

    function updateIfNeeded()
    {
        switch (state)
        {
            case 'NO_REQUEST':
                throw new Error(
                    'Unexpected draw callback.\n' +
                    'Please report this to <https://github.com/elm-lang/virtual-dom/issues>.'
                );

            case 'PENDING_REQUEST':
                rAF(updateIfNeeded);
                state = 'EXTRA_REQUEST';

                var nextNode = view(nextModel);
                var patches = diff(currNode, nextNode);
                domNode = applyPatches(domNode, currNode, patches, eventNode);
                currNode = nextNode;

                return;

            case 'EXTRA_REQUEST':
                state = 'NO_REQUEST';
                return;
        }
    }

    return function stepper(model)
    {
        if (state === 'NO_REQUEST')
        {
            rAF(updateIfNeeded);
        }
        state = 'PENDING_REQUEST';
        nextModel = model;
    };
}

makeStepper は 1フレームごとに実行される関数の登録を行う関数 stepper の作成を行います。virtualDOMの振る舞いを記述しているのもここになります。DOM差分を計算して適用している様子がわかります。

applyPatches では差分適用のほかイベントに対する関数の登録も行っています。renderer によって作成された関数 updateViewonMessage 関数の中で利用されています。

dispatchEffects では特に何もしないため最後にコールバック関数を呼び出します。

modelsucceed でラップしてものをコールバック関数は受け取り新たな process.root としています。同時に processenqueue しています。

succeedstep 関数で処理されます。process.stack.callback に登録した loop 関数がようやく実行されます。

が、特に何事もなく process は処理されて初期化処理は終了します。

まとめ

Elmアプリケーションの初期化処理でした。今回の重要登場人物は次のとおりです。

  • makeProgram ... デバッグの有無を判定
  • normalSetup ... fullscreen, embed を定義
  • initialize ... 初期化タスク定義と初期化実行
  • spawnLoop ... 初期化タスクの合成
  • rawSpawn ... processでラップ、キューにタスクを登録
  • work ... キューに溜まった process を処理
  • step ... タスク処理
  • makeStepper ... DOMの差分処理

流れもざっくり振り返りましょう。

Elmオブジェクト準備

  1. オブジェクトを作る
  2. normalSetup を呼んで fullscreen を定義する

初期化する

  1. initialize を呼ぶ
  2. spawnLoop で初期化タスクを作成
  3. rawSpawn を呼び、キューに追加
  4. work を呼び初期化タスクを順番に実行
  5. step を呼びタスクを処理、process.root を設定
  6. initApp に登録された初期化処理を実行
  7. 初期viewの追加

「超速! Webページ速度改善ガイド 使いやすさは「速さ」から始まる」を読んだ初学者の感想

今までパフォーマンスといえばSQLチューニングやらWebサーバのチューニングやらしかやってこなかったpastelIncです。 Webパフォーマンスについて必要に駆られなかったこともあり、勉強してなかったんですがSPA開発をしていて必要にかられたので表題の本で勉強してみました。

webperf.guide

なぜこの本だったのか

あほむさん (twitter: @ahomu) と煎茶さん (twitter: @1000ch) の存在が自分の中で大きかったからです。 以前から勉強会や記事、podcastでお見かけすることの多かったお二人です。なかでもパフォーマンスを語るシーンにおいては勉強になることも多く僕にとってのWebパフォーマンスの代名詞でした。そのお二人が書かれた本ということでこの本を手に取った次第です。

パフォーマンス初学者が読んでも良い内容か

間違いなく良いです。個人的には初学者が読んじゃいけない本なんてないと思ってます。興味があれば読む、それだけです。いきなり高度すぎたら少しずつ読み解いていけばよいのです。

超大満足

本書はそんなパフォーマンス初学者の僕にもうってつけな最高の本でした。読みながらただひたすらに「へぇ〜」って感じでした。 もう少し詳しく言うと本書は読み進めていくと「実際に自分でコード書いて確認したい〜」欲が溢れてきます。面白いです。

DevToolsとお友達になれる

Chrome DevToolsは Console タブをメインで使ってましたが、実はいろいろできることを本書で知りました。

パフォーマンスボトルネックの手がかりをつかむための基礎知識が分かる本

本書を読むとパフォーマンス調査をするための基礎知識が手に入ります。パフォーマンス調査は仮説 -> 検証の繰り返しになると思いますが、仮説を立てるときの材料が豊富に用意されています。勘で仮説を立てるシーンが減りそうな気がします。また、仮説を怠り検証ばかり繰り返しなぜかわからないけどうまくいった経験がある方いると思いますが、なぜうまくいったのかを知らなければ同じことの繰り返しになってしまいます。本書は推測するな、計測せよを掲げていきなり検証をするのではなくまずは計測することの大切さを訴えています。本書を読むとなぜ計測が大切なのか身にしみると思います。

非機能要件を勉強する必要あるのか

あると思います。機能要件は開発チームの誰でも割りとヘルプできると思いますが非機能要件となると途端に職人を必要とする現場は多いのではないでしょうか。非機能要件は学ぶことでエンジニアとして一つ尖る事ができると思うのです。 また、パフォーマンスを知りビジネスサイドとも話ができると一口にパフォーマンスを担保してほしいと言われた時に、どこまで出来て何を妥協してほしいのか説明できるようになります。お金を積んで解決する問題であれば訴えることも出来ますし、ビジネスサイドも天秤にかけることができます。結果としてプロダクトは継続して受け入れられやすくなるのではないでしょうか。

まとめ

買いましょう。読みましょう。おすすめです。

Salesforce大切なことはドキュメントに詰まってる

自分が初めてSalesforce開発で必要となった知識の参照元を並べてみます。

Lightning Componentの初期化時に何かしたい

Salesforce Developers

aura:attributeに入れる型って何があかんの?

Salesforce Developers

既存のJSライブラリを使いたいんだけど

Salesforce Developers

Lightning ComponentをLightningページで利用するための準備

Salesforce Developers

私のドメインって何?

Lightning ComponentをLightning アプリケーションビルダーで利用する時に必須となる

ヘルプ | トレーニング | Salesforce

何か参考にできるプロジェクトないの?

DreamHouse JP サンプルアプリケーション | Salesforce Developers

SOQLでリレーションクエリ作りたい

Salesforce Developers

sObjectの関係が知りたい

ER図があるぞ

Salesforce Developers

JavaScriptでのイベントの捌き方

Salesforce Developers

条件分岐するマークアップ書けるの?

aura:if

Salesforce Developers

Lightning アプリケーションと Lightning アプリケーションビルダーって違うの?

違う。Lightning アプリケーションは作ってもリリースする方法がわからなかったのでLightning アプリケーションビルダーを使ってLightning アプリケーションを作った。

Lightning Design System を使用した Lightning アプリケーションの作成 | Salesforce

コンポーネント間でJavaScriptコードを共有したいんだけど

静的リソースとして読み込んで使うしかないんや

Salesforce Developers

Lightning Componentの型情報ってApexではどうなるの

Lightning Components & AuraEnabled method parameters: What’s working and what’s not

sObjectってなにさ

Salesforce Developers

SOQLでの操作を一通り覚えたいんやけど

DML を使用したレコードの操作 単元 | Salesforce Trailhead

package.xml の書き方教えて

Salesforce Developers

開発環境とステージング環境作りたいんだけど

Sandbox利用しよう。DevHubがオンになっている組織ならスクラッチ環境を開発で使って、ステージングはSandboxでやるといいと思います。

Sandbox の詳細 単元 | Salesforce Trailhead

Salesforce Developers

VisualforceってLightning Componentと比べてどうなの

Visualforce と Lightning Experience | Salesforce Trailhead

クイックアクション、アプリケーションビルダーではまだサポートしてなかった

Lightning Component quick action not appearing in available actions for Lightning App page - Salesforce Stack Exchange

カスタム項目を必須項目にしたら全てのレコードタイプで必須扱いになる

全てのページレイアウトから削除できないので困るときには必須項目とせずにページレイアウト条で必須項目にしよう

ヘルプ | トレーニング | Salesforce

Apexサーバ側コントローラからエラーをLightning Componentに伝えたい

Salesforce Developers

一つのレコードを返すSOQLクエリ

Salesforce Developers

Apexのプリミティブ型には何があるの

Salesforce Developers

組み込みの例外

Salesforce Developers

Date型とDatetime型を変換する際にはタイムゾーンを意識しよう

Date型のフォーマット - KayaMemo

毎月の最終日の日付が知りたい

apex - How do we get the last day of the month from a given date - Salesforce Stack Exchange

Cronジョブみたいなのないの

ある

Apex スケジューラを使用したジョブのスケジュール 単元 | Salesforce Trailhead

サーバサイドのアクションを呼び出したい

Salesforce Developers

マークアップ内で式使いたい

Salesforce Developers

データバックアップの指針が知りたい

ヘルプ | トレーニング | Salesforce

外部APIを呼び出して使うけど気をつけることあるかな

ある

Salesforce Developers

リリース方法

Salesforce Developers

変更セットを使用した Sandbox からのリリース 単元 | Salesforce Trailhead

Standard Pricebookがテストケースで見つからないんだけど

Standard pricebook is not found in test class - Salesforce Developer Community

まとめ

一通り自分が初めてSalesforce開発で必要となった知識の参照元を並べてみました。 自分の言葉が少なめなのは補足の必要が無いからです。エラーが起きてもルールの範囲内であることが多く思いもしない物は少ないです。こういうところにプロダクトとしての素晴らしさがにじみ出てますね。

Apexコードを書くのに必要な知識は何

Apex

Apexは強く型付けされたオブジェクト指向プログラミング言語です。構文はJavaライクであり、Force.com プラットフォームサーバで処理を描く時に使用します。Apex を使うと、ボタンクリック、関連レコードの更新などのほとんどのシステムイベントにビジネスロジックを追加できます。

Lightning ComponentがクライアントサイドでApexがサーバサイドと言う理解でも良いと思います。 Lightning Componentからのリクエストをサーバサイドで処理できるようになります。 Lightning Componentから $A キーワードを利用することでエンドポイントの呼び出しができるので調べてみてください。

SOQL (Salesforce Object Query Language)

Force.com プラットフォームのデータベース操作に利用するクエリ言語です。SQLのような構文で書く事ができますが両者は違うものです。例えばSOQLでは SELECT * が出来ません。Salesforceはマルチテナント環境にありますが、マルチテナント環境では全員が「データベースを共有」しているようなものであるため、 * などのワイルドカード文字を使用すると問題が発生するからです。

ガバナ制限

これがApexコード格上での曲者。実はApexにはマルチテナント環境ゆえの制限事項が幾つかあります。

実行ガバナと制限 - Salesforce Developers

実行プロセスが共有リソースを独占しないように制限しており、超えた場合は 処理できない実行時例外 がthrowされるようになっています。何度も updateinsert 命令を発行したりすると簡単にSOQLの制限に引っかかってしまいます。 他にもforループで上記クエリを発行するのは絶対にやめましょう。

Apex開発で押さえておきたいポイント - Qiita

SOQLの制限で困ったときには実行したいトランザクションがどこからどこまでなのか把握してクエリを削減することに勤めましょう。トランザクションが分割可能であれば分割しましょう。

Apexトリガ

Salesforce のレコードに対するイベント (挿入、更新、削除) の前または後に処理できるApexプログラムです。 やたら操作の多いSalesforce上の仕事を自動化する際に利用できます。 これもガバナ制限には注意が必要です。特にトリガが原因で他のトリガを誘発してしまうことがあります。 例えば Opportunity のトリガは OpportunityLineItem の変更にも反応してしまうので、 OpportunityLineItem のトリガを作る際には Opportunity のトリガ内容にも注意しなくてはいけません。 先に書いてしまいましたがトリガは一つのsObject(Opportunity...etc.)に関連付けます。

Apex開発で押さえておきたいポイント - Qiita

Salesforce Developers

Apex一括処理

どうしてもガバナ制限を超えるような大規模ジョブを行う必要がある時にはApex一括処理を利用しましょう。

Apex 一括処理の内部的なしくみは次のとおりです。Apex 一括処理を使用して 100 万件のレコードを処理するとします。一括処理クラスの実行ロジックは、処理するレコードのバッチごとに 1 回コールされます。一括処理クラスを呼び出すたびに、ジョブは Apex ジョブキューに置かれ、別個のトランザクションとして実行されます。この機能には 2 つの大きな利点があります。

  • トランザクションは新しいガバナ制限のセットで開始されるため、コードをガバナ実行制限内に抑えやすくなります。
  • いずれかのバッチが処理に失敗しても、他のすべての正常に処理された一括処理トランザクションロールバックされません。

Apex 一括処理の使用 単元 | Salesforce Trailhead

単体テスト

Salesforceのリリース時には変更セットという機能を使ってリリースするのですが、この際には必ず単体テストが実行されます。 失敗すればリリースできないですし、 コードカバレッジ も75%なければエラー扱いとなりリリースできません。 なので単体テストは必ず書きましょう。テストしやすいように疎結合なコードを書くことももちろん大切ですよ。

まとめ

かなり簡素に大切なことだけ書きました。 逆にもっと細かいけどメモとして残していた知識がいくつかあるのでそちらは別途記事にします。