あなたはクリーン コードに関する私の本「コードを洗う」からの抜粋を読んでいます。 PDF、EPUB、ペーパーバック、Kindle 版として利用可能です。今すぐコピーを入手してください。
コードをモジュールまたは関数に編成する方法と、コードを複製する代わりに抽象化を導入する適切な時期を知ることは重要なスキルです。他の人が効果的に使用できる汎用コードを書くことも、もう 1 つのスキルです。コードを分割する理由は、コードをまとめておく理由と同じくらいたくさんあります。この章では、これらの理由のいくつかについて説明します。
私たち開発者は、同じ作業を 2 回行うことを嫌います。 DRY は多くの人にとっての信条です。ただし、同じことを行うコードが 2 つまたは 3 つある場合は、たとえそれがどれほど誘惑に駆られたとしても、抽象化を導入するにはまだ時期尚早である可能性があります。
情報: 繰り返さない (DRY) 原則では、「すべての知識がシステム内で単一の、明確で権威のある表現を持たなければならない」ことが要求されており、これは多くの場合 として解釈されます。コードの重複は厳密に禁止されています.
しばらくはコードの重複という苦痛を抱えて生きてください。おそらく最終的にはそれほど悪くはなく、コードは実際にはまったく同じではありません。ある程度のコードの重複は健全であり、何かを壊すことを恐れることなく、コードをより速く反復して進化させることができます。
いくつかのユースケースだけを考慮すると、優れた API を思いつくのは困難です。
多くの開発者やチームがいる大規模なプロジェクトで共有コードを管理するのは困難です。あるチームの新しい要件が別のチームでは機能せず、コードが壊れる可能性があります。あるいは、数十の条件を備えた保守不可能なスパゲッティ モンスターが完成することになります。
チーム A が、名前、メッセージ、送信ボタンなどのコメント フォームをページに追加していると想像してください。次に、チーム B はフィードバック フォームが必要なので、チーム A のコンポーネントを見つけて再利用しようとします。次に、チーム A も電子メール フィールドが必要ですが、チーム B がコンポーネントを使用していることを知らないため、必須の電子メール フィールドを追加し、チーム B ユーザー向けの機能を無効にします。次に、チーム B には電話番号フィールドが必要ですが、チーム A が電話番号フィールドなしでコンポーネントを使用していることを知っているため、電話番号フィールドを表示するオプションを追加します。 1 年後、2 つのチームは互いのコードを破ったことで憎しみ合い、コンポーネントは条件だらけで保守不可能になりました。入力フィールドやボタンなど、下位レベルの共有コンポーネントで構成される個別のコンポーネントを維持していれば、両チームは時間を大幅に節約し、より健全な関係を築くことができます。
ヒント: コードが設計され、共有としてマークされていない限り、他のチームがコードを使用することを禁止することをお勧めします。 dependency クルーザーは、そのようなルールの設定に役立つツールです。
場合によっては、抽象化をロールバックする必要があります。条件やオプションを追加し始めるときは、自分自身に問いかける必要があります。それは依然として同じもののバリエーションなのか、それとも分離すべき新しいものなのか?モジュールに追加する条件やパラメーターが多すぎると、API が使いにくくなり、コードの保守やテストが難しくなる可能性があります。
重複は、間違った抽象化よりも安価で健全です。
情報: 詳しい説明については、Sandi Metz の記事「間違った抽象化」を参照してください。
コードのレベルが高くなるほど、コードを抽象化するまでに長く待つ必要があります。低レベルのユーティリティ抽象化は、ビジネス ロジックよりもはるかに明白で安定しています。
コードの再利用は、コードの一部を別の関数またはモジュールに抽出する唯一の理由ではありません。また、最も重要な理由ですらありません。
コードの長さは、モジュールや関数を分割する必要がある場合の指標としてよく使用されますが、サイズだけでコードの読み取りや保守が難しくなるわけではありません。
線形アルゴリズムは、たとえ長いものであっても、複数の関数に分割し、それらを順番に呼び出すと、コードが読みやすくなることはほとんどありません。関数 (ファイル) 間を移動するのはスクロールよりも難しく、コードを理解するために各関数の実装を調べる必要がある場合、その抽象化は適切ではありませんでした。
情報: Egon Elbre がコードの可読性の心理学に関する素晴らしい記事を書きました。
Google Testing Blog から抜粋した例を次に示します。
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Pizza クラスの API についてはたくさんの質問がありますが、作成者が提案する改善点を見てみましょう。
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
すでに複雑で複雑なものがさらに複雑で複雑になり、コードの半分は単なる関数呼び出しです。これによってコードが理解しやすくなるわけではありませんが、作業がほぼ不可能になります。この記事では、おそらく要点をより説得力のあるものにするため、リファクタリングされたバージョンの完全なコードは示していません。
Pierre "catwell" Chapuis は、新しい機能の代わりにコメントを追加することをブログ投稿で提案しています。
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
これはすでに分割バージョンよりもはるかに優れています。さらに良い解決策は、API を改善し、コードをより明確にすることです。 Pierre 氏は、オーブンの予熱は createPizza() 関数の一部にするべきではないと提案しています (私自身もたくさんのピザを焼いていますが、私も全く同感です!)。現実にはオーブンはすでにそこにあり、おそらく前のピザですでに熱くなっているからです。また、ピエール氏は、この関数はピザではなく箱を返すべきだと提案しています。元のコードでは、スライスとパッケージングの魔法がすべて終わった後、ボックスは一種の消滅を起こし、最終的にはスライスされたピザが手に残ることになるからです。
問題をコーディングする方法がたくさんあるのと同じように、ピザを調理する方法もたくさんあります。結果は同じに見えるかもしれませんが、一部のソリューションは他のソリューションよりも理解、変更、再利用、削除が簡単です。
抽出されたすべての関数が同じアルゴリズムの一部である場合、名前付けも問題になる可能性があります。コードよりもわかりやすく、コメントよりも短い名前を考案する必要がありますが、これは簡単な作業ではありません。
情報: コードのコメント化については「コメントを避ける」の章で説明し、名前付けについては「名前付けは難しい」の章で説明します。
私のコードには小さな関数はほとんど見つからないでしょう。私の経験では、コードを分割する最も有益な理由は、変更頻度 と 変更理由 です。
頻度を変更するから始めましょう。ビジネス ロジックは、ユーティリティ関数よりもはるかに頻繁に変更されます。頻繁に変更されるコードと非常に安定したコードを分離することは合理的です。
この章の前半で説明したコメント フォームは前者の例です。キャメルケース文字列をケバブケースに変換する関数は、後者の例です。新しいビジネス要件が発生した場合、コメント フォームは時間の経過とともに変更され、分岐する可能性があります。大文字と小文字の変換関数はまったく変更される可能性が低く、多くの場所で安全に再利用できます。
データを表示するために見栄えの良いテーブルを作成していると想像してください。このテーブル設計は二度と必要ないと思うかもしれないので、テーブルのすべてのコードを 1 つのモジュールに保持することにします。
次のスプリントでは、テーブルに別の列を追加するタスクを取得します。そのため、既存の列のコードをコピーし、そこにある数行を変更します。次のスプリントでは、同じデザインの別のテーブルを追加する必要があります。次のスプリントでは、テーブルのデザインを変更する必要があります…
私たちのテーブルモジュールには、少なくとも 3 つの 変更する理由、または 責任があります:
これにより、モジュールの理解が難しくなり、変更も難しくなります。プレゼンテーション用のコードでは冗長さが増し、ビジネス ロジックを理解することが難しくなります。責任を変更するには、より多くのコードを読んで変更する必要があります。これにより、どちらかの反復処理が難しくなり、時間がかかります。
汎用テーブルを別個のモジュールとして持つことで、この問題は解決されます。ここで、テーブルに別の列を追加するには、2 つのモジュールのうち 1 つを理解し、変更するだけで済みます。パブリック API を除いて、汎用テーブル モジュールについて何も知る必要はありません。すべてのテーブルのデザインを変更するには、汎用テーブル モジュールのコードを変更するだけで済み、個々のテーブルに手を加える必要はおそらくまったくありません。
ただし、問題の複雑さに応じて、モノリシックなアプローチから始めて、後で抽象化を抽出することは問題ありませんし、多くの場合、より良い方法です。
コードの再利用であっても、コードを分離する正当な理由になる場合があります。あるページでコンポーネントを使用すると、すぐに別のページでもそれが必要になる可能性があります。
すべての関数を独自のモジュールに抽出したくなるかもしれません。ただし、欠点もあります:
私は、1 つのモジュール内でのみ使用される小さな関数をモジュールの先頭に保持することを好みます。この方法では、同じモジュールで使用するためにそれらをインポートする必要はありませんが、別の場所で再利用するのは面倒です。
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
上記のコードには、このモジュールでのみ使用されるコンポーネント (FormattedAddress) と関数 (getMapLink()) があるため、ファイルの先頭で定義されています。
これらの関数をテストする必要がある場合 (テストすべきです!)、モジュールから関数をエクスポートし、モジュールのメイン関数と一緒にテストできます。
特定の機能またはコンポーネントと一緒にのみ使用することを目的とした機能についても同様です。これらを同じモジュールに保持すると、すべての関数が一緒に属していることがより明確になり、これらの関数がより見つけやすくなります。
もう 1 つの利点は、モジュールを削除すると、その依存関係も自動的に削除されることです。共有モジュール内のコードは、まだ使用されているかどうかを知るのが難しいため、コードベースに永久に残ることがよくあります (ただし、TypeScript を使用するとこれが簡単になります)。
情報: このようなモジュールは、ディープ モジュール と呼ばれることもあります。これは、複雑な問題をカプセル化しているが、単純な API を備えた比較的大きなモジュールです。深いモジュールの反対は浅いモジュールです。つまり、相互に対話する必要がある多くの小さなモジュールです。
複数のモジュールまたは関数を同時に変更する必要が頻繁にある場合は、それらを 1 つのモジュールまたは関数にマージした方がよい場合があります。このアプローチは、コロケーションと呼ばれることもあります。
コロケーションの例をいくつか示します:
コロケーションによってファイル ツリーがどのように変化するかは次のとおりです:
Separated | Colocated |
---|---|
React components | |
src/components/Button.tsx | src/components/Button.tsx |
styles/Button.css | |
Tests | |
src/util/formatDate.ts | src/util/formatDate.ts |
tests/formatDate.ts | src/util/formatDate.test.ts |
Ducks | |
src/actions/feature.js | src/ducks/feature.js |
src/actionCreators/feature.js | |
src/reducers/feature.js |
情報: コロケーションについて詳しくは、Kent C. Dodds の記事をご覧ください。
コロケーションに関するよくある不満は、コンポーネントが大きくなりすぎるということです。このような場合は、マークアップ、スタイル、ロジックとともに、一部の部分を独自のコンポーネントに抽出することをお勧めします。
コロケーションの考え方は、懸念事項の分離とも矛盾します。これは、Web 開発者が HTML、CSS、および JavaScript を別のファイル (多くの場合、ファイル ツリーの別の部分) に保持するように仕向けた時代遅れの考え方です。長すぎるため、Web ページに最も基本的な変更を加えるだけでも、3 つのファイルを同時に編集する必要があります。
情報: 変更理由は単一責任原則としても知られており、「すべてのモジュール、クラス、または関数は機能の単一部分に対して責任を負うべきである」と述べられています。ソフトウェアによって提供され、その責任はクラスによって完全にカプセル化される必要があります。」
場合によっては、特に使用が難しい API やエラーが発生しやすい API を使用しなければならないことがあります。たとえば、特定の順序で複数のステップを実行したり、常に同じ複数のパラメーターを指定して関数を呼び出したりする必要があります。これは、常に正しく実行できるようにするためにユーティリティ関数を作成する十分な理由です。おまけに、このコード部分のテストを作成できるようになりました。
URL、ファイル名、大文字と小文字の変換、書式設定などの文字列操作は、抽象化の良い候補です。おそらく、私たちがやろうとしていることのためのライブラリはすでに存在します。
次の例を考えてみましょう:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
このコードがファイル拡張子を削除し、ベース名を返すことに気づくまでに少し時間がかかります。不必要で読みにくいだけでなく、拡張子が常に 3 文字であると想定されていますが、実際はそうではない可能性があります。
ライブラリ、つまり組み込み Node.js のパス モジュールを使用して書き換えてみましょう:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
これで、何が起こっているかは明らかです。魔法の数字はなく、どんな長さのファイル拡張子でも機能します。
抽象化のその他の候補には、日付、デバイス機能、フォーム、データ検証、国際化などが含まれます。新しいユーティリティ関数を作成する前に、既存のライブラリを探すことをお勧めします。私たちは、一見単純な関数の複雑さを過小評価しがちです。
そのようなライブラリの例をいくつか示します:
時々、私たちは調子に乗ってコードを簡素化も短縮もしない抽象化を作成してしまうことがあります。
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
別の例:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
このような場合にできる最善の方法は、万能の インライン リファクタリング を適用することです。つまり、各関数呼び出しをその本体で置き換えます。抽象化しなくても問題ありません。
最初の例は次のようになります:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
2 番目の例は次のようになります。
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
結果は単に短くなり、読みやすくなっただけではありません。現在では、JavaScript のネイティブ関数や機能を自家製の抽象化なしで使用しているため、読者はこれらの関数が何を行うかを推測する必要はありません。
多くの場合、少し繰り返すのが効果的です。次の例を考えてみましょう:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
見た目はまったく問題なく、コードレビュー中に何の疑問も生じません。ただし、これらの値を使用しようとすると、オートコンプリートでは実際の値ではなく数値のみが表示されます (図を参照)。これにより、適切な値を選択することが難しくなります。
baseSpacing 定数をインライン化できます:
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
これで、コードが減り、同様に理解しやすくなり、オートコンプリートによって実際の値が表示されます (図を参照)。そして、このコードは頻繁には変更されないと思います - おそらく決して変更されないでしょう。
フォーム検証関数からの次の抜粋を考えてみましょう:
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
ここで何が起こっているのかを把握するのは非常に困難です。検証ロジックにはエラー メッセージが混在し、多くのチェックが繰り返されます…
この関数をいくつかの部分に分割し、それぞれが 1 つのことだけを担当することができます。
検証を配列として宣言的に記述することができます。
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
各検証関数と検証を実行する関数は非常に汎用的なため、それらを抽象化することも、サードパーティのライブラリを使用することもできます。
これで、どのフィールドにどの検証が必要か、また特定のチェックが失敗した場合にどのエラーを表示するかを記述することで、任意のフォームに検証を追加できます。
情報: 完全なコードとこの例の詳細な説明については、「回避条件」の章を参照してください。
私はこのプロセスを「何を」と「どのように」を分離すると呼びます:
利点は次のとおりです:
多くのプロジェクトには、utils.js、helpers.js、または misc.js というファイルがあり、開発者は、より適切な場所が見つからない場合にユーティリティ関数を追加します。多くの場合、これらの関数は他の場所で再利用されることはなく、ユーティリティ ファイル内に永久に残るため、関数は増大し続けます。こうしてモンスター ユーティリティ ファイルが誕生します。
Monster ユーティリティ ファイルにはいくつかの問題があります:
これらは私の経験則です:
JavaScript モジュールには 2 種類のエクスポートがあります。 1 つ目は 名前付きエクスポート:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
これは次のようにインポートできます:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
2 番目は デフォルトのエクスポート:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
これは次のようにインポートできます:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
デフォルトのエクスポートには利点があまりありませんが、いくつかの問題があります:
情報: greppability については、その他のテクニック 章の greppable コードの作成セクションで詳しく説明します。
残念ながら、React.lazy() などの一部のサードパーティ API ではデフォルトのエクスポートが必要ですが、それ以外の場合はすべて名前付きエクスポートを使用します。
バレル ファイルは、他の多数のモジュールを再エクスポートするモジュール (通常は、index.js または Index.ts という名前) です。
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
主な利点は、よりクリーンなインポートです。各モジュールを個別にインポートする代わりに:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
バレル ファイルからすべてのコンポーネントをインポートできます:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
ただし、バレル ファイルにはいくつかの問題があります。
情報: TkDodo はバレル ファイルの欠点を詳細に説明しています。
バレル ファイルの利点は、使用を正当化するにはあまりにも小さいため、使用しないことをお勧めします。
私が特に嫌いなバレル ファイルのタイプの 1 つは、./components/button/button ではなく ./components/button としてインポートできるようにするためだけに単一のコンポーネントをエクスポートするものです。
DRYers (コードを決して繰り返さない開発者) を荒らすために、誰かが別の用語を作りました: WET、すべてを 2 回書く、または 私たちはタイピングを楽しむ。抽象化に置き換えるまで少なくとも 2 回。これは冗談であり、私はこの考えに全面的に同意するわけではありません (コードを 2 回以上複製しても問題ない場合もあります) が、すべての良いことは適度に行うことが最善であることを思い出させてくれます。
次の例を考えてみましょう:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
これはコードの DRY の極端な例であり、特にこれらの定数のほとんどが 1 回しか使用されない場合、コードの読みやすさや保守のしやすさは向上しません。ここで実際の文字列ではなく変数名が表示されても役に立ちません。
これらの追加の変数をすべてインライン化しましょう。 (残念ながら、Visual Studio Code のインライン リファクタリングはオブジェクト プロパティのインライン化をサポートしていないため、手動で行う必要があります。)
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
現在では、コードが大幅に減り、何が起こっているかを理解しやすくなり、テストの更新や削除も簡単になりました。
私はテストで非常に多くの絶望的な抽象化に遭遇しました。たとえば、次のパターンは非常に一般的です:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
このパターンは、各テスト ケースで mount(...) 呼び出しの繰り返しを回避しようとしますが、テストが必要以上に混乱してしまいます。 mount() 呼び出しをインライン化しましょう:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
さらに、beforeEach パターンは、各テスト ケースを同じ値で初期化したい場合にのみ機能しますが、そのようなケースはほとんどありません。
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
React コンポーネントをテストするときに 一部 の重複を避けるために、私は多くの場合、defaultProps オブジェクトを追加し、それを各テスト ケース内に展開します。
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
こうすることで、重複があまりなくなりますが、同時に、各テスト ケースが分離され、読みやすくなります。各テスト ケースの固有の特性を確認しやすくなったため、テスト ケース間の違いがより明確になりました。
同じ問題のより極端なバリエーションを次に示します。
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
前の例と同じ方法で beforeEach() 関数をインライン化できます。
const findByReference = (wrapper, reference) => wrapper.find(reference); const favoriteTaco = findByReference( ['Al pastor', 'Cochinita pibil', 'Barbacoa'], x => x === 'Cochinita pibil' ); // → 'Cochinita pibil'
私はさらに進んで、test.each() メソッドを使用します。これは、多数の異なる入力を使用して同じテストを実行するためです。
function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: () => {} };
これで、すべてのテスト入力とその期待される結果が 1 か所に集められ、新しいテスト ケースを簡単に追加できるようになりました。
情報: 私の Jest と Vitest のチートシートをチェックしてください。
抽象化に関する最大の課題は、厳格すぎることと柔軟すぎることの間のバランスを見つけることと、いつ物事の抽象化を開始し、いつ停止するかを知ることです。多くの場合、何かを本当に抽象化する必要があるかどうかを確認するまで待つ価値があります。多くの場合、抽象化しないほうが良いのです。
グローバル ボタン コンポーネントがあるのは良いことですが、柔軟性が高すぎて、さまざまなバリエーションを切り替えるためのブール型プロパティが多数ある場合は、使用するのが難しくなります。ただし、厳格すぎる場合、開発者は共有ボタン コンポーネントを使用する代わりに独自のボタン コンポーネントを作成することになります。
私たちは、他の人にコードを再利用させることに注意する必要があります。多くの場合、これにより、独立しているはずのコードベースの部分間に緊密な結合が生じ、開発が遅くなり、バグが発生します。
以下について考え始めます:
フィードバックがある場合は、マストドンで私にツイートするか、GitHub で問題をオープンするか、artem@sapegin.ru にメールしてください。コピーを入手してください。
以上がコードの洗浄: 分割して征服するか、マージして緩和するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。