質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.34%

  • JavaScript

    17480questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • ECMAScript

    127questions

    ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

  • Lodash

    14questions

    Lodashは、JavaScriptのユーティリティライブラリ。Underscoreの派生ライブラリで、配列・オブジェクトの操作に便利です。また、コードの可読性も高めることができます。

よりスマートかつ高パフォーマンスに期待値を得たい

解決済

回答 1

投稿

  • 評価
  • クリップ 1
  • VIEW 195

teratailler

score 25

 前提・実現したいこと

※初心者ながら自分なりにコードを書いて目的(やりたいこと)は達成できています。

具体的なモデルがあったほうがわかりやすいと思うので、まずそちらを共有させていただきます。
以下サロン予約サイトです。
https://beauty.hotpepper.jp/slnH000314178/coupon/

サロン予約サイトに掲載されているサロンは複数のメニュー(子)を持っています。
さらに、そのメニューは、複数のオプション(孫)を持っています。このことから
サロンのメニューは複数のオプション(子)を持っている = サロンは複数のオプション(孫)を持っている
と定義出来るかと思います。
システム的なマスタデータは後者のサロンのオプションとなります。
データストアはRDBでなくNoSQLなので、冗長(非正規)な構成となり、参照カウント(何個のメニューに参照されているか)をマスタデータに持っています。
これがシステム上で扱うデータモデルの前提となります。

ここでイメージ図です。

  • サロンのメニューは複数のオプション(子)を持っている
サロン
 ├ メニューA
 │  ├ オプションA
 │  └ オプションB
 ├ メニューB
 │  ├ オプションA
 │  ├ オプションB
 │  └ オプションC
 ︙
  • サロンは複数のオプション(孫)を持っている
サロン
 ├ オプションA、参照カウント=2(2つのメニューに参照されている状態)
 ├ オプションB、参照カウント=2(2つのメニューに参照されている状態)
 └ オプションC、参照カウント=1(1つのメニューに参照されている状態)


オプションの具体的な例としては、カラー(髪を染める)というメニューであれば、金髪、茶髪等の選択肢が該当します。

そして、上記データモデルを前提としたシステム的な主要件は「メニュー登録・編集画面でオプションを追加・更新・削除できて、それがマスタデータと同期されていること」です。
つまり、メニュー登録・編集画面で以下のようなことを実現したいです。

  • サロンのオプションからオプションを選択して、メニューのオプションに追加できる
    その際、マスタデータの参照カウントをインクリメントする。
    ※今回こちらに該当するコードは除外しています
  • サロンのオプションに希望のオプションがなければ、メニューのオプションとして新規作成・追加できる。
    その際、新規作成したオプションを参照カウント=1としてマスタデータにも反映する。
  • メニューのオプションを更新できる。
    その際、マスタデータにその更新後の値が存在しなければ、メニューのオプションとして新規作成・追加し参照カウント=1としてマスタデータにも反映する。
  • メニューのオプションを削除できる。
    その際、他メニューから参照されていなければ、マスタデータからも削除する。他メニューから参照されていれば、マスタデータの参照カウントをデクリメントする。

あとは、ソースコード見ながらのほうが説明しやすいので、ところどころソースコード中にコメントを入れてます。

 発生している問題・エラーメッセージ

以下期待値を得るため、ライブラリ等使わずJavascriptのみでコーディングを試みましたが、悪戦苦闘した結果、ライブラリ(Lodash)を使うことにしました。

なんとか自力で書いた(自分の中で最善)コードを書きましたが、もっとスマートかつ高パフォーマンスに以下の期待値を得たいです。

周りにレビュアーがおらず、Teratailだけが頼りです。

// 期待値
const expectedSalon = {
    name: 'い~サロン',
    menus: [
        {
            name: 'カット',
            // サロンのメニューは複数のオプション(子)を持っている
            options: [
                {name: 'オプション1', price: 100},
                {name: 'オプション2', price: 200},
                {name: 'オプション3', price: 300}
            ]
            // ︙(省略)
        },
        {
            name: 'カラー',
            // サロンのメニューは複数のオプション(子)を持っている
            options: [
                {name: 'オプション1', price: 100},
                {name: 'オプション2', price: 222},
                {name: 'オプション3', price: 300},
                {name: 'オプション5', price: 500}
            ]
            // ︙(省略)
        }
    ],
    // サロンは複数のオプション(孫)を持っている
    menuOptions: [
        {name: 'オプション1', price: 100, refCount: 2},
        {name: 'オプション2', price: 200, refCount: 1},
        {name: 'オプション2', price: 222, refCount: 1},
        {name: 'オプション3', price: 300, refCount: 1},
        {name: 'オプション5', price: 500, refCount: 1},
    ]
    // ︙(省略)
};

 該当のソースコード

// 初期(更新前)データ
const salon = {
    name: 'い~サロン',
    menus: [
        {
            name: 'カット',
            // サロンのメニューは複数のオプション(子)を持っている
            options: [
                {name: 'オプション1', price: 100},
                {name: 'オプション2', price: 200},
                {name: 'オプション3', price: 300}
            ]
            // ︙(省略)
        },
        {
            name: 'カラー',
            // サロンのメニューは複数のオプション(子)を持っている
            options: [
                {name: 'オプション1', price: 100},
                {name: 'オプション2', price: 200},
                {name: 'オプション3', price: 300},
                {name: 'オプション4', price: 400}
            ]
            // ︙(省略)
        }
    ],
    // サロンは複数のオプション(孫)を持っている
    menuOptions: [
        {name: 'オプション1', price: 100, refCount: 2},
        {name: 'オプション2', price: 200, refCount: 2},
        {name: 'オプション3', price: 300, refCount: 2},
        {name: 'オプション4', price: 400, refCount: 1}
    ]
    // ︙(省略)
};

// カラーメニューを画面から更新(=Postされたデータ)
// 更新内容: オプション1はそのまま、オプション2を更新、オプション3とオプション4を削除、オプション5が2つ追加
const updatedColorMenu = {
    name:'カラー',
    options: [
        {name: 'オプション1', price: 100},
        {name: 'オプション2', price: 222},
        {name: 'オプション5', price: 500},
        {name: 'オプション5', price: 500}
    ]
    // ︙(省略)
}

// ↓↓↓↓↓以降がPost後の処理↓↓↓↓↓
// 追加されたオプションを抽出
const addedOptions = _.uniqWith(
    _.differenceWith(updatedColorMenu.options, salon.menuOptions, (a, b) => {
        return _.isEqual(
            a,
            _.omit(b, ['refCount'])
        );
    }
), _.isEqual);
console.log(addedOptions);

// 削除されたオプションを抽出
const removedOptions = _.differenceWith(salon.menuOptions, updatedColorMenu.options, (a, b) => {
        return _.isEqual(
            _.omit(a, ['refCount']),
            b
        );
    }
);
console.log(removedOptions);

// マスターとなるオプションを生成
let masterOptions = [];
// 削除されたオプションをマスターから削除
salon.menuOptions.forEach(x => {
    if (_.some(removedOptions, x)) {
        if (x.refCount > 1) {
            x.refCount = x.refCount - 1;
            masterOptions.push(x);
        }
    } else {
        masterOptions.push(x);
    }
});
// 追加されたオプションをマスターに追加
addedOptions.forEach(x => {
    x.refCount = 1;
    masterOptions.push(x);
});

console.log(masterOptions);

 試したこと

上記の通り、何とか自力で期待値と一致させることができました。

 補足情報(FW/ツールのバージョンなど)

Lodashを使っています。
余分なプロパティは可読性の観点から省略しています。
データストアはRDBでなくNoSQLです

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • mts10806

    2018/07/19 20:02

    達成基準が不明瞭なので回答も付きにくいと思いますよ。要件によってコード丸々書き直す必要が出てくるでしょうし。リファクタリング依頼となると「作業依頼」となり非推奨です。特に気になる箇所だけ抜き出してそこだけにフォーカスした質問にするとか、質問の仕方を工夫されたほうが良いです。

    キャンセル

