2024.12.19
システムの穴を運用でカバーしようとしてミス多発… バグが大量発生、決算が合わない状態から業務効率化を実現するまで
Redux w/ iOS(全1記事)
リンクをコピー
記事をブックマーク
杉上洋平氏:みなさんこんばんは。よろしくお願いします。Reduxについて本を書かせていただいたんですけれども、その中で本に付随してサンプルコードのアプリケーションをつけているんですけれども、本日はそちらのコードについて解説したいと思います。よろしくお願いします。
まず自己紹介ですけれども、杉上と申します。
Twitterのプロフィール写真がちょっと怖そうと言われるんですけど、そんなことはないので懇親会でも気軽に話しかけていただければと思うんですけれども。このアイコンで「@susieyy」ってアカウントでTwitterとかFacebook等やってますので、気軽にフォローください。
去年の4月からフリーランスとして活動してまして、証券会社のスタートアップであるFOLIOさんというところを去年の4月からお手伝いしております。メンバーは今日も来てくださってる松館さんだったり、それから岸川さん、西信さんというメンバーと一緒に日々開発をしております。
それでついに『iOS設計パターン入門』がですね……。今年の5月にクラウドファンディングを募らせていただきまして、そちらが達成し、著者6名がんばって書きまして、ついに電子版の正式リリースに至りました。パチパチパチ。
(会場拍手)
本当に著者の血と涙と汗が結晶となってみなさんにお届けできるかと思うのですけど、まだクラウドファンディングでお申し込みいただいたみなさんに電子版というかたちでリリースしている段階でして、クラウドファンディング以外の方は、製本して年末年始あたりにはご購入いただける状態になると思うので、まだお申込みできていない方はもうしばらくお楽しみにお待ちください。
著者6名、今日みなさんいらしているような気もするので、懇親会でもよかったらぜひ声をかけてみてください。こことかそことかあそことか。懇親会でも、ぜひ設計についてみなさんとお話ししたいなと思います。お願いします。
この設計の本はみなさんのお役に立ちたいなという思いで書いているんですけれども、設計の「正解」が載っているわけではないので、ぜひみなさんのアプリ開発において、チームだったりとかこういったコミュニティでディスカッション・議論するのに読んでいただいて、その出発点として活用いただければと思っております。
では、本題のほうに入っていきます。サンプルアプリケーションのコードを解説するにあたって、みなさんReduxについて馴染みのない方も多いので、まず最初に軽く「Reduxはこういうものだよ」というところをご紹介してから、そのアプリのデモと、それからXcodeを起動して、そのコードを見ながら実際のコードについて解説する流れでお話しいたします。
Reduxですけれども、まずは「なんでReduxが生まれたのか?」というところからお話しします。それを説明するにあたって、この画像がすごく的を射ているなと思います。
これなんだと思われます?
高速道路なんですけれども、世界にはこんな高速道路もあるんですね。立体的に入り組んで、走っていると「いったいどうやったらどこへ行けるんだろう?」というようなつくりになっているんですけれども、これ、日々のアプリケーションの中で感じたこと、このような複雑なアプリケーションの開発、あるいは「このコード複雑だな」って感じたこと、みなさん少しはあるんじゃないかなと思います。
ReduxはもともとWebですけれども、Webのアプリケーションも複雑になっていますし、iOSのアプリケーションも複雑になってきているという課題感があって、そのなかでとくに「状態が、いつ、どうして、どのように更新されていくのか?」というところをエンジニアが把握することが難しくなってきているような背景がありました。
そこでReduxの開発・著者であるDanさんが、この複雑という問題の最大のポイントは、この複雑性の根源として、変化(mutation)が非同期(asynchronicity)を伴う点にあると考えたんですね。
この2つが混じると「この状態がどんなふうに変わっていくんだろう?」というところが把握しにくくなると。
これを解決するために2つのアプローチを用いました。
1つ目は、ReduxはFluxのアーキテクチャというところからインスパイアされていて、それはFacebookが開発したアーキテクチャですが、イベントが発生して何か状態が変化するにあたって、その方向を1方向に制限するという特徴を持ったアーキテクチャです。これによって「いつ」「どのよう」に更新が起きるかどうかというところを明瞭にしています。
2点目は、Elmのアーキテクチャ。Elmってあまり聞き慣れないかもしれませんが、Webのアーキテクチャでして、関数型で、ロジックと、それからビューも関数で実装するアーキテクチャになっています。
そちらのアーキテクチャのポイントである純粋関数でロジックを記述し、副作用を排除して、かつデータをイミュータブルとして扱うと。それによって厳格な整合性のとれた状態管理を実現すると。
このFluxとElmの2つのアーキテクチャのいいところを組み合わせてこの課題を解決しようと、Reduxを開発しました。
Fluxはご存じの方が多いかと思うんですけれども、「ReduxはFluxの次」というか、「Fluxを改良してReduxができた」と思われている方もいるかもしれないんですけれども、Fluxの次世代版というわけではなくて、あくまでもインスパイアを受けているだけであって、いいところだけをもらって作られているので、どちらかというとFluxとReduxは並列に並ぶようなアーキテクチャの関係になっています。
この2点のインスパイアをもとにReduxが作られました。具体的にどういう仕組みで動いているかというところを説明しますね。
左側の人がユーザーで、あとViewがあります。右側が大きなReduxの状態を管理しているStoreというところになります。ユーザーが何かマウスクリックや画面のタップをしたときにイベントが発生すると、ViewではActionというものを生成します。
この生成したActionをStoreにDispatchします。このDispatchというのはStoreが持っている機能で、Storeに対してActionをDispatchします。
Storeでは状態を管理しています。Stateという三角のツリー構造になっているものが見えるかと思います。これがReduxに非常に特徴的なんですけれども、アプリケーション全体で1つの状態を管理しています。それがツリー構造状になっているんですね。
そのStateにそれぞれReducerというものが紐付いています。ReducerというのもElmにインスパイアされた影響がよく出てるところで、ロジック処理を行うところになるんですけれども。
そのReducerは純粋関数で記述されていて、Dispatchされたアクションを受け取って、「ああ、こういうことをするんだな」「こういうアクションが来たからこういうふうにStateを変更しよう」と。Reducerが関数として処理をして、現在のStateから新しいStateを作ってまた画面に返してあげるというのが、Reduxの一連の流れになります。
Actionにはもう1種類、Action Creatorというものがあります。これは関数なんですが、Actionを作る関数なのでAction Creatorになります。画面からは、Actionを作ってDispatchするのではなくて、Action Creatorを呼び出して、Action Creatorの中でActionを作っています。
これはどういうときに利用するかというと、Reducerは、純粋関数(副作用を認めない関数)でロジックを定義しているのですが、Action Creatorでは副作用を認めているので、例えば、通信を行ったりとか、あとはユーザーで外にデータを書き込むとかというような、副作用を伴う処理を行いたい場合は、Action Creatorで処理を行って、Actionという関数を作って返します。
続いて「Middleware」っていう赤い枠のところですけれども、こちらはそのActionとReducerの間にいて処理をする関数です。この関数も副作用を許容しています。例えば、一番シンプルな例で言うと、ActionをDispatchしたときにコンソールにログを出す処理のような利用方法もあります。
駆け足ですが、Reduxの解説は以上としまして、ここからは実際にアプリのデモとコードの解説に入ります。
今回ご紹介するサンプルアプリは、GitHubのAPIを活用して、リポジトリの一覧と詳細、それからログイン機能と、ローカルでUserDefaultsを活用してリポジトリをお気に入りにするという機能を有したサンプルアプリケーションになります。
実際に起動するところから、動いているところを見ていきましょう。
(以下、デモ)
コーディングが走って、通信が完了したら一覧画面が出ました。これはいまログイン状態なので、ここにリポジトリが出てます。「もう少し見る」という機能を追加していて、もう少し見ることができます。
それからリフレッシュ機能を有していて、プルリフレッシュで画面が更新されます。
ここはお知らせ機能なんですけれども、今1個から2つに増えましたけれども、お知らせがランダムで増えるようにしています。
この画面自体はUICollectionで作っているんですけれども、いろんな要素があります。お知らせだったりとかヘッダーの画像だったりとか、それから本の広告だったりとか。これを押すと本の「PEAKS」のページに飛びます。
詳細のページに飛んでみましょうか。Angular Style Guide。これ昔翻訳をちょっとお手伝いしたので。
ローディング中ですね。Readmeをレンダリングして表示しています。
リポジトリの一覧と詳細が出てますというところと、それからこの星マークがお気に入りの画面ですね。
これはUserDefaultに保存します。これ押してみると、一覧のほうにも反映されています。画面を横断してリアルタイムでデータを受け渡ししているというところのつくりと、それから一覧にも反映されています。この機能は後ほど紹介します。
あとはログイン画面ですね。ここはUIStackViewでやっています。
最後におまけとして、コンポーネントの一覧です。どういうコンポーネントがどのファイルかというのを覚えやすいように、コンポーネント単位と。それから、上のほうの画面も「こういう画面ですよ」というところをお知らせします。
これ動画ですけれども、通信できないときにちゃんと「オフラインだよ」と出してあげたり、通信できない状態で起動するとリトライボタンが出ます。
これだけ1つの画面で正常系・異常系だったり表示のパターンをたくさん作っているのは、Reduxはやっぱり状態の複雑さに対して回避するアプローチをメリットとしているので、1つの画面が複雑であればあるほどReduxメリットがあるかなと思って、サンプルアプリもこのようなつくりにしました。
では、実際にコードを見ていきましょうか。その前に、このアプリケーションで使っているキーテクノロジーを軽く紹介します。
Reduxのアーキテクチャ部分は、ReSwiftというオープンソースのライブラリがあって、そちらを活用しています。それからRxSwift。IGListKitは、Instagramが作っている差分更新のアルゴリズムを有したライブラリです。先ほどのリポジトリの一覧と詳細の画面はこのIGListKitで作られています。
続いて、OpenAPI。Swaggerと言われていたものですね。APIのエージェントを活用しています。
あとは、Dependency Injectionを意識しつつ、Testableに書いていますというところをご紹介します。
コードについては、API、ビジネスロジック、Viewという流れで解説しようと思っていますので、ちょっとReduxから離れるんですけれども、まずは通信するAPIの部分、今回だとOpenAPIでコードをジェネレートするところから解説します。
OpenAPIは、brewでも入れられますし、サイトからでもダウンロードできるんですけど、Javaでできていて。
こういうCLIだとかですね。
このスクリプトでAPIをジェネレートしているんですけれども、これのインプットとなるのが、yamlファイルでGitHubのAPIを定義したものがあります。これはオープンソースで公開されていて、GitHubが作ったものではないんですけれども。すごい量ですね。1万7,000行ぐらいになっています。
どういうつくりになっているかというと、「emojis」というエンドポイントがgetで、どういうリクエストパラメータを送るとどう返すか、例えば「200のときは、どういうモデルの構造のものを返すよ」ということが定義されているAPIのインターフェースです。
下のほうにいくと、こちらがデータですね。こんなふうにJSONが返ってくるレスポンスの構造を定義化されています。これだと、arrayで、タイプが、項目がstringでとか、インプットがboolean、Integerで返ってきますよというところが定義されています。
ちょっと大きいので、今回使うところだけシンプルに切り出して、このGitHubのyamlをOpenAPIのジェネレータのインプットとしてあげてですね。
このジェネレータは、このyamlファイルからいろんな言語のクライアントを作ってくれるんです。Swiftだけではなくて、GoだったりJavaScriptだったりJavaだったり。なので、今回はSwift 4で作りました。
こういうことを定義して、これを先ほどのスクリプトを実行するとこのように一瞬でコードがジェネレートされました。こちらでジェネレートされたコードはPodfileとしてライブラリ化されたものが出力されていますので、アプリケーションから利用する場合は、このようにPodのほうに定義してbundle installしてあげることで利用できるようになります。
先ほどジェネレートされたコードがこのあたりになるんですけれども。例えばGitHubのUserというモデルは、structでUserでEquatableで、このようなプロパティを持っています。SwiftのOptionalというところも定義ができるので、nilかどうかというところもインターフェースで定義されているかたちでジェネレートされます。
このように、モデルがジェネレートされつつ、あとは通信部分ですね。Alamofireが内部で利用されていて、Alamofireを介したAPI Clientの部分も自動的にジェネレートされております。
それからリクエスト部分。例えば「自分のオーナーであるリポジトリをゲットする」というそのリクエストの部分がジェネレートされるので、アプリケーションからは、この通信する部分はもうモデルもAPI Clientとしてもジェネレート済みなので、このメソッドを呼んであげるだけでもう通信した結果が得られる状態になっています。
今回のサンプルアプリケーションでは、このAPI部分をRxでSingleでラップして利用するようにしています。ここがラップしているコードで、Singleでラップしてあげつつ、エラーが返ってきた場合は通信エラーなのか、それからレスポンス系のエラーなのかどうかというところも明快になっていると思います。
それから、このAPI、targetにしてるんですけど、こちらでいろんなありえるような通信エラーの型も定義してあげてます。
いわゆるモデル層というか、APIで通信する部分のつくりはこのようになっています。
続いて、Reduxのビジネスロジックの部分に入ってきます。
先ほど説明したように、Reduxは1つのアプリケーションで大きな……1つのStateでアプリケーション全体を管理する。このStateの定義がこちらになるんですけれども、一番上、AppStateというstructに対して、この中でまたいろいろstructのstateが紐付いています。
例えば、ユーザーのリポジトリの一覧のステートだったり、パブリックなリポジトリのステートだったり、セッティング画面のステート、お気に入り画面のステートというようなつくりの定義があります。これは状態だけのステートですね。
続いて、Reducer部分。こちらはReducerなのでfunctionとして定義されています。
ReducerはActionを受け取ると処理を行うところとお話をしましたけども、Stateと同じようにツリー構造になっています。受け取ったActionは、この「Action」という汎用的な型になっているので、どんなActionが来たかというのを、型を見ながら、下の階層のどのReducerを実行するのかというところを定義しています。
ReducerはこのActionをインプットとして、今のStateをインプットとして、新しいStateを返します。それがこの三角形のいろんな部分で、ActionとStateをインプットにして新しいそのStateの部分を返すということを実行して、新しいビジネスロジックを築いているという流れになっています。
では、実際にリポジトリ画面のStateを見ます。こちらが、一番最初のこの画面のリポジトリ一覧のState部分を担うStateです。
Structで定義されていて、通信の結果を入れるような変数があったり、あとはComputedプロパティなども活用しながら、「今ローディングを出すべきなのか?」とか「今プルリフレッシュ中なのか?」、それから「エラーを表示すべきなのか?」というところを状態として作り込んでいます。
例えば「ネットワークエラーを表示すべきか?」というbooleanに対しては、通信した結果のエラーがネットワークエラーかどうかを見たり。あと通信は種類を入れます。初期化、初期の通信。プルリフレッシュだったら出す・出さないとか。通信の種類も分けてエラーをどのように出すかというところです。こちらが一覧のStateです。
続いて、このStateに紐づくActionをenumで記述しています。例えば「通信を開始するよ」「通信が成功したよ・失敗したよ」。それから「もっと見る」のボタンがあったんですけれども、それを押されたとき。あとは広告を非表示にするだとか。そういうシンプルなActionと。
こちらはAction CreatorというActionを作る関数ですね。ここは通信を行っています。先ほどご紹介したOpenAPIをジェネレートしたGitHubのAPIを、Singleでラップしたコードを呼び出します。
この結果を、正常系であればmapでActionに変換しています。こちらのActionですね。requestSuccessというAction。エラーの場合はエラーのActionに。これによって、非同期なんですけれども、「通信失敗した・成功した」をActionに変換するので、通信のAction CreatorはActionを作っているということですね。
続いて、ユーザーリポジトリのReducerになります。わかりにくいですね。ほとんどReducerはアクションをインプットにしているんですけれども、Reduxのおもしろいところは、自分の画面で発行したActionが自分の画面に関係するReducerに来るだけではないんですね。
Actionを発行すると、すべてのReducerにこのActionが伝搬されます。なので、自分のこの画面に関係なく違う画面のStateやReducerに対してもActionが伝搬されます。このリポジトリ一覧のReducerは、来たActionが、自分が処理すべきActionなのかどうか、型を見て判断します。
この部分は自分の画面のActionに対する処理なんですけれども、それ以外にも「お気に入りが押されたよ」みたいなActionのときとか、「タブが押されたよ。通信しましょう」とか「アプリケーションが起動したので通信しましょう」というような、いろんなものをActionとしてディスパッチするようにしているので、アプリケーション内で起こるいろんなことを、その画面で発行されたActionになっても容易にハンドリングすることができます。
では、この画面で起きるActionについてもうちょっと深掘りしていきましょう。今回、通信を開始するActionがディスパッチされたら、先ほどenumで定義していたので、このActionがこの画面のenumであれば、このようにパターン網羅でこの画面に関するActionをすべて網羅的に記述することができます。
「通信を開始したよ」という状態を入れて、次「通信が成功したよ」という場合はレスポンスが返ってきているので、それもまたデータを入れてあげますと。通信成功したら処理はこれぐらいなんですけれども、このへんは「お知らせをランダムに出す」という処理なので、実質はIGListKitで画面を表示してるとお話ししましたけれども、通信が返ってきたら基本的にはデータを作って表示させてあげるというところだけなので。
IGListKitはUICollectionViewをViewとして活用しつつ、データは1次元の配列にいろんな型のデータを詰めてあげて、そのデータの型によって画面の表示を切り分けてあげています、というおもしろい特徴があります。
例えば、この画面のデータは1次元配列で格納しているんですけれども、このヘッダー部分のデータ、それからリポジトリのサマリーのデータとか、「もっと見る」のボタンのデータを1つの配列に入れています。
こちらですね。配列を作ってあげて、お知らせをランダムで作って入れてあげて、ヘッダーを入れてあげて、どんどん1次元配列にappendしていくと。
もう型が違うのでAny型になっているんですけれども、いろんな型のデータが配列に入っています。
次は画面の解説に入るんですけれども、一覧のViewControllerですね。ここではIGListKitのデータが返ってくる要素の型によって表示を分けています。ちょっと見にくいんですけれども、Switch文で入ってきているそれぞれのエレメントのデータがどのような型かどうかということで分岐処理が入っていて。
例えばAdvertising。先ほどのふわっと消えたところですね。IGListKitはUICollectionViewを活用しているんですけれども、ちょっと工夫が入っていて。Instagramの画面わかりますかね? 写真があって、そこのプロフィールに「知り合いがいるよ」とかいろんな要素が表示されていると思うんですけれども。
IGListKitはSNSのタイムラインみたいな、多様な情報が入っても一覧で表示できるものというところにフォーカスしていて、それぞれの表示要素に対してまたsection controllerという概念を用いて、個別に実装しています。ここでは、AdvertisingElementのデータ型だったらこのSectionControllerで表示の実装を書いていて。
UICollectionViewの使い方と本当によく似てるんですけれども、セルのサイズと、セルを返してあげるところと、押されたときの処理ですね。表示する要素のデータ型ごとにSectionControllerがあって、このようなSectionControllerにコンポーネントを重ねたようなかたちで表示が実装されている。
先ほどセルを押したときにWebViewが表示されたと思うんですけれども、WebViewもActionとして実装してまして。Actionを実行してその画面を遷移しつつ、Rxも使いながら、画面がviewDidAppearで現れたときに、もう1回そのAdvertiseを消すというActionを実行しています。
Advertiseを消すというActionが実行されると、先ほどのステートのAction、こちらのhideAdvertisingってActionのところにReducerが流れてきて。
Stateとしては表示をいったんfalseというStateで持ちつつ、先ほどの作っているデータの一覧の中からAdvertisingのところだけが、表示すべきかどうかというところ見ながら、falseになったので、この要素が一覧からない状態で作られます。
Reduxがおもしろいところは、毎回そのActionがReducerで実行するとStateを新しく作るので、先ほどのIGListKitの配列の一覧の部分を表示してるAdvertiseをdeleteするという考え方ではなくて、配列を毎回全部作り直すんですね。表示しないので追加しないというかたちでつくります。
これによって、前回の表示している広告ありの表示の段階から、表示がない、広告がないArrayができます。
それをUICollectionViewでリロードするとアニメーションがあんまりきれいにいかないんですけれども、IGListKitは差分更新という概念を持っているので、今回この配列の2つ、前後の2つで広告の要素だけなくなっていることをちゃんと計算で検知してくれて、なくなってるのでそこだけアニメーションでふわって消してくれてるんですね。
なので、ロジック側は、今表示すべき一覧をゼロから毎回作ればいいという、非常にシンプルなロジックになります。
時間が押してきているので、あとはお知らせ機能を少し説明します。お知らせ機能は、先ほど画面に跨いで表示ができていたというところをご紹介しましたけれども、こちらがお気に入りのStateになります。
Action Creatorがいくつか並んでいるんですけれども、お気に入りはUserDefaultsに1個ずつ入っています。なので、「お気に入りをする」、それから「お気に入りを解除する」という関数があるんですけれども、お気に入りをする関数では、お気に入りにしたデータをUserDefaultsに保存してあげて、Actionではもう一度UserDefaultsから今あるお気に入りの一覧を読み込んであげて、Actionして、削除はまたその逆ですね。
お気に入りする・しないということをしたあとに、この今持っているお気に入りの一覧のArrayをActionとして生成しているので、先ほどのこのツリーのように、いろんな画面にActionが渡っていきます。
リポジトリ一覧、リポジトリ詳細、それからお気に入り一覧。この3つの画面のStateは、このお気入りのArrayが入っているActionが来たら、自分に関係があるので処理をしようというハンドリングをすることで、画面を跨いだ横断的な処理も可能になっています。
先ほどのユーザー一覧では通信の部分等を説明しましたけれども、そのちょっと上に、「お気に入りの型のアクションであれば」というところもCase文、Switch文で定義しています。
このお知らせのActionが来たときに、お知らせの一覧が入っているので、もう1回リポジトリの一覧をcreateDataSourceで作り直してですね。作り直すときにお気に入りに入っていれば、IDをマッチングしてお気に入りかどうかということをデータ要素として作ってあります。
この場合、先ほどはAdvertiseのデータが消えたので、差分更新が、「データがある・ない」というところで自動的にフッて消えてくれましたけれども、データが一部更新されたというところでもIDで差分更新してくれて、そこのセルだけ自動的にリロードをかけてくれます。
なので、今ふわってアニメーションがかかっているように見えるんですけれども、ここはこのArrayの一覧を全部作り直して、ここがTrueに変わっているので、ここのセルだけが自動的に差分検知されて、TableView、UICollectionViewのセルのリロードがかかって、このように表示ができている。このようなつくりになっています。
すいません。もう少し解説したかったんですけれども、お時間が来てしまっているようなので、こちらのアプリケーションに興味があったら懇親会のほうで聞いていただきたいです。
サンプルコードはすべて本のほうについていますし、このサンプルアプリケーションを解説した内容を紙面のほうにも記載しているので、興味を持っていただいた方はぜひ本のほうもご購入いただければと思います。
すみません、駆け足になりましたが、以上になります。ありがとうございました。
(会場拍手)
2024.12.20
日本の約10倍がん患者が殺到し、病院はキャパオーバー ジャパンハートが描く医療の未来と、カンボジアに新病院を作る理由
2024.12.19
12万通りの「資格の組み合わせ」の中で厳選された60の項目 532の資格を持つ林雄次氏の新刊『資格のかけ算』の見所
2024.12.16
32歳で成績最下位から1年でトップ営業になれた理由 売るテクニックよりも大事な「あり方」
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
PR | 2024.12.20
モンスター化したExcelが、ある日突然崩壊 昭和のガス工事会社を生まれ変わらせた、起死回生のノーコード活用術
2024.12.12
会議で発言しやすくなる「心理的安全性」を高めるには ファシリテーションがうまい人の3つの条件
2024.12.18
「社長以外みんな儲かる給与設計」にした理由 経営者たちが語る、優秀な人材集め・会社を発展させるためのヒント
2024.12.17
面接で「後輩を指導できなさそう」と思われる人の伝え方 歳を重ねるほど重視される経験の「ノウハウ化」
2024.12.13
ファシリテーターは「しゃべらないほうがいい」理由 入山章栄氏が語る、心理的安全性の高い場を作るポイント
2024.12.10
メールのラリー回数でわかる「評価されない人」の特徴 職場での評価を下げる行動5選
Climbers Startup JAPAN EXPO 2024 - 秋 -
2024.11.20 - 2024.11.21
『主体的なキャリア形成』を考える~資格のかけ算について〜
2024.12.07 - 2024.12.07
Startup CTO of the year 2024
2024.11.19 - 2024.11.19
社員の力を引き出す経営戦略〜ひとり一人が自ら成長する組織づくり〜
2024.11.20 - 2024.11.20
「確率思考」で未来を見通す 事業を成功に導く意思決定 ~エビデンス・ベースド・マーケティング思考の調査分析で事業に有効な予測手法とは~
2024.11.05 - 2024.11.05