はいさい、ちゅらデータぬオースティンやいびーん!
今回の記事では、オブジェクト指向の反省から生まれた、SOLID主義について解説すると共に、それをフロントエンド開発でどう活かすべきかについての意見を書いていきたいと思います。
指摘があれば、ぜひコメントしていただければと思います!
フロントエンドの実装は、ぐちゃぐちゃコードになりがち
フロントエンド開発に関わっている人なら誰しもわかること。
それは、フロントエンドのコードが難解になりがち、というところです。
なぜかにはいくつかの理由があります。筆者が思い当たるところは以下の通りです。
- HTML、CSS、JavaScriptという、三つの言語が関わってくる
- ビジネス・ロジックがフロントエンドに迷い込みがち
- フロントエンド開発が軽視されがち
- ジュニア・エンジニアにフロントエンドが任されることが多い
これらの理由について少しばかり解説します。
HTML、CSS、JavaScriptという、三つの言語が関わってくる
無論、読者はご存知のはずですが、フロントエンド開発には実にさまざまな知識を要求されています。
ましてや、ユーザー体験の充実した、保守性の高いコードを書こうと思えば、ますます高いスキルが求められます。
HTML、CSSはそもそも言語ではないという考え方がありますが、これは違うと思います。
以前のHTMLならともかく、HTML5でできることが実に多く、HTMLだけで完結する機能が多くあります。特に<video>など、あの辺は奥が深いです。また、<input>のバリデーションも非常によくできています。
CSSもまた、ないがしろにされがちではないでしょうか?筆者は特にそうです。
CSSの擬似クラスをよく心得ている先輩がいますが、彼の書いたコードを読むといつもびっくりします。CSSでできることは実に多いです。
HTML、CSS、そしてそこにJavaScriptも入れば、まともなフロントエンドを書くには幅広い知識が必要。
HTMLか、CSSか、JavaScriptか、どこでやればいいかわからないと、冗長でわかりにくいコードが出てくる、しょうがない気がします。
ビジネス・ロジックがフロントエンドに迷い込みがち
特に最近、バックエンドはAPI、フロントエンドはReact・Vueなどのフレームワークを使ってバックエンドとフロントエンドを分ける傾向があるかと思います。
これの良いところを言えば、バックエンド・フロントエンドの役割分担がしやすくなったことが大きいかと思います。特にバックエンドの自動テストは書きやすくなりました。
悪いところを言えば、以前まで主流だったModel-View-Controllerを否定するような風潮が生まれていること。もしくはその良さが伝わっていないような、そんな気がします。
つまり、バックエンドでやっていたビジネスロジックを、フロントエンドでやることが増えている、そのような気がしませんか?
本来なら、フロントエンドは、View、つまり情報を見せて、ユーザーの入力を受け取る役割しかなかったのですが、その境界線がぼやけている気がします。
フロントエンドにビジネス・ロジックが入るようになったことが悪いことかどうかは筆者は分かりません。
しかし、ビジネス・ロジックをフロントエンドに持ち込むと、否応なくそれだけ複雑になるのは否定できません。
フロントエンド開発が軽視されがち
締め切りが迫っていて、重要なバックエンド・ロジックにより多くの時間を費やし、フロントエンドに割ける時間が十分じゃない、このようなこと、ありませんでしょうか?
実際、これはよくあることではないかと思います。
ジュニア・エンジニアにフロントエンドが任されることが多い
上記の理由にも関係しているのですが、やはり重要なロジックにはシニア・エンジニアが割り当てられ、失敗しても障害はそんなに深刻にならないであろうところに、ジュニア・エンジニアが回される。
フロントエンド開発で叩き上げられ、ミドル・クラスになると、AWSを触ったり、バックエンドのコア・ロジックに触れたりするようになります。
最初から、バックエンドのコアなロジックを、入ったばかりのジュニア・エンジニアに任せようと考えるマネジャーはいません。
フロントエンドで確実に技術を高めて、また、信用を得てからこそ、重要な仕事を任されるようになる。
これは人情ですし、正しい道です。
ただ、そのせいで、やはりフロントエンドはジュニア・エンジニアの半分勉強の砂場にも化してしまうので、わけのわからないコードが多発しやすい。
筆者が以前書いたフロントエンドのコードはまさに、英語でいうと、
Cluster ****
でございます。今もまだそうかもしれません...
フロントエンド開発の大変さからチームを救ってくれるのは、SOLID主義。
SOLID主義は、オブジェクト指向開発における、主にJavaの開発かと思いますが、保守性の問題を解決するための思想です。
言葉の頭文字を集めて、格好よくSOLIDと命名したのです。
S : Single Responsibility
一つのクラスには一つの役割しかない。
(最近の)React・Vue開発においては、クラス構文をしていないので該当しない、とは言えません。
Reactの部品は、Functional Programmingに見えて、中身を見ると、オブジェクトそのものです。Vueも然り。
フロントエンドの部品開発に置いても、部品には一つの役割があり、部品が更新される正当な理由は一つだけ、そう考えて作ってみれば、意外と部品の責務の境界線が明瞭に見えてくるものです。
例えば、新規投稿を送信する部品を考えれば、役割は一つ、入力した情報の妥当性を評価して妥当な入力であれば送信する、それだけです。
部品の役割という単位で考えて分けると、コードが完結して分かりやすいものです。
部品 => Single Responsibility.
O: Open-Closed Principal
これは、ソフトウェアは拡張しやすいように作り、機能追加で既存のコードを大幅に変更しなくていいように作ろう
という考え方です。
フロントエンド開発の部品に置き換えれば、部品のロジックに新しい関数、メソッドを追加しても既存の関数、メソッドを大幅に変更しなくても、それらが新機能でも使えるように作るべきだということです。
最近、筆者は既存のデータを編集するフォームを部品として書いて、その部品にはフォームの他に、モーダルも入っていました。
コードレビューで指摘されたのですが、
この部品をフォームのみにしておけば、今度、モーダルじゃなくて、
そのまま使いたい時に、この部品をわざわざ修正せずに済むじゃないか?
まさにその通りだと思います。これこそがフロントエンドにおけるOpen-closed principalだと思います。
L: Liskov Substitution Principle
簡単にいうと、違うクラスを継承している場合は、そのクラスの機能に触れずに使える
こと。
これはLitもしくは単なるWeb Componentsを使っていないと応用が効かない考え方だと思います。
Litだと、LitElement
というクラスを継承して部品を作りますが、クラス構文を書いているのでさらに継承することができます。
例えば、LitElementはデフォルトではShadow DOMにレンダーしますが、以下のようにLight DOMにレンダーするようにできます。
createRenderRoot
を、Light DOMのRootを返すように上書きしたLightDOMLitElement
というクラスを一回作れば、他のところでは、このクラスを継承さえすれば、どうやってLight DOMにレンダーしているのかを考えずにコードが書けるのです。
import { LitElement } from "lit";
export default class LightDOMLitElement extends LitElement {
protected createRenderRoot() {
return this;
}
}
他の部品ではこうするだけです。
import LightDOMLitElement from "./lit-dom-lit-element";
import { html } from "lit";
export default class MyComponent extends LightDOMLitElement {
render() {
return html`<h1>Light DOM! Yay!</h1>`
}
}
Reactだと、Custom Hooksのことに当たるかと思います。そのCustom Hooksの仕組みをいじらなくても、部品で使えるように作ろう、という話かと思います。
個人的にはDRY(Don't Repeat Yourself、同じコードを数箇所書くな)に似ている気がしますがいかがでしょうか?
そういう意味では、むやみにコードを抽象化しないことも大事かと思いますが、それは別の話でしょう。
このように、共通する機能を、その仕組みを意識せずに利用できるようにするのがLiskov Substitution Principleで、フロントエンド開発でも活かせる場面があります。
I: Interface Segregation Principle
コードは、使用しない関数・変数に依存してはならない
という考えです。Segregation = 分けるという意味ですが、インターフェイス分離ということです。
つまり、上記の例で挙げた新規投稿を扱うフォームをもう一度考えれば、新規投稿の入力を受け取り、妥当性を評価し、送信するというのが役割であれば、
- その部品が既存の投稿を保持する必要はない
- ユーザーがどのページにいるかなどの情報を知る必要もない
- ユーザー認証を考慮する必要はない
これらは全て違うインタフェース=部品もしくはバックエンドでやるべきことなので、このフォームの部品はそれらを知らなくていいです。
Reactで言うと、Prop Drillingという現象があります。Prop Drillingとは、親部品から小部品Aの小部品Bに渡したいPropを、小部品Aでは使わないので、バトンタッチしないといけない問題のことです。
<ParentComponent>
<ChildComponentOne data={props.data}>
他に関係のないことをやっている...
<ChildComponentTwo>
{props.data}
実際、ここでdataをレンダーするようなこと
</ChildComponentTwo>
</ChildComponentOne>
</ParentComponent>
Interface Segregation Principleさんに言わせれば、これは良くないことです。
React・Vueで開発した人なら、これはいやというほどわかるはずです。
ここで、解決になるのに以下のような方法でしょう。
-
本当に
<ParentComponent>
がそのdata
を保持するべきかをよく考える - ReactのContext APIを使う
- Reduxのような中央ステートライブラリーを使う
このように、とある部品に関係のないロジックが入っており、そのロジックに依存していると、でバギングも大変になるし、保守性も悪くなる。
なので、使われないコードに依存するような部品を書かないようにしましょう!
D: Dependency Inversion Principle
- 上級モジュールは下級モジュールからインポートするべきでない
- 抽象モジュールには、詳細的な実装をするべきでない
つまり、上記のLightDOMLitElementでいうと、以下のように詳細を入れた実装をするべきではないということでしょうか。
import { LitElement } from "lit";
export default class LightDOMLitElement extends LitElement {
protected createRenderRoot() {
return this;
}
firstUpdated() {
super.firstUpdated();
this.querySelector("h1")!.styles.color = "red";
}
}
import LightDOMLitElement from "./lit-dom-lit-element";
import { html } from "lit";
export default class MyComponent extends LightDOMLitElement {
render() {
return html`<h1>Light DOM! Yay!</h1>`
}
}
なぜなら、
- 抽象クラスなのに、
this
つまりこのクラスを継承してLight DOMにレンダーする<my-component>
のような部品に<h1>がレンダーされることがないとエラーになる - 抽象クラスなのに、<h1>の文字の色を定めている
またReactでいうと、クラスを継承するので、1番目の上級モジュールは下級モジュールからインポートするべきでない
は該当しないとしても、
2番目の抽象モジュールには、詳細的な実装をするべきでない
は、Custom Hooksではよくある問題かと思います。
以下の例で考えましょう。
import { useEffect, useState } from "react";
export const useData = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch("https://somewhere.com?page=1")
.then(result => result.json())
.then(data => setData(data));
});
return data;
};
この例だと、useData
では、Fetch先のURLをハード・コードしているので、このCustom Hookは汎用性がないですね。
まとめ
これまで、SOLID主義がフロントエンド開発でどう活かせるかについて解説して来ましたが、いかがでしょうか?
フロントエンド開発は本当に本当に重要なものなのだと、筆者は思っています。
ユーザーは、バックエンドのことを知らないのです。
ユーザーが見るのは、フロントエンドです。
ユーザー体験が悪ければ、せっかく作ったサービスは、使われないし、その価値をユーザーにわかってもらえない。
バグのない、気持ちのいいユーザー体験を実現するには、しっかりしたコードベースが必要です。
そのためには、我々フロントエンド・エンジニアは誇りを持って毎日努力して、締め切りが許す限り保守性・品質の高いを書く必要があります。