回答 1

checkベストアンサー

+3

まずは概念的な所に触れます。
パフォーマンスだけ見るのであれば、Lodashは投げ捨ててfor...of文を回しましょう。

しかし、質問文と与えられた初期値を読む限り、初期のデータが変というか駄目なんじゃないですかね?
初期データが複雑で変な形式をしているから、
Lodashをこねくり回してアレやらコレやらのデータを突合させる必要がある。

変なデータをこねくり回すより、シンプルなデータを使って小学生でも出来るような計算でさっさと解決する。
こういうアプローチもとても重要ですので一度見直してみてください。

また、インクリメントといったワードが質問文に入ってきている所を見るに、
質問者さんのやりたいことはjQueryではなく、Vue.jsなんかのJSフレームワークを覚えれば一気に解決する可能性があります。
JSフレームワークを使っていれば表示の為に数値のインクリメントといった発想に至る事は殆どありません。


さて、質問文自体の回答に入っていきます。

わかり易さを考慮するのであればチェーン記法で書き直せる箇所は多いのでは?
参考サイト: Lodashのチェーン記法について - Qiita

differenceWithをチェーン記法に持ってこれるかどうかはちょっと怪しいのですが、
サンプルとしてaddedOptionsをチェーン記法にしてみました。
動作検証してみてください。

const addedOptions = _.chain(updatedColorMenu.options)
  .differenceWith(salon.menuOptions, (a, b) => _.chain(b)
    .omit(['refCount'])
    .isEqual(a)
    .value()
  )
  .uniqWith(_.isEqual)
  .value()
console.log(addedOptions);

※データが歯抜け過ぎるしネストされすぎで、回答文のコードは検証出来てません。
もし回答文のコードが誤っており、正確なコードが欲しい場合、A -> Bを記述するように質問文を修正してください。


質問文のコードはやりたいことが右に左に行ってしまう上、
カッコのネストで凄まじく読みづらいですが、
チェーン記法であればやりたい事が直列繋ぎにして書けるので、慣れればかなり読みやすくなります。
チェーン記法はLodashの存在意義と言えるでしょう、是非覚えて使いこなしましょう。

このメソッドチェーンで繋ぎまくるプログラミングは途中の計算結果が吸い出せず、
これホンマに合ってるんかな???でも検証し辛いし困った…となるかと思います。
その時はtapthruのメソッドが役に立つはずです。

const users = [
  {name: 'taro', score: 80},
  {name: 'jiro', score: 85},
  {name: 'saburo', score: 90},
];
const eighty = _.chain(users)
  .filter(user => user.score >= 80)
  // この時点の計算結果をconsole.logに吐き出して確認したいんだけど…
  .tap(console.log)
  .filter(user => user.score < 90)
  .value();
console.log(middle);

console.logは引数をコンソールに書き出してundefinedを返すメソッドです。
なので、メソッドチェーンで繋ぐプログラミングとはすこぶる相性が悪いのですが、
.tapという関数の中に自分を渡して起動させますが、戻り値は捨ててそのまま次で行くという投げっぱなしの処理を行う為のメソッドが存在します。
まさに計算途中の値をconsole.logalertで確認する為に作られたメソッドと言っても過言ではないでしょう。

.thru.tapの様に関数を発火させますが、変更後の戻り値を使って次の処理に進む関数です。
計算結果をちょっと補正したい時に威力を発揮するので使って見てくださいね。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/07/31 17:01

    ありがとうございます!
    データ設計を見直しつつ、教えていただいた記法を参考にさせていただきます。

    キャンセル

同じタグがついた質問を見る

  • JavaScript

    17480questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • ECMAScript

    127questions

    ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

  • Lodash

    14questions

    Lodashは、JavaScriptのユーティリティライブラリ。Underscoreの派生ライブラリで、配列・オブジェクトの操作に便利です。また、コードの可読性も高めることができます。