新版: やばい臭いのするCSSコード(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

新版: やばい臭いのするCSSコード(翻訳)

今をさかのぼること2012年に、Code Smells in CSSというタイトルでCSSに潜むアンチパターンについての記事を公開しました。その後4年が経過し、記事の内容についての考えは今も変わっていませんが、さらに追加すべきリストが増えました。繰り返しますが、これらはあくまでコードから立ち上るやばい臭いという兆候であり、これがあるからといって必ずしも絶対的な悪手ではないことにご注意ください。ユースケースによっては全面的に容認できることもあるかもしれません。それでもやばい臭いが残ることに変わりはありませんが。

本題に入る前に、「コードの臭い(訳注: リンク先は日本語版です)」とは何かをおさらいしておきましょう。以下はWikipediaからの引用です(訳注: 強調は原著者)。

コードの臭い(または悪臭)とは、コンピュータプログラミングにおいてソースコードにより深刻な問題が潜んでいる可能性をうかがわせる何らかの兆候を指す。Martin Fowler曰く「コードの臭いとはシステム内のより深刻な問題につながりやすい表面的な兆候である」。
他の観点として、法則と品質に基づく方法がある。「臭いとは、基本的な設計法則に反し、設計の品質が損なわれていることを示す、コード内の特定の構造である」。
通常、コードの臭いそれ自体はバグではなく、技術的に誤っているわけでもなければ、現在のプログラムの機能を損なっているわけでもない。
むしろコードの臭いは、将来において開発速度を低下させたりバグや機能不全などのリスクを増加させる、設計上の弱点を指す。コードの悪臭は、技術的負債をもたらす要素の存在を示すことがある。
Robert C. Martinはコードの臭いのリストを、ソフトウェアの技能に役立つ「価値体系」と呼んでいる。

つまりコードの臭いは技術的に誤っているとは限らないものであり、単にリトマス試験紙として有用であるということです。

1. @extendを使っている

最初のトピックは期待どおりうまく短くまとめられました。筆者は@extendには副作用と落とし穴があると長年主張し続けてきましたが、今こそ@extendをコードの臭いリストに加えたいと思います。@extendは別に絶対悪ではありませんが、多くの場合悪手です。@extendは疑いの眼差しを持って取り扱いましょう。

@extendが引き起こす問題はさまざまですが、以下のようにまとめられます。

  • @extendの実際のパフォーマンスはmixinよりも落ちる: Gzip圧縮は同じコードが繰り返されていると圧縮がよく効くため、mixinなどで繰り返しが多い方が圧縮率は高まります。
  • @extendは強欲: Sassのextendがあるクラスを見つけると、そのすべてのインスタンスを@extendするため、アホみたいに長いセレクタチェーンが生成されます。
  • @extendはコードベースを引っかき回す: ソースの秩序はCSSの命であり、プロジェクトでセレクタがあっちこっちに動き回るような事態は常に避けるべきです。
  • @extendはコードの足跡をあいまいにする: @extendは、精密に解きほぐして追わなければならないSassの複雑さを覆い隠してしまいます。セレクタのクラスを複数使うアプローチなら、すべての情報をマークアップの中核に配置できます。

さらに詳しい英語記事

2. クラス名を&で結合している

Sassでもうひとつ言っておきたいのは、次のように&でクラス名の文字列を結合する手法についてです。

.foo {
  color: red;

  &-bar {
    font-weight: bold;
  }

}

上のSassから以下のCSSが生成されます。

.foo {
  color: red;
}

.foo-bar {
  font-weight: bold;
}

この手法の明らかなメリットは、無精できるという点です。fooという名前空間を1度書けば済むので、コードは間違いなくDRYになります。

しかしここには、ソースコード内にfoo-barという文字列が存在しなくなってしまうというデメリットが潜んでいます。foo-barという文字列をコードベースから検索するなら、HTMLかさもなければコンパイル済みCSSを検索しないと見つけられません(それもプロジェクトにコンパイル済みCSSをチェックインしていればの話ですが)。ある日突然、.foo-barというスタイルをコードから探し出すことが非常に困難になっていることに気付かされます。

筆者は、CSSを省略せずに書く(ロングハンド)方法を好んでいます。つまり、クラスを&でリネームするよりもクラスを検索する方がずっと多いので、検索性の方が筆者にとって重要です。もしSassの文字列結合を激しく多用するプロジェクトに参加することがあれば、コードを追うのが大変になるだろうとだいたい想像がつきます。

もちろん、Sassのsourcemapは有用ではないかという反論もあるかと思います。.nav__itemというクラスを探すなら、nav.scssファイルを開くだけでよいというわけです。残念なことに、これはいつもうまくいくとは限りません。詳しくお知りになりたい方は、筆者のscreencastをご覧ください。

3. backgroundのあいまいなショートハンド

最近筆者が他に議論したことといえば、backgroundショートハンドの文法ぐらいです。詳しくは関連記事をご覧いただくとして、ここでは簡単に以下のようにまとめてみました。

.btn {
  background: #f43059;
}

以下を期待して上のCSSを書いているとします。

.btn {
  background-color: #f43059;
}

これがコードの臭いのもうひとつの事例です。

前者のCSSが使われていると、開発者がほぼ常に後者を意図していたとしても、そのとおりになるとは限りません。後者は単に背景色を設定または変更するだけですが、前者は背景画像や位置や添付ファイルなどをリセットしたり設定を取り消したりすることもあります。

私はこうしたbackgroundのショートハンドがプロジェクトで使われているのを見た瞬間、いつかきっとプロジェクトで問題が発生するという嫌な予感がよぎります。

4. 同じキーセレクタがあちこちで使われている

訳注: Mozillaドキュメントから以下を補足します。
IDセレクタ、クラスセレクタ、タグセレクタのうち最も右に現れるセレクタをキーセレクタと定義します。
Writing Efficient CSS: スタイルシステムによるルール分類より

キーセレクタとは、実際に対象を指定してスタイルを与えるセレクタのことです。しかしキーセレクタは、いま作業している開き波かっこ{の直前にあるものだけとは限りません。次のCSSをご覧ください。

.foo {}

nav li .bar {}

.promo a,
.promo .btn {}

上のCSSにあるキーセレクタは、以下の4つです。

  • .foo
  • .bar
  • a
  • .btn

仮にこのコードベースを採用して.btnがどう展開されるかをチェックすると、次のような出力になる可能性がありえます。

.btn {}

.header .btn,
.header .btn:hover {}

.sidebar .btn {}

.modal .btn {}

.page aside .btn {}

nav .btn {}

CSSのひどさはさておき、ここで問題にしたいのは.btnが繰り返し定義されている点です。ここから次のことがわかります。

  • ボタンがどのように表示されるかを示す真に一意な情報源が存在しない
  • 多くの改変が発生する: ミュータブルなCSSによって.btnクラスが異なる結果を出力する可能性がある

筆者がこんなCSSを目にしたら、ボタン表示をどう変えても広範囲に渡って影響が生じ、ボタンのスタイルがどこから来ているのかを正確に追うことがきわめて難しくなるという事実をただちに悟るでしょう。何をどう変えても、画面のあちこちで大量の連鎖反応が引き起こされます。これこそがミュータブルCSSの重大な問題のひとつです。

解決方法: BEM↓などの手法を用いて、重複のない完全に新しいクラスを作成しましょう。

.btn {}

.btn--large {}

.btn--primary {}

.btn--ghost {}

上のCSSのキーセレクタはいずれも重複していません。

5. 他のコンポーネントのファイルで使われているクラス

この問題は上と一見似ていますが、根本的に異なります。他のコンポーネントのファイルで使われているクラスを見つけたら、それはコードの臭いの兆候です。

前述のコードの臭いでは、同じキーセレクタのインスタンスが複数あることを問題にしましたが、こちらはそうしたセレクタがどこに潜んでいるかを問題にしています。Dave Rupertから寄せられた疑問をご覧ください。


以下はthing.cssとsome-context.cssのどっちに置けばいいの?

.some-context .thing { /* 特殊なルールやオーバーライド */ }

コンテキストに応じてスタイルを変える必要がある場合、追加のCSSをどこに置くべきでしょうか。

  • thingのスタイルを設定するファイルに置くべきか?
  • コンテキストを制御するファイルに置くべきか?

次のCSSがあるとしましょう。

.btn {
  /** 何かスタイルを書く **/
}

.modal .btn {
  font-size: 0.75em;
}

この.modal .btn {}はどのファイルに配置すべきでしょうか。

答えは「.btnのあるファイルに配置すべき」です。

スタイルは、可能な限り(キーセレクタなどの)主題に基いてグループ化すべきです。上の例では.btnが主題であり、実際に扱う対象です。.modal.btnのコンテキストに過ぎないため、ここで.modalにスタイルを設定することはまったくありません。要するに、.btnのスタイル設定を他のファイルに書き出すべきではありません。

.btnのスタイル設定を他のファイルに書き出すべきではない理由は、言語学で言う「語と語の自然なつながり(コロケーション: collocation)」に集約されます。使うボタンのすべてのコンテキストは1か所にまとめる方がずっと便利です。プロジェクトですべてのボタンのスタイルをざっくりと把握したいとき、私なら_components.buttons.scssを開くだけで済むことを期待します。他のファイルをあれこれ開きたくはありません。

こうしておけば、全ボタンのスタイルを新しいプロジェクトにそっくり持っていくのが非常に楽になります。しかしもっと重要なのは、認知上のオーバーヘッドを和らげられる点です。ほんのわずかなスタイル変更のために、テキストエディタで10個もファイルを開かなければならないときの気持ちは皆さんもよくご存知でしょう。こうした事態を避けることができます。

スタイルは主題に基いてファイルにグループ分けしましょう。スタイルの設定対象がボタンであれば、それがどんなCSSであれ、_components.buttons.scssにまとめるべきです。

簡単な秘訣として、「今自分が設定しているのはxのスタイルか、それともyのスタイルか」を自問自答するとよいでしょう。xのスタイルを設定しているのであればx.cssに保存すべきですし、yのスタイルを設定しているのであればy.cssに保存すべきです。

おすすめ: BEMミックス

ここまで書いておいてこう言うのも何ですが、私は前述のCSSはまず書きません。私なら実際にはBEMミックスを使うでしょう。しかしこれは、また別の疑問への回答です。

// _components.buttons.scss

.btn {
  /** 何かスタイルを書く **/
}

.modal .btn {
  /** 何かスタイルを書く **/
}

// _components.modal.scss

.modal {
  /** 何かスタイルを書く **/
}

上のように書く代わりに、次のように書きます。

// _components.buttons.scss

.btn {
  /** 何かスタイルを書く **/
}


// _components.modal.scss

.modal {
  /** 何かスタイルを書く **/
}

  .modal__btn {
  /** 何かスタイルを書く **/
  }

この第3の新しいクラス.modal__btnは、次のような感じでHTMLに適用されます。

<div class="modal">
  <button class="btn  modal__btn">Dismiss</button>
</div>

これがBEMミックスと呼ばれる手法です。ここではモーダルダイアログに属するボタンを参照する第3の新しいクラスを導入しています。

この手法ならコードの置き場所で悩まずに済みますし、ネストを避けられるのでコードを特殊化せずに広範囲に適用でき、かつ.btnクラスの繰り返しを避けられるので動作の改変も防止できます。まるで魔法のようです。

6. CSSの@import

CSSの@importについては、単なるコードの臭いどころか、むしろはっきりバッドプラクティスであると主張いたします。重要なアセットであるCSSダウンロードのパフォーマンスが大きく損なわれ、必要なタイミングより読み込みが遅れてしまいます。@importでCSSがダウンロードされるときのワークフローを単純化すると、以下のような感じになります。

  1. HTMLファイルを読み込み、そこからCSSファイルがリクエストされる
  2. CSSファイルが読み込まれ、そこから別のCSSファイルがリクエストされる
  3. 最後のCSSファイルが読み込まれる
  4. ページのレンダリングが開始される

仮に@importを1つのファイルに固めたとしたら、ワークフローは次のような感じになるでしょう。

  1. HTMLファイルを読み込み、そこからCSSファイルがリクエストされる
  2. CSSファイルが読み込まれる
  3. ページのレンダリングが開始される

CSSを1つのファイルに固められない事情がある場合(Google Fontsにリンクしている場合など)は、@importではなく、<link />要素を2つ使うべきです。この方法ではカプセル化がわずかに損なわれます(CSSファイルの依存関係をすべて扱える方がありがたいでしょう)が、それでもパフォーマンス面でずっと有利です。

  1. HTMLファイルを読み込み、そこからCSSファイルがリクエストされる
  2. CSSファイルが2つとも読み込まれる
  3. ページのレンダリングが開始される

そういうわけで、前回の「コードの臭い」リストに2項目を追加しました。筆者の「コードの臭い」リストには、筆者が常日頃から直面している問題が盛り込まれています。願わくば皆さまもこうした問題を避けられますように。

関連記事

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