BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル モデル駆動ソフトウェア開発のためのベストプラクティス

モデル駆動ソフトウェア開発のためのベストプラクティス

モデル駆動ソフトウェア開発(MDSD [1][2])はもはや業界の非主流派ではない。大成功してより多くのソフトウェアプロジェクトで適用されている。この記事では、過去数年間に集めた経験に基づきベストプラクティスへの私たちの貢献を伝えたいと思う。

ドメイン特化言語 (DSL) とコードジェネレータがでてきてからしばらく経っているので、必然的に、この記事はこの分野でベストプラクティスに関して解説する最初の記事ではないこれらの以前に述べられたプラクティスのいくつかはしっかりと定着したが、一方では、関連がなくなったり、すでに廃れてしまったりしたプラクティスさえある。この記事の最初の部分では「時の試練に耐えた」これらの原則を扱う。さらに、他の新しいベストプラクティスはまだはっきりしていないが、時間をかけて明らかになってきている。これらについてはこの記事の二番目の部分で解説する。

もっとも重大な基本的提案から始めよう。

生成したコードと手書きのコードをお互いに分離する

重大な点ではあるけれども何度も繰り返して無視されることがある。すべての成果物はそれが生成されたものであるか、開発者によって操作されたものであるか、はっきりと明白でなければならない。これは、ユーザが後で追加したり変更したりするコードにコードジェネレータを使うべきではないという意味ではない。ただこれは明示的にしなければならず、とりわけこのコードは一度だけ生成すべきである。(『用語解説』を参照)100%生成したコードは、変更したり、リポジトリにチェックインしたりすべきではない。それは使い捨てにできるものであり、重要な成果物はモデルなのだ。

受動的なコード生成、つまり「ウィザード」型の1回限りの生成は最近とても人気がある。スカッフォールディングの一般的な概念のもと、ほとんどすべての新しい (ウェブ) フレームワークで新しいプロジェクトの最初の段階は可能な限り簡単なものになっている。これは、最初はとても役に立ち、フレームワークの受け入れをかなり促進できる。しかしながら、これらの最初の段階はプロジェクトのライフサイクルのほんの少しの部分を構成するものであり、ユーザに短期間の支援を提供するにすぎない。

このベストプラクティスに反して相対的に広まっていることは、保護されたリージョンを使うことである。これは、ジェネレータが上書きしない生成したコードのテキストリージョンを指す。保護されたリージョン内で開発者は手書きで変更でき、ジェネレータがそのギャップを埋める。しかし、そうすることにより生成したコードはファーストクラスのコードのレベルに上がり、例えば、チェックインしなければならなくなるのだ。(追加の作業も発生する)さらに開発者が書き込んだ保護されたリージョンはアクティベートされなければならない。そうでなければ保護されていない状態になる。プロジェクトの熱い戦いの最中にこれは簡単に見過ごされるので、コードが失われ、長い時間エラーを調査して大きな苛立ちを感じることになる。

それでは、実際のベストプラクティスはどのようなものか?2つのバリエーションがあって、うまい具合に一緒に使うこともできる。まず生成したコードに別のディレクトリを準備すべきである。(通常は src-gen)こうすれば生成したコードと生成していないコードの物理的な分離ができる。さらに、すべての生成した成果物は、「// WARNING! GENERATED CODE, DO NOT MODIFY」のようなコメントで始めるべきである。完璧主義の人は、そのコードがどのテンプレートで生成したかを追記してこれを補うかもしれない。どちらのオプションも、IDEの中で生成したファイルと生成していないファイルを区別するのに使うことができる。

生成したコードをチェックインしない

生成したコードはチェックインすべきではない。それはJavaのバイトコードや他の派生した成果物をチェックインすべきではないのと同じ理由である。派生した成果物は100%冗長であり、それらは再生可能ではない情報は含んでいない。そのような成果物をチェックインすることは、管理するデータのボリュームを増加させ、同期化するたびにコンフリクトが発生する。それらのコンフリクトは、OverrideとCommitを使うときは無視するほうがよいのだ。一方で、すべてチェックインすれば、あなたはもう一杯コーヒーを取りに行けるかもしれないが......

保護されたリージョンでこの問題はさらに大きくなる。そのような場合、すべてを単に上書きすることができないからだ。同時に他の開発者が保護されたリージョンを変更していることもありえるだろう。

生成したコードをチェックインする意味があるのは、唯一なんらかの理由でビルドプロセス中に実行するジェネレータを統合することができない場合である。

ジェネレータをビルドプロセスに統合する

ビルドプロセスの自動化は、開発チームの生産性を大いに改善する方法である。これに関して重大な点は、プロセスを実際に完全に自動化し、手動の作業をなくすことである。本来ならビルドプロセスは、フェーズチェックアウト、コンパイル、リンク (必要な場所で)、テストプラットフォームの配置とユニットテストの実行で構成される。MDSDプロジェクトでは、生成フェーズはコンパイルフェーズの前に追加されなければならない。

ビルドプロセスはローカルだけでなくこの目的のために設置されたサーバー上でも実行されることは言うまでもない。これはプロジェクトに参加しているすべての開発者からのコードが絶えず統合される (継続的統合) ことを保証する。

ターゲットプラットフォームのリソースを使う

ターゲットフラットフォームの概念はさまざまなタスクに役立つ。多くの場合、アプリケーションはモデルから全体が派生するのではなく、ただターゲットの言語 (例えば、Java) でわずかに表現される部分から成る。

DSLが一般的に役に立つ分野はドメインモデルやサービスレイヤーなどのスキーマ情報を扱う。これらのレイヤーにも含まれているロジックはいかなるプログラミング言語でもうまい具合に定義できる。

保護されたリージョンを使って生成したコードにロジックを統合する代わりに、ターゲット言語の構造を使い、異なるファイルにさまざまなアスペクトを置くオプションがある。一方は生成され、他方は不足している情報を追加しながら開発者によって提供されるのである。

例えば、Javaでは継承が使える。たくさんのシナリオで、三段階の継承 (MDSD) はその価値が証明されている。これにより抽象基底クラス (アンセスタクラス) は一般的フレームワークによって提供され、その属性や振る舞いは生成された同様な抽象クラスによって継承される。そして、具象クラスはこの生成されたクラスから継承する。

図1: 三段階の継承

もちろん、継承やデリゲーションの組み合わせのようにさらに数多くのバリエーションがある。他のターゲット言語で、Includesのように他の概念は適切な場所で使わなければならない。

クリーンなコードを生成する

単にコードを自動的に生成したからといって、二度と再び人間の目で読まれないという意味ではない。生成したコードはそれ故に手書きのコードに適用するのと同じ品質の要求を満たすべきである。同質のコードベースは、後の理解とエラー箇所の発見をずっと容易にする。

生成プロセスと直接連動してコードフォーマッターを使うことによって、コードの挿入とフォーマットはテンプレートの複雑さを増すことなく自動化できる。生成したコードは適切なコードスタイルに従い、きちんと確立されたデザインパターンを使うべきである。

コンパイラを使う

ターゲットプラットフォームを使う別の機会は、ターゲット言語のコンパイラを通して開発者とコミュニケーションすることである。例えば、上述した3段階の継承が使われた場合、それぞれのメソッドを実装するために抽象メソッドが生成されるだろう。そして、コンパイラは、開発者が具象クラスにこのメソッドを実装するように要求するだろう。

自分自身へのもっと複雑な依存関係では、開発者がまだ作成していないコードを使ってダミーのコードを生成することをコンパイラは可能にするだろう。

ターゲット言語にコンパイラがないが、それにもかかわらずターゲットプラットフォームの開発者が特定のことを手動で追加した (明らかに生成したコードの中ではない!) ことを別の開発者が保証しようとする場合、openArchitectureWareの Recipe Framework [7] を利用するとよい。Recipeは、各ジェネレータを実行した後、ある状態になるかどうか開発者に確認するよう指示する。例えば、特定のクラスを手動で作成し、それが生成されたクラスから継承しているかどうかといったことである。その状態にならない場合、開発者は随時通知を受ける。

メタモデルで語る

Eric Evans氏は、彼の著書、ドメイン駆動設計 [5] の中でユビキタス言語の概念を記述している。このアイデアの最重要ポイントは、プロジェクトで技術的、または、専門的な概念を定義し、それに関してすべての参加者がコミュニケーションできる1つの同一言語を開発することである。このようにして、複雑な概念がはっきりと明示的に扱われる。その結果として、プロジェクトの重要で「生きている」コンポーネントになることができる。これらの概念とそこで使われる言葉は、プロジェクトのすべての段階に相互に関連がある。

このモデルがメタモデルに関連する概念に使われる場合、技術的な概念にも明白な名前と意味を与えるべきである。専門用語と意味を一貫して用いることにより、例えば、チームミーティングで重要なアスペクトがさらにはっきりと説明できる。決定された概念がもはや要求に合っていないとき、それもすぐに明らかになる。そのような場合、DSLは変更されたフレームワークの状態に適応させなければならない。

繰り返しDSLを開発する

DSLは前もって準備された方法を使って開発されるのではなく、理想で言えば、APIと同じで徐々に増やしていくものである。まず、より明らかな核となる抽象化を定義する。そして、記述されたドメインを理解しながら、個々の要素を追加したり、削除したりする。

DSLは公開されたインタフェースで、API開発の原則もこの分野に当てはまる。インタフェースのライフサイクルに関してはすでに合意したものであり、既存の規約を将来の開発プロセスの中で破ってはいけない。

どのような場合であっても、DSLの中心にある技術は移行機能に大きな影響を持つ。例えば、新しく導入された概念ではもはや利用できないものを置き換えるために、手動でモデルを移行するには、エディタに古いモデルを読み込まなければならない。XMIやデータベースを使ったオブジェクトベースの保存は、しばしばメタモデルへの変更がそれらの互換性をなくすため、使えなくなることが多い。そのような場合、古いシステムから新しいメタモデルへのモデル変換を使ったプログラムで移行が行われなければならない。さもなければ、保存フォーマットレベルに落として古いデータを互換性のあるものにしなければならないのだ。

DSLの具体的なシンタックスがモデルを保存しているものと同じ (つまりテキストとして) 場合は、移行はもちろんずっと簡単である。

繰り返しモデル検証を開発する

すでに強調されているように、モデル駆動開発プロセスは処理されているモデルがマシンで読むことができる正式な形式で利用できることを前提にしている。モデルが持つ形式は、そのメタモデルで決まっていて、同時にモデリングツールの土台を形作る。(実際のところ、メタモデルとは関係なく動くモデリングツールがあり、その結果としてMDSDのモデルチェーンで深刻な問題を引き起こしている。)さて、正当なモデリングツールは形式的に正しいモデルのみを作成すると仮定できるはずだ。そうすれば、モデルはジェネレータで問題なく処理できる。これは残念ながら間違っている。メタモデルは、作成されたモデルの静的な面を記述しているに過ぎない。セマンティクスはこの静的情報で表現することはできない。例として、「限定されたステートマシンで、各ステートは一度だけ出現できる」という条件がある。この条件は普通のメタモデル言語で表現できない。この問題をうまく避けるためにOCLやoAWのCheckのような制約言語が作られたのである。これにより先程述べたような(「すべてのマシーンステートの中で名前が重複してはいけない」)条件が表現できる。

「限定されたステートマシンで、各ステートが他のステートに遷移によって接続されていなければならない」という条件は、直接メタモデルの中でも実装できる。(1...nという多重度によって)このような場合、制約がそれでもなお好まれることが多い。ひとつはメタモデルが結果としてよりはっきりと簡単になるからであり、もうひとつはモデル設計中にできる中間ステートが全体的なスキーマに従うからである。それは多くのツールにとって保存時の基本的な前提条件である。

制約は繰り返し開発されるべきである。まず第一に、明らかに正しいと思われる制約が定義される。プロジェクトの最中に生成エラーが発生するとすぐに、制約がこの問題を解決するのに使われる。そのエラーは、モデルのチェックが不完全であることが原因で起こったものである。このように、モデルの妥当性チェックはだんだん完成してギャップがなくなっていく。

レファレンスモデルを使ってジェネレータをテストする。

できれば自動テストスイートを使ってコードジェネレータ自身もテストされるべきである。しかし、どうやってこれを実現するのか?

よく知られている選択肢は、ソースのテキストを予想されるソースコードと単に比較することである。しかしながら、この構成はとても脆弱で、ジェネレータの非常に小さな変更でさえテストを無効にする。実際のところ、生成したコードが要求にあう方法でターゲットプラットフォームの中で動くことを確認することが目的である。

これは、生成したコードをテストするのにユニットテストを使ってとても簡単に実現できる。ほとんどのターゲット言語には対応するフレームワークがある。このプロセスで、レファレンスモデルは特徴的な方法を用いてDSLの概念を使って生成される。この場合、特徴的という意味は、例のドメインが「発明されて」使われたのではなく、表されているDSLの概念によってモデル要素の名前がつけられることである。ドメインモデルDSLのレファレンスモデルは、例えば、以下のようになる。

abstract entity AbstractEntity {
attr String oneStringAttr
attr String[] manyStringAttr
ref SimpleEntity toOneReference
}

entity SimpleEntity {
}
// etc...

理想を言えば、テストスイートはそのようないくつかのレファレンスモデルと関連するテストで構成される。そのテストは異なるアスペクトと組み合わせを記述するだろう。次のイテレーションで、新しいレファレンスモデルは、取り除く前に不具合を見つけるために利用できる。既存のレファレンスモデルとそれぞれのテストはこのプロセスで妥協することはない。

ちなみに、モデル検証とモデル変換は同じようにこの方法でテストでき、また、そうすべきでもあるのだ。

適切な技術を選択する

モデルを表現し編集する多数の異なる方法がある。どの技術やシンタックスが適切であるかは、一般的な要求やプロジェクトの文化によるであろう。この点は、決め手となるドメインの型だけでなく他の選択肢と比べてどれだけ技術の適応性があるか、そして、開発プロセスでどれだけ尽力が要求されるかという問題でもある。さらに考慮すべき点は、技術がアプリケーション開発者の開発プロセスと既存のツール群に統合されるときの容易さである。開発環境 (例えば、Eclipse) への統合は今日ではほぼ必要条件である。

理想を言えば、シンタックスはコアな抽象化をはっきりと識別し、それほど問題なく拡張できるとよい。そして記述的で素早い編集に対応できるとよい。モデリング中のフィードバックは速くて広範囲に渡り、応答時間 (例えば、モデリング -> 生成 -> 実行) はできる限り短くする。

完璧な技術というものは、残念ながらまだ存在しない。例えば、UMLでのモデリングは、言語を定義すること、つまり、プロファイルの定義で時間と労力を使う小さな初期投資を意味することが多い。一方で、ほとんどの場合、結果を得るまでの時間はとても長い。DSLでもツールでもそれほど簡単には拡張できず、モデルを処理しながらとても大きく複雑なメタモデルを受け入れなければならない。私たちの経験から言うと、「本物の」DSLを使うプロジェクトの大部分はその価値がある。それぞれのドメインの要求がぴったりとはまり、重いものを何も持たないように調整してありさえすれば。

UMLをカプセル化する (他の複雑なメタモデルも)

技術を選ぶとするとすべての警告にもかかわらず、結局UMLになる。そして、ドメインでさらに簡単なメタモデルへと処理する前にUMLモデルを変換することになるのだ。これで後に続く下流のプロセス段階は、より簡単でエラーが起こりにくくなるだろう。さらに、モデルの検証は変換の後で行われるべきである。これによって、制約も簡単にドメインに特化して定式化できるようになる。

この非干渉化により、ジェネレータを変更しなくても後の段階で「本物の」DSLに移行するさらなる可能性が出てくる。

グラフィカルなシンタックスを正しく使う

グラフィカルなモデリングは大きな強みがある。それは、はっきりとした簡単な方法で重要な要素間の関係を表現する。詳細が書かれているからではなく視覚的に表現されているため、多くの情報がずっと早くまとめられる。

グラフィカルなモデルを使うとき、視覚化によって得られる可能性を活用することができる。視覚的な方法でモデル要素のもっとも重要な特徴を表現するシンタックスを見つけるようにしよう。UMLは、これに関してとても役に立つロールモデルである。この場合について例を挙げると、抽象クラスはイタリック体の名前によって識別される。属性の可視性を表すために、'#'、'+'、'-' などのオペレータを使ったアノテーションは、グラフィカルなDSLでもテキストのDSLでも両方使える。

でも気をつけて!シンタックスはあっという間に情報でいっぱいになる。図がごちゃごちゃしていたり、散らかったりしていてはいけない。例を挙げれば、テキストをあらゆる種類のフォントとフォーマットで表してはいけない。これは正しいバランスを崩すような問題である。画面設計の基本的原則はこの方面で役に立つヒントを与えてくれる。

テキストのシンタックスを正しく使う

テキストのシンタックスを定義しているときに、簡潔であることと包括的であることの間のちょうどよい妥協点にたどり着くことが重要である。モデラーがどれほどの専門性を持っていると期待されるかによって、解説のキーワードを省略することができるかもしれない。

以下でJavaの見慣れたシンタックスを比較する。それは、架空ではあるが、可能性のある代案として抽象クラスの定義のシンタックスを表わす。

JAVA:   public abstract class Foo

FIKTIV: class name=Foo visibility=public abstract=true

Javaのシンタックスが架空のものより読みやすいことはおそらく疑問の余地はない。しかしながら、それは自己表現的ではない。架空のシンタックスでは、どの属性にどの値を割り当てるかが明確になっている。ユーザはメタモデルを知っていればよいだけで、シンタックスは明らかである。XMLはちょうどこの方法で機能する。私たちの意見では、このことは明らかになっていないけれども、シンタックスが明白で簡単に理解できることは、実際にはXMLの小さな利点である。

さらに、表現力と明示性の適切なバランスは重要である。シンタックスをたくさんのシンタックスシュガーを使って表現できるかどうかはDSLのユーザ次第である。彼らがスマートでDSLをたくさん使う場合、さらに表現力が豊かになる。そうでない場合は、シンタックスは明示性を強化すべきである。

例外による設定を使う

さらに重要な点は、設定に関して正しく選択し、よく考えたアプリケーションを使うことである。すべての情報が示されるグラフィカルなシンタックスとは対照的に、通常、ある種のプロパティビューやテキストのモデリングの中で情報がデフォルトである場合、その情報が簡単に除かれることがある。

もう一度、これをJavaで見てみよう。以下に別に作成した架空のシンタックスを示す。

Java   : class Foo
Fiktiv : class Foo visibility=package final=false abstract=false

非常に簡単にわかるように、設定を正しく選択することで読みやすさは大いに改善できる。それは、最初から絶対はっきりさせておくべきであるが、これらの設定はAPIの一部でもある。デフォルトは、既存のAPIの規約を破らずに過去にさかのぼって変更できない。だからそれらを使う。でもそうする前に二度考えよう。

チームワークはテキストのDSLが好き

古典的なモデリングツールはチームワークに関して問題がある。この理由は、モデルが保存される方法にある。UMLツールは、通常1つの大きなXMIファイルにモデルを保存する。XMI (XML メタモデルインターチェンジ) は極めて技術的かつ一般的形式である。基本となるXMIモデルのコンフリクトをすでに解決しようとした人は、これがどのような問題を引き起こすのか知っている。特に、長く不可解なUUIDを使って参照することは、私たち開発者にとって単に手に余ることなのだ。そして、そのような場合にはコンフリクトを避けようとする。例えば、モデルに排他的な書き込みアクセス権限だけを与えたり、モデルに非常に細かい粒度のパーティション構造を使ったりする。

この問題の本質を探るために、読みやすく簡単にメンテナンスできるフォーマットでモデルを保存することも考えるべきである。今日、1つのDSLで複数のシンタックスを持つのは全く可能であり、例えば、テキストのDSLにグラフィカルなエディタを使うことさえできる。

多くの専門的なUMLツールは、チームサーバがあり、チームは異なった方法でモデリングできる。残念ながら、これはツールチェーンを壊すことにもなる。ソースコードと同期化するために、- MDSDのモデルはソースコードだ! - 2つの別々なツールと2つの別々なリポジトリを使わなければならないのである。これは、統合ビルドなどに関して、さらにマイナスの影響がある。

複雑さを減らすためにモデル変換を使う

複雑なコードジェネレータは、変換 - モデルからコードへの段階 - が大きい場合に生じる。この複雑さを扱う1つの方法は、ジェネレータを2つの部分に分けることである。(「分断攻略」)

このプロセスで、生成するコードにできるかぎり近くなるようにモデルは設計される。モデル変換はこの入力モデルを新しいメタモデルとして同じタイプの一時モデルに変換する。そして、コードジェネレータは一時メタモデルからターゲットプラットフォームにマップするだけである。

この手順には、メタモデルを通して明らかなインタフェースを作成するといううれしい副作用がある。各部分は異なるチームで開発され、他のシナリオでお互いに独立して再利用できる。

もちろんそのような一時的段階は、必要なだけコードジェネレータに統合できる。実際には、経験によれは、1つの変換 (もちろん多くの場合、一つもないが) で完全に十分である。モデル変換を使う場合、MDAでは多くのカスケードレイヤーを推奨する。これは、さまざまなプラットフォームを分離するためである。著者の控えめな意見では、これは実際のところ実行可能ではない。もちろん、「例外は規則がある証拠」という古い格言がここに当てはまることは疑いない :-)

広範囲のプラットフォームのために生成する

モデルからコードへの変換をできる限り小さくして、ジェネレータの複雑さを最小限にするもう1つの重要な方法がある。それは、できる限り遠いところでジェネレータに対応するプラットフォームを開発することである。例えば、持続レイヤーを生成する代わりに、適切なフレームワーク (例えば、JPA実装) を使うことができる。他のプロジェクト特有の抽象化は、コードを生成するこのプラットフォーム (AbstractEntity-Classなど) で開発する。

結論

ここで述べられたベストプラクティスは、実際に数年間集められた私たちの経験 (同僚や友達の経験も含む) を反映している。読者に対するもっとも重要なアドバイスは、実用的であることである。DSLやコードジェネレータは、適切に使えば非常に役に立つツールだ。しかし、常に解決される問題に注目すべきである。多くの場合、DSLを使って全部ではないが一部のアスペクトを記述するのは意味がある。最初からモデル駆動の手法に従うことを決めたプロジェクトは、この最後のアドバイスを無視している。MDSD技術を利用するかどうか、またどうやって利用するか確かでない場合は、その分野のコンサルタントからいくつか専門的アドバイスをもらおう。

用語解説

受動的ジェネレータ [4]: は一度呼ばれる。作成されたコードは、それ以降、手動で開発される。開発環境におけるほとんど全部のウィザードはこのカテゴリに入る。

アクティブジェネレータ [4]: は何度も何度も呼び出される。設定ファイルやモデルを使って構成する。MDSDジェネレータは典型的にこのカテゴリに入る。

著者について

Sven Efftinge氏は、itemis AG社のキール支社を率いる。彼は、Textual Modeling Framework (TMF) のプロジェクトリーダーであり、他のEclipseモデリングプロジェクトのコミッターでもある。さらに、openArchitectureWare 4 と Xtextフレームワークのアーキテクト、かつ、開発者である。

Peter Friese氏は、itemis AG社のソフトウェアアーキテクト、かつ、MDSDの専門家である。彼は、ソフトウェア開発のモデル駆動プロセスアプリケーションとソフトウェアツールの開発で広範囲におよぶ経験を持つ。Peter氏は、Eclipseモデリングプロジェクトのコミッターであり、オープンソースプロジェクトのopenArchitectureWare、AndroMDA、そして、FindBugsにも参加している。

Jan Kohnlein博士もまたitemis AG社のソフトウェアアーキテクトとして働く。彼は、数年間、MDSDのための開発ツールを設計しており、EclipseモデリングプロジェクトとopenArchitectureWareのコミッターでもある。

3人の著者は itemis AG社 [8] に勤務する。Eclipseモデリングプロジェクトでさらなる開発に集中し、この分野で専門的なサービスを提供する。

リンクと文献

[1] Stahl, Volter: モデル駆動ソフトウェア開発
[2] Se-Radio : MDSDに関するエピソード(リンク)
[3] Volter, Bettin: MDSDのパターン(リンク)
[4] Hunt, Thomas: The Pragmatic Programmer. Addison-Wesley, 2000 (邦題 : 達人プログラマ)
[5] Evans, Eric: Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley Longman, 2003
[6] 生成したコードを生成していないコードとどうやって区別するか(リンク)
[7] openArchitectureWare (リンク)
[8] itemis (リンク)

原文はこちらです:http://www.infoq.com/articles/model-driven-dev-best-practices
(このArticleは2008年6月25日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT