この記事は Agent Grow Advent Calendar 2021 2日目の記事です。
今年もクリスマスの季節がやってまいりましたね!
この時期は毎年欠かさずカレーを作っており、その様子はこれまでアドベントカレンダーに投稿させていただきました。
ですが今年は私よりも上位のカレー魔人が入社されたので、
カレーはそちらの方にお任せして、私はソフトウェア設計についての記事を書いてみようと思います。
ソフトウェア設計って何ぞや?という方や、普段使ってるプログラミング言語は結構使えるようになってきたんだけど、次に何を勉強しようかな?
とお考えの方にとってちょうどいい内容になるのではないかなと思います。
せっかくなので最近チーム内で話題に上がっていた「外部API連携」を実現する場合に、自分の考えを散りばめながら実際に設計してみようと思います。
ちなみにスクロールバーの小ささを見てお分かりのとおりかなり長い記事になってしまったので、休み休み読んでいただければと思います。
【準備】今回考察するAPI連携の定義を決めていく
さあ、それでは早速設計を始めてみましょう!
・・・と書き始めようと思ったのですが、関連する機能の仕様などが明確でないと、
ソフトウェア設計が妥当なものかそうでないかを判断することができないと思います。
ということで、まずは今回考察するAPI連携の内容についてまとめてみましょう。
外部API連携の解説・考察はよくありがちな題材ではありますが、今回は
何かしらの外部Webサービスで管理しているユーザ情報を作成・取得・更新・削除 1)いわゆるCRUD するためのAPI連携
を実現することを考えたいと思います。
以降で連携に関連するさまざまな前提条件を明示していきます。
※ ちなみにひと通り設計が終わったら、外部サービスの仕様変更が入ることにします。どんな仕様変更が発生するかはお楽しみに!2)仕様変更がないならどんな設計でも大した問題にならないですからね
外部APIと連携するために必要な情報の定義
だいたいの外部API連携を行うときには、事前にAPI連携を行うための情報を登録してあげる必要があります。
なので、ここでは自前のDBでAPI利用者IDを持っておくことにしましょう。
また、その登録情報をもとにAPIアクセス用のトークンを取得して、API呼び出し時にそのアクセストークンをパラメータとして一緒に渡してあげる作りが多いので、
APIアクセストークンが必要 ということにしておきましょう。3)このへんは本記事の本筋じゃないところなので、ざっくりと決めています
あとは今回は外部サービスのユーザ情報に関連するAPI連携を行いたいので、当然ユーザ情報も取り扱う必要がありそうですね。
APIでやり取りすることだけを考えたいので、特に自前のDBなどで保存はしないことにしましょう。
これらをいつものカレーレシピ風にまとめると、API連携に必要な情報は以下になります。
【API連携の材料】
– API利用者ID ※面倒なのでDBへ保存しているものとします
(APIトークンを取得するための情報なら何でもいいと思いますが、ここではよくありがちな)
– APIアクセストークン
(API利用者ID ごとに一意)
– ユーザ情報
(例えば:名前、ユーザID 4)いわゆる「一意なキー(unique key)」というやつですね、その他の個人情報)
まあ細かい実装は本題ではないので、ざっくり更新したい情報とAPI連携に必要な情報を取り扱うくらいで見ていただければと思います。
今回想定するAPI仕様
これまたよくあるパターンではありますが、今回利用する外部サービスのAPIは以下のような仕様になっていることにします。
※ よくある REST API 的なものをイメージしていただければばっちりです!
APIアクセストークン取得API仕様
パラメータ:
– API利用者ID(必須)レスポンス:
– APIアクセストークン
取得したAPIアクセストークンには特に有効期限はないものとします。5)なんだかセキュリティ面が心配な仕様ですね
また、APIアクセストークンはAPI利用者IDごとに固有の文字列が生成されるものとします。
ユーザ情報取得API仕様
パラメータ:
– APIアクセストークン(必須)
– 対象者のユーザID(省略時には全ユーザの情報を取得)レスポンス:
– ユーザ情報のオブジェクト
(詳細は省略)
ユーザ情報作成API仕様
パラメータ:
– APIアクセストークン(必須)
– 作成対象のユーザ情報
(詳細は省略)レスポンス:
– 処理の成否
– エラーメッセージ
ユーザ情報更新API仕様
パラメータ:
– APIアクセストークン(必須)
– 更新する対象のユーザ情報
(詳細は省略)レスポンス:
– 処理の成否
– エラーメッセージ
ユーザ情報削除API仕様
パラメータ:
– APIアクセストークン(必須)
– 削除対象のユーザID(必須)
(ユーザ情報取得API にて取得できることとします)レスポンス:
– 処理の成否
– エラーメッセージ
API実行時の想定シーケンス
ざっくりこんな感じの処理を行うと、うまいことAPIを実行できることにします
1. DBから API利用者ID を取得する
2. API利用者IDを使ってAPIアクセストークン取得APIを実行し、APIアクセストークンを取得する
3. 各種ユーザ情報に関連するAPIを呼び出す際には、APIアクセストークンとそれぞれ必要なパラメータを渡す
4. 実行したAPIのレスポンスをうまく処理する6)このへんは本筋じゃないので(ry
結構シンプルではありますが、API実行に際しては必ずAPIアクセストークンが必要になる というところがポイントになります。
実際にWeb画面でユーザ情報を入力できるようなサービスを作ろうと思ったら、最初にユーザ情報を取得して画面に表示するためにユーザ情報取得APIを実行したり、
画面で入力したユーザ情報を変更するためにユーザ情報変更APIを実行したり
やりたいことに合わせてそれぞれのAPIを実行するイメージになります。
次のセクションで私のクラス設計を記載します。
興味がある読者の方は、自分ならこんな感じで設計するかなぁ・・・という案を考えていただくと、より本記事を楽しめるのではないかなと思います。※ こういうの、ミステリ小説の「読者への挑戦状」みたいでいいですね。
【本題】APIを実現するためのクラスを設計してみる
さあ、前置きが長くなってしまいましたがいよいよここからが本題です!
必要な情報は分かった。利用できるAPIも分かった。それじゃあどういうクラス設計でそれらを実装していきましょうか・・・。7)ここからを書きたかったんですよねぇ
個人的に今回ポイントになりそうなところは
- 必ず必要になるAPIアクセストークンをどう取り扱うか
- どういう思想で処理を切り出すか
かなと思っています。
しいて言うならプラスして、できれば将来的な仕様変更に強い作りにしておきたいな っていう感じになりますかね。
APIアクセスするためのクラス設計(私の場合)
※ コード表示プラグインの仕様で「>」がHTMLセーフな文字列にエンコードされてしまうので、
以降コード内では「>」は全角の「>」で記載しています
// API接続するためのクラス class ApiConnector { // APIアクセストークン protected $access_token; // コンストラクタ public function __constructor() { $this->getApiToken(); } // APIアクセストークンを取得する protected function getApiToken() { $access_info = $this->getAccessInfo(); $access_infoをパラメータにしてアクセストークン取得APIを実行する処理をゴニョゴニョ $this->access_token = アクセストークン; } // DBからAPI利用者IDを取得する protected function getAccessInfo() { DBに接続してうまいことAPI利用者IDを取得する処理をゴニョゴニョ return 取得したAPI利用者ID; } // ユーザ情報を一覧で取得 public function getUserList() { $this->access_tokenを使ってユーザ情報取得APIを実行する処理をゴニョゴニョ(ユーザIDを省略して一覧取得) return 取得したユーザ情報; } // ユーザ情報取得 public function getUserInfo($user_id) { $this->access_tokenと$user_idを使ってユーザ情報取得APIを実行する処理をゴニョゴニョ return 取得したユーザ情報; } // ユーザ情報作成 public funciton createUserInfo($user_info) { $this->access_tokenとuser_infoを使ってユーザ情報作成APIを実行する処理をゴニョゴニョ return API実行の成否; } // ユーザ情報更新 public funciton updateUserInfo($user_info) { $this->access_tokenと$user_infoを使ってユーザ情報更新APIを実行する処理をゴニョゴニョ return API実行の成否; } // ユーザ情報削除 public funciton deleteUserInfo($user_id) { $this->access_tokenと$user_idを使ってユーザ情報削除APIを実行する処理をゴニョゴニョ return API実行の成否; } }
ポイントとしては以下になります。
- コンストラクタでAPIアクセストークンを取得し、メンバ変数として持っておく
- publicメソッドとしては、各種ユーザ情報APIと関連するもののみ
- 各種ユーザ情報APIにアクセスするための事前準備は全部protectedにして、外からはアクセスできなくしている
- 各種APIアクセス時には、APIで操作するために必要な情報のみを渡すようにする
- ユーザ情報取得APIはひとつだけど、利用したいタイミングが異なるのでメソッドとしては一覧取得と個別情報取得とで分けておく
こうしておくことで、
$api_connector = new ApiConnector(); $user_info = $api_connector->getUserInfo($user_id);
のようにするだけで各種ユーザ情報APIを利用することができるようになります。
もちろんAPIアクセストークンをメンバ変数として持たずに、
各種ユーザ情報API実行直前でAPIアクセストークン取得APIを実行する方法でも実現可能です。
あるいはこのクラスではAPIアクセストークンを取得せず、
各種APIユーザ情報API実行時に引数として渡すような設計でも実現可能です。
また、staticなメソッド・クラス変数を使って実装し、インスタンスを生成しないような作りも可能でしょう。
いまの仕様であればどのような設計をしたとしても、いまやりたい外部API連携を実現できていればどれも正解だとは思います。
ではなぜ私がこのようなクラス設計にしたのかというと、その方がエレガント8)辞書によると、落ち着いて気品のあるさま。優美なさま。だからです。
・・・とだけ書いても全然ピンとこない方もいらっしゃると思うので、実用的な観点で説明すると将来的な仕様変更に強くするためです。
仕様変更に強い設計というのはいろいろなところでよく聞きますが、仕様変更に強い設計とは具体的にどのような設計なのでしょうか?
私が考える「仕様変更に強い設計」とは?
ここで唐突な持論説明フェイズに入ります。
この観点はいろいろな考え方があると思いますので、絶対的な正解はないと思っています。
私が考えている仕様変更への強さの尺度は
内部・外部問わず仕様変更が発生したときに、ソースコードの変更を伴う変更箇所(クラス・メソッドなど)をいかに少なく抑えることができるか
です。これは
仕様変更による修正を行う際に、メソッドの引数と戻り値の仕様、およびその振る舞いが変化する箇所がどのくらい少ないか
と言い換えることもできるのかなと思います。
もちろんあらゆる仕様変更に対してソースコードの変更が不要であれば、仕様変更に対して最強であるといえると思います。9)現実では確実に何かしらの変更が必要になるものだとは思いますが・・・
仕様変更に伴うソースコード変更が発生したとしても、例えば1クラスのみの一部の変更で済んだり、1メソッドのみの変更で収まるのであれば、その設計は仕様変更に強いと言える
というイメージかなと思っています。
もう少し具体例を交えて説明すると・・・
例えば先ほど記載したApiConnectorクラスを使って外部APIを実行する際には
$api_connector = new ApiConnector(); $user_info = $api_connector->getUserInfo($user_id);
という使い方をすればよい と説明しています。
これが例えば仕様変更により
$token = APIアクセストークンを取得するAPIを実行 $api_connector = new ApiConnector(); $user_info = $api_connector->getUserInfo($user_id, $token);
のように、外部APIを実行するメソッドから引数としてAPIアクセストークンを渡さなければいけなくなったとしましょう。
ApiConnectorクラスを使ってAPIを利用している箇所が1か所だけならそこだけ変更すれば大丈夫ですが、これが仮に100か所から使われていたらどうでしょう。
さらに、100か所中何カ所かは以下のような使い方をしていたらどうでしょう?
// API連携するサービスに関連したAPI連携インスタンスを取得する(さらにほかのサービス友連携できるようになったものとして) $api_connector = ApiConnectorFactory::getConnector(サービス名); $user_info = $api_connector->getUserInfo($user_id);
あるいはPHP独自の使い方で
function getUserInfo($ApiClassName) { $api_connector = new $ApiClassName; return $api_connector->getUserInfo($user_id); }
のように実装されている箇所があったら、おそらくこのクラスを利用している箇所を特定するだけでも、かなり骨が折れるのではないかと思います。
もちろんこのようなトリッキーな実装がなければよいのですが、複数人のチームとして開発していると、このような実装が絶対に発生しない!とは言い切れなくなってくるものです。
そしていつか必ず変更漏れや変更ミスが発生し、変なバグを埋め込んでしまいます。
外部サービスとのAPI連携の仕様変更が発生したとしても、メソッドの引数や戻り値の仕様に変化がないように変更できれば、
$api_connector = new ApiConnector(); $user_info = $api_connector->getAccessInfo();
のまま利用できるし、先の例のようなトリッキーな使い方をしていたとしても仕様変更に伴うバグにおびえなくてもよくなります。
そのため、私としては
仕様変更が発生したときに、ソースコードの変更を伴う変更箇所(クラス・メソッドなど)をいかに少なく抑えることができるか
が仕様変更の強さの指標になるのかなと考えています。
そしてこのへんをうまくやるためにデザインパターンや契約による設計などの知識が役に立つのかなと思っています。10)気になる方はそれぞれのキーワードでググってみてください!
【検証】さあお待ちかね、仕様変更タイム!
それでは私の設計が本当に仕様変更に強いエレガントな設計になっているのか、さっそくよくある仕様変更を発生させてみましょう!
ここでは外部連携をしているとよくありがちな11)そして困る仕様変更が発生したことを想定してみましょう!
APIアクセストークン情報取得APIを実行すると、それまでのトークンが無効になってしまう
API連携で利用していた外部サービスで、APIアクセストークンを悪用したセキュリティインシデントが発生してしまいました!
それに伴い、APIアクセストークン情報取得APIを実行すると都度新しいアクセストークンを発行し、それまで発行されていたAPIトークンが無効になる仕様になってしまいました。12)これだけでは根本解決ができないでしょうけどね
つまり、今までは
1回目の実行:asdf1234 を取得
2回目の実行:asdf1234 を取得
3回目の実行:asdf1234 を取得
というように、何度実行しても同じトークンを取得できていたのですが、
仕様変更により
1回目の実行:asdf1234 を取得
2回目の実行:qwer5678 を取得(以後asdf1234 を使ったAPIアクセスはエラーになる)
3回目の実行:zxcv3456 を取得(以後asdf1234、qwer5678 を使ったAPIアクセスはエラーになる)
というように、APIアクセストークン情報取得APIを実行するごとに有効なキー情報が更新されるようになってしまいました。
この仕様変更を踏まえてクラス設計を変更すると・・・
この変更により、並行してAPIアクセストークンを取得してから実際にAPIを実行するまでの間に時間が空くと、APIアクセストークンが無効になる可能性が出てきました。
一番シンプルに対応しようと思ったら、私ならこのように実装すると思います。
// API接続するためのクラス class ApiConnector { // コンストラクタ public function __constructor() { // 何もしない } // APIアクセストークンを取得する protected function getApiToken() { $access_info = $this->getAccessInfo(); $access_infoをパラメータにしてアクセストークン取得APIを実行する return アクセストークン; } // DBからAPI利用者IDを取得する protected function getAccessInfo() { DBに接続してうまいことAPI利用者IDを取得する return 取得したAPI利用者ID; } // ユーザ情報を一覧で取得 public function getUserList() { $token = $this->getApiToken(); $tokenを使ってユーザ情報取得APIを実行する処理をゴニョゴニョ(ユーザIDを省略して一覧取得) return 取得したユーザ情報; } // ユーザ情報取得 public function getUserInfo($user_id) { $token = $this->getApiToken(); $tokenと$user_idを使ってユーザ情報取得APIを実行する return 取得したユーザ情報; } // 以下同様にAPI実行直前でアクセストークンを取得するような作りにする }
こうすることで、ほとんどのケースでトークンが有効なことを担保することができます。
※ 並行してAPIが実行されるような場合だと、APIアクセストークンが無効になる可能性はゼロにはなりません
もしAPI実行が必ず成功するように実装するのであれば、
// ユーザ情報取得 public function getUserInfo($user_id) { $token = $this->getApiToken(); $tokenと$user_idを使ってユーザ情報取得APIを実行する if (APiアクセストークンエラーだった) { return $this->getUserInfo($user_id); } return 取得したユーザ情報; }
のように成功するまでリトライする方法もありますし、
セキュリティ的な観点は別として、APIアクセストークンをDBに保存して、都度トークンを取りに行くのではなく保存したAPIトークンを使いまわす という実装もあり得るかもしれません。
本当に仕様変更に強かったか
結局ソースコードを変更することになりましたが、果たしてこれが「仕様変更に強い」と言い切れるのでしょうか?
本仕様変更前にこのクラスを使って外部APIを実行する際には
$api_connector = new ApiConnector(); $user_info = $api_connector->getUserInfo($user_id);
という使い方をすればよい と説明していました。
今回の変更ではこのソースコードを変更する必要はありませんし、戻り値の仕様も変化ありません。
なので、私の尺度では仕様変更に強いといえると思います。
外部サービスの1秒当たりのAPI実行可能回数が1回になってしまった
API連携で利用していた外部サービスで、今度はDDoS攻撃によるセキュリティインシデントが発生してしまいました!
それに伴い、API利用者IDごとに1秒当たり1回しかAPIを実行できなくなってしまいました!13)利便性 とは
もし1秒以内に複数回APIを実行してしまった場合、APIからはエラーのレスポンスが返ってきてしまいまう仕様になってしまいました。
この仕様変更を踏まえてクラス設計を変更すると・・・
1秒1回しか実行できないんだと、かなりストレスが溜まりそうですね。
アクセストークン取得APIも実行回数に含まれるので、これまでのようにAPI実行直前に都度アクセスキーを取得するような作りにしておくのはよくなさそうですね・・・、
こうなると、いかに外部APIを実行する回数を減らせるかが勝負になってきそうですね。
しょうがないので、APIを実行した結果をなるべくDBに保存してキャッシュをとるようにしようと思います。
キャッシュする情報は以下のようにしましょうかね。
【DBに保存する情報】
– APIアクセストークン(とその取得日時)
– 外部APIで取得したユーザ情報(とその取得日時)
※ 一応ユーザのユニークキーごとに情報を持つイメージ
それぞれ、情報を取得したタイミングでDBに情報が存在しなければDBにも保存するイメージで実装します。
外部サービスで管理している情報をこちらのDBでキャッシュする場合の厄介なポイントは、
DBに保存しているデータが古くなってしまっている可能性があることです。
今回対象にしているユーザ情報が今回作成している外部API経由でしか変更できないのであれば安心なのですが、
外部APIを提供している側のサービスでも当然変更できますし、他のサービスでも同様に外部APIを使ってユーザ情報を変更しているかもしれません。
なので、API経由でのデータ取得日時が1分以上前だったら外部APIでユーザ情報を取得しなおすようにしましょうか・・・。
この場合、古い情報が表示される可能性があるのは制限事項となりますね。
また、ユーザ情報を更新した場合には、該当のユーザ情報のキャッシュを削除して無効にしておくのも忘れちゃいけないポイントですね。
あとは将来的にアクセストークンに有効期限が設定されそうなので、このタイミングで取得日時を保存するようにしておきます。
・・・正直ここまでして作る必要があるようには思えないので、現実世界ではAPI連携やめてサービス側で操作するようにしたほうがユーザビリティが高くなりそうですが、ここではその点については目をつむりましょう。
実装はざっくりこんな感じになりますかね。
// API接続するためのクラス class ApiConnector { // コンストラクタ public function __constructor() { // 何もしない } // APIアクセストークンを取得する protected function getApiToken() { DBから有効なアクセストークンを取得 if (有効なアクセストークンがあった) { return アクセストークン; } // 以下はDBに有効なアクセストークンが存在しない場合の処理 $access_info = $this->getAccessInfo(); $access_infoをパラメータにしてアクセストークン取得APIを実行する アクセストークン情報をDBへ保存する return アクセストークン; } // DBからAPI利用者IDを取得する protected function getAccessInfo() { DBに接続してうまいことAPI利用者IDを取得する return 取得したAPI利用者ID; } // ユーザ情報を一覧で取得 public function getUserList() { DBからユーザ情報を取得する if (一番最後に更新されたのが1分以内だった) { return 取得したユーザ情報; } $token = $this->getApiToken(); $tokenを使ってユーザ情報取得APIを実行する処理をゴニョゴニョ(ユーザIDを省略して一覧取得) if (回数制限かトークンエラーでAPI実行に失敗した) { return $this->getUserList(); } return 取得したユーザ情報; } // ユーザ情報取得 public function getUserInfo($user_id) { DBから対象のユーザ情報を取得する if (ユーザ情報が存在した かつ 1分以内に取得した情報だった) { return 取得したユーザ情報; } $token = $this->getApiToken(); $tokenと$user_idを使ってユーザ情報取得APIを実行する if (回数制限かトークンエラーでAPI実行に失敗した) { return $this->getUserInfo($user_id); } 取得したユーザ情報をDBへ保存する return 取得したユーザ情報; } // ユーザ情報作成 public funciton createUserInfo($user_info) { $token = $this->getApiToken(); $tokenとuser_infoを使ってユーザ情報作成APIを実行する処理をゴニョゴニョ if (回数制限かトークンエラーでAPI実行に失敗した) { return $this->createUserInfo($user_info); } 作成したユーザ情報をDBへ保存する(この情報自身がキャッシュになるので) return API実行の成否; } // ユーザ情報更新 public funciton updateUserInfo($user_info) { $token = $this->getApiToken(); $tokenと$user_infoを使ってユーザ情報更新APIを実行する処理をゴニョゴニョ if (回数制限かトークンエラーでAPI実行に失敗した) { return $this->updateUserInfo($user_info); } 更新したユーザ情報をDBから削除する return API実行の成否; } // ユーザ情報削除 public funciton deleteUserInfo($user_id) { $token = $this->getApiToken(); $tokenと$user_idを使ってユーザ情報削除APIを実行する処理をゴニョゴニョ if (回数制限かトークンエラーでAPI実行に失敗した) { return $this->deleteUserInfo($user_info); } 削除したユーザ情報をDBからも削除する return API実行の成否; } }
API実行失敗時の処理をコピペして書くのが気持ち悪い&事故が起こりやすいので、
実際にはprotectedなメソッドに切り出してうまく共通化したいですね。
※ この辺りは言語によって実装方法が変わってくると思うのであえてそのままにしています
本当に仕様変更に強かったか
結局ソースコードを変更することになりましたが、果たしてこれが「仕様変更に強い」と言い切れるのでしょうか?
毎回同じ説明をするのもアレなので、先ほどの例と同様にこのクラスの処理を呼び出す際に
$api_connector = new ApiConnector(); $user_info = $api_connector->getUserInfo($user_id);
などのままで問題なく動作することをご確認いただければと思います。
【参考】私的エレガントに設計するための3つの原則
最後に、私が考えるエレガントな設計をするためのコツをいくつか箇条書きにしておきますね。
【私的エレガントな設計をするための3つの原則】
– クラスが外部に公開する情報・メソッドはなるべく少なくする
– クラスを利用する時に考慮しなきゃいけないことはなるべく少なくする
– 決まりきった処理や決まりきったデータは適切に共通化・抽象化する
それぞれの項目をちゃんと補足しようとすると、おそらく本記事と同じくらいの分量が必要になるので、いったんはポイントの提示だけにしておきます。
機会がありましたらそのあたりのお話も記事にさせていただこうと思います!
まとめ
ここまで読んでくださったみなさまが考えた設計は仕様変更に耐えることができましたか?
こんな誰得だよっていう技術記事を最後までがっつり読んでくださった方々には本当に感謝でございます。
きっとこういうお話がお好きで(いい意味で)奇特な方なのではないかと思っております。
本当は「ほかのサービスの外部APIを取り扱うことになった」とか「APIアクセストークンに有効期限が設定された」とかの仕様変更も考えていたり、
原則についてももっとちゃんと書こうと思っていたのですが、さらに文章量が多くなりすぎるのでやめておきました。。。
さて、2日目からこんな分量で大丈夫か?という心配はありますが、きっと明日担当の方はうまくやってくれることでしょう!
それでは、明日の記事も楽しみにお待ちください!
注訳はこちら
↑1 | いわゆるCRUD |
---|---|
↑2 | 仕様変更がないならどんな設計でも大した問題にならないですからね |
↑3 | このへんは本記事の本筋じゃないところなので、ざっくりと決めています |
↑4 | いわゆる「一意なキー(unique key)」というやつですね |
↑5 | なんだかセキュリティ面が心配な仕様ですね |
↑6 | このへんは本筋じゃないので(ry |
↑7 | ここからを書きたかったんですよねぇ |
↑8 | 辞書によると、落ち着いて気品のあるさま。優美なさま。 |
↑9 | 現実では確実に何かしらの変更が必要になるものだとは思いますが・・・ |
↑10 | 気になる方はそれぞれのキーワードでググってみてください! |
↑11 | そして困る |
↑12 | これだけでは根本解決ができないでしょうけどね |
↑13 | 利便性 とは |