2015年4月13日
@extendを使うべき時、@mixinを使うべき時
(2014-11-20)by Harry Roberts
本記事は、原著者の許諾のもとに翻訳・掲載しております。
(編注:2020/08/18、いただいたフィードバックをもとに記事を修正いたしました。)
私がクライアントからよく受ける質問に 「@mixinと@extend、それぞれどのような時に使うべき?」 というものがあります。
“引数を使わない@mixinは悪である”。 これは以前からある経験則です。同じコードを2つのインスタンスで重複させるだけの@mixinは不快でさえあります。しかし、@extendを使うべき時、@mixinを使うべき時、これらを見極めることにはもっと深い意味があるのです。
それでは詳しく考察していくことにしましょう。
@extendを使うべき時
私は普段、@extendは決して使わないようにとアドバイスしています。@extendには、一見したところ魅力的な特徴がたくさんあるのですが、注意しなければいけない点はそれ以上にあります。言ってしまえば 見かけ倒し だということです。
それでも@extendを使いたいなら、下記のことを守る必要があります。
1 本当に必要かどうか再度よく考える
2 プレースホルダセレクタ を使う
3 出力の際は細心の注意を払う
@extendは理論上ではとても優れているのですが、実際のところ、意図しない結果を招く可能性が非常に高いのです。@extendを使ったことで、スタイルシートが倍以上のサイズになってしまったり、ソースの順序がめちゃくちゃになってしまったり、 セレクタが4,095個に達してしまったりしたケース をたくさん目にしてきました。重大な問題が起きる可能性が少しでもあり、得られるものがほとんどないのなら、あえてそのような機能やツールを使う必要はないでしょう。用心するに越したことはないのです。開発支援ツールを正しく使わなかったために、スタイルシートが4,095個を超えるセレクタ群になるなどということは、全く馬鹿げています。
注:@extendの存在自体を全否定しているわけではありません。ただ、@extendを使うのなら多くのことに注意をしなければならないし、油断もできないのだということを言いたいのです。
では、@extendを使うべきなのは、一体どんな時なのでしょうか。
まず重要なのが、@extendが セレクタのグルーピングを行う ということを理解しておくことです。@extendを使う時、あなたは他のセレクタと特徴を共有するために、あるセレクタをスタイルシートの別の場所に移植しています。そして他のセレクタもまた移植されます。結果として、あなたはこれら全てのセレクタのグルーピングを行っているということになります。 @extendを誤って使ってしまうことにより、不適切な基準に基づいたグルーピングがなされてしまう可能性があるのです。 言うなればCDのコレクションをジャケットの色でグルーピングするようなものです。確かにある種のグルーピングではあるのですが、有用な関連性は生まれません。
つまり、適切な特徴に基づいたグルーピングが不可欠であるということです。
私自身も身に覚えがあるのですが、しばしばこのようなものを見かけます(“…”は例えば100行が省略されていると思ってください)。
%brand-font {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
h1 {
@extend %brand-font;
font-size: 2em;
}
...
.btn {
@extend %brand-font;
display: inline-block;
padding: 1em;
}
...
.promo {
@extend %brand-font;
background-color: #BADA55;
color: #fff;
}
...
.footer-message {
@extend %brand-font;
font-size: 0.75em;
}
これはもちろん、以下のようなCSSを与えてくれます。
h1, .btn, .promo, .footer-message {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
h1 {
font-size: 2em;
}
...
.btn {
display: inline-block;
padding: 1em;
}
...
.promo {
background-color: #BADA55;
color: #fff;
}
...
.footer-message {
font-size: 0.75em;
}
ここで問題なのは、お互いに何百行も離れた位置に記述されているような関連性のないルール同士を、偶然特徴を共有しているというだけで無理やりグルーピングしてしまっているということです。それだけでなく、ソースの順序においても詳細度がめちゃくちゃになった異常なものを生み出してしまっています。コードベース上のセレクタを全くの偶然で分類してしまっているのです。これは 良くないニュース です。
他のルールセットと特徴を共有するために、全くの偶然と場当たり的な共通点だけで、ソースから何百行も離れた不適切な位置に無関係なルールセットを移植してしまいました。 これこそが@extendの良くない使い方です (実際には、このケースでは引数を持たない@mixinが最適です。@mixinについてはあとで述べます)。
他の@extendの乱用事例として、こんなものもあります。
%bold {
font-weight: bold;
}
...
.header--home > .header__tagline {
@extend %bold;
color: #333;
font-style: italic;
}
...
.btn--warning {
@extend %bold;
background-color: red;
color: white;
}
...
.alert--error > .alert__text {
@extend %bold;
color: red;
}
この結果はご想像の通り、以下のようになります。
.header--home > .header__tagline,
.btn--warning,
.alert--error > .alert__text {
font-weight: bold;
}
...
.header--home > .header__tagline {
color: #333;
font-style: italic;
}
...
.btn--warning {
background-color: red;
color: white;
}
...
.alert--error > .alert__text {
color: red;
}
これは299バイトになります。
移植したセレクタは、しばしば繰り返しを避けようとした宣言よりも長くなります。
今回、繰り返しを避けずに実際にfont-weight:bold;をn回宣言していれば、 264バイト にファイルサイズを縮小することができていたのです。簡単な例ですが、せっかくの労力がむしろ意図したものと逆の結果を生んでしまう可能性を証明しているといえるでしょう。1つの宣言を@extendすることはしばしば逆効果になるのです。
では、 いつ@extendを使えばいいのでしょうか?
@extendは、明確に関連しているルールセット同士で特徴を共有するときに使用します。以下が完璧な例です。
.btn,
%btn {
display: inline-block;
padding: 1em;
}
.btn-positive {
@extend %btn;
background-color: green;
color: white;
}
.btn-negative {
@extend %btn;
background-color: red;
color: white;
}
.btn-neutral {
@extend %btn;
background-color: lightgray;
color: black;
}
結果はこうなります。
.btn,
.btn-positive,
.btn-negative,
.btn-neutral {
display: inline-block;
padding: 1em;
}
.btn-positive {
background-color: green;
color: white;
}
.btn-negative {
background-color: red;
color: white;
}
.btn-neutral {
background-color: lightgray;
color: black;
}
これが@extendの正しい使い方です。これらのルールセットは継承関係にあります。特徴は理由があって共有されており、偶然に基づいてはいません。加えて、セレクタをソースから何百行も離れたところに移植していることもなく、 詳細度グラフ も問題ありません。
@mixinを使うべき時
引数を使わない@mixinは悪であるという主張には一理ありますが、残念ながら事はそこまで単純ではありません。
このルールはDRYの原則への誤解から生じています。DRYはプロジェクトにおける Single Source of Truth を守るための原則です。DRYは 自分自身を 繰り返すなと言っているのであって、繰り返しそのものを一切避けろとは言っていないのです。
自分自身を繰り返す、というのはつまり、宣言を50回手動で入力する、というようなことです。これはDRYに反しています。もし手動ではない方法で宣言を50回繰り返したとすると、直接的に同じものを書いているわけではないので、それはDRYに反してはいません。この2つの違いは微妙ですが、この違いを理解することはとても重要です。 コンパイルされたシステムでの重複は悪いことではありませんが、ソース内の重複はいいことではありません。
「Single Source of Truth」というのは、繰り返されるソースを一カ所に保持し、手動ではない方法で使い回したり再利用したりできるソースのことです。もちろん、システムによって繰り返されているのかもしれません。しかしそれでも、そのソースは1つしか存在しません。つまり、ソースを変更すれば、その変更が別の場所にも適用されます。ソースコード内で複製をしなくていいということです。これが、Single Source of Truthです。DRYの原則に従うには、この考え方がとても重要になります。
これを念頭に置けば、引数を使わない@mixinが実際にとても便利であることが理解できるはずです。では、先ほどの%brand-font{}の例に戻りましょう。
特定のfont-weight特定のフォントを使用したプロジェクトをイメージしましょう。プロジェクト内では常に特定のfont-weightを合わせて決定づける必要があります。
.foo {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
.bar {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
.baz {
font-family: webfont, sans-serif;
font-weight: 700;
}
コードベースで、これらの2つの宣言を手動で何度も繰り返すのは、かなり面倒でしょう。なじみのあるregularやboldではなく、700という数字を扱わなくてはいけませんし、もしwebフォントやfont-weightをどこかで変更すれば、プロジェクトの全体を通して、全てを変更していかなくてはなりません。
@extendを無理やり使うべきではないと前述しましたが、ここで使うべきなのが@mixinです。
@mixin webfont() {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
.foo {
@include webfont();
}
...
.bar {
@include webfont();
}
...
.baz {
@include webfont();
}
そうです、この例ではコンパイルにより同じコードが複製されるのであり、自分で複製したわけではありません。 ここで重要なことは、これらは関連性のないルールセットであるということです。つまり、ルールセットに関連性を持たせてはいけないということです。関連性があるのではなく、偶然いくつかの共通の特徴を持っているだけのことです。この複製は理にかなっています。そして、予測されていたものになります。これらの宣言をnカ所で使いたければ、宣言をnカ所に出現させればいいのです。
Single Source of Truthを保ちながら、繰り返された同一の宣言を吐き出すだけであれば、引数を使わない@mixinがとても便利です。@mixinはクリップボードのコピー&ペーストのSass版拡張のようなものだと思ってください。つまり、様々な場所で記憶したいくつかの文字列を@mixinでペーストするだけです。これはSingle Source of Truthの考え方に則っています。手動で1度変更を加えるだけで、同一の宣言を全て変更することができるのです。これはかなりDRYですね。
また、Gzipが重複を好むのも注目すべきことです。Gzipはファイルサイズがわずかに大きくなってもそのコストはほとんど無視できるのです。
もちろん@mixinは、繰り返されるソースの中で動的値を生成する時には非常に便利です。この場合は引数を使った@mixinです。これがいいアイデアではないと否定する人は誰もいないと思います。これらはDRYですし、さらにSingle Source of Truthを瞬時に修正できるようになるのです。以下の例を見てみましょう。
@mixin truncate($width: 100%) {
width: $width;
max-width: 100%;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.foo {
@include truncate(100px);
}
同じ宣言を吐き出しますが、ケースバイケースで動的なwidthの値をセットします。
これは@mixinの書式で最も一般的で広く認められたものです。そして、私はこれらのアイデアは素晴らしいと全員が賛同してくれると思っています。
要約
@extend は、DRYにしようとするルールセットが本質的、テーマ的に関係性がある時だけ使いましょう。存在しない関連性を無理に作ってはいけません。もし、それをやってしまうと、プロジェクトの中で異常なグルーピングが行われてしまい、コードのソースの順序に悪影響を及ぼしてしまいます。
@mixinを使うのは、重複する箇所に値を動的に挿入する時か、 Single Source of Truth を保ちながらプロジェクト全体を通して同じ宣言のグループを繰り返すことのできるSass版のコピー&ペーストとして使う時にしましょう。
要約の要約
@extendは「理由があって同じ」である場合に使いましょう。@mixinは「偶然同じ」の場合に使いましょう。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa