JavaScript,TypeScriptでのユーティリティクラスを今回は扱います
この記事で分かること
- ユーティリティクラスの定義
- eslintで非推奨にされている背景
- ユーティリティクラスがもたらす技術的負債
- ユーティリティクラスではなくて個別で関数、定数宣言するべき
1. 背景
staticなプロパティのみのclassを実装することがあリましたが使い時として妥当だったのかと思うことがあった為です。
2. ユーティリティクラスの定義
- 以下をプロパティに持つクラス
- static メソッド
- static 変数
- インスタンス化できない
- 状態を持たない
「ヘルパー・クラス」もユーティリティクラスに含まれるそうです。
ユーティリティクラスの定義が誤っていた場合はコメントでご教授ください🙏
ユーティリティクラスの例
3. @typescript-eslint/no-extraneous-class
eslintのプラグインで以下のユーティリティクラスを禁止にするルールが追加されていたのでここで紹介します。
以下のmdを意訳してます。
誤っていたらコメントでご指摘願います
https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/docs/rules/no-extraneous-class.md
Wrapperクラスはユーティリティクラスです。
This rule reports when a class has no non-static members, such as for a class used exclusively as a static namespace.
「このルールは、静的名前空間としてのみ使用されるクラスなど、クラスに非静的メンバがない場合に報告されます」と記述されています。
3.1 なぜこのルールができたのか
class Wrapper {
static method1(){~~~}
static method2(){...}
}
結論は以下です。
-
Wrapper classes add extra cognitive complexity to code without adding any structural improvements
Wrapperクラスは構造的な改良を加えることなく、コードに余計な認知的複雑さを加える
具体的に深掘りしますと以下の2つです。
-
Whatever would be put on them, such as utility functions, are already organized by virtue of being in a module.
Wrapperクラスのstaticプロパティのようなものは、モジュールの中にあることで既に整理されている。
↓以下書き換えたものです。
export const method1 = () => {~~~}
export const method2 = () => {...}
-
As an alternative, you can import * as ... the module to get all of them in a single object.
3.2 個別 import で使用することを推奨されています
以下に経緯を記述します。
モジュールからエクスポートされたすべてのメンバー(変数、関数、クラスなど)を一つのオブジェクトにまとめてimportすることでWrapperクラスを使っているのと同様のことが可能です。
import * as Utility from 'Utility.tsまでのファイルパス'
Utility.method1();
Utility.method2();
alias(as)が任意の文字列になるのでそれを統一させたい場合は以下のように一つのオブジェクトにまとめると良いと思います。
統一させるメリットはリファクタの時に一括で修正できることです。
export const Utility = {
method1: () => {~~~},
method2: () => {...}
}
デメリット
-
IDEs can't provide as good suggestions for static class or namespace imported properties when you start typing property names
IDEは、静的なクラスやネームスペースにインポートされたプロパティについて、プロパティ名を入力し始めると、適切なサジェスチョンを提供してくれません。
以下VSCodeで検証しましたが、モジュールを丸ごとimport,静的なクラス(Utilityクラス)においては適切なサジェスチョンだったと思いました。
オプション | サジェスチョンタイミング |
---|---|
namespace | 先頭の文字がプロパティの先頭の文字と一致 |
モジュールを丸ごとimport | 正常なタイミング |
Utilityクラス | 正常なタイミング |
-
検証内容
-
namespace
export namespace Utility { method1: () => {} method2: () => {} }
-
モジュールを丸ごとimport
export const method1 = () => {} export const method2 = () => {}
-
Utilityクラス
export class Utility { static method1: () => {} static method2: () => {} }
-
-
It's more difficult to statically analyze code for unused variables, etc. when they're all on the class (see: Finding dead code (and dead types) in TypeScript).
未使用の変数などがすべてクラス上にあると、コードを静的に分析するのが難しくなる(参考:TypeScriptでデッドコード(とデッド型)を見つける)。
-
They(Namespace Imports ) don't play as well with tree shaking in modern bundlers
現代のバンドルでは、名前空間インポートの場合はツリーシェイキングと相性が悪いそうです。
バンドラーが使用するものだけでなくてモジュール全体を最終的なバンドルに含める可能性があるということです。
web.devの記事によるとwebpackでこのようなことが起こると記述されていました。
4. ユーティリティクラスの定義を守るための実装上の課題
- インスタンスを作れてしまう
対応策としては以下のようにインスタンスを外部から作成できないようにすることです。
class UtilityClass {
private constructor(){}
}
- 非staticなプロパティを記述できてしまう
対応策としてはeslintでカスタムのruleを作ることが一つかなと思いました。
外部からの呼び出しでインスタンスを作れない(privateなコンストラクタ)にしている場合は対応しなくても良いかなと思いました。
5. ユーティリティクラスがもたらす技術的負債
こちらの記事で紹介されていました。以下に抜粋したものを引用します。
5.1 ユーティリティ・クラスが巨大になる
ユーティリティ・クラスはあいまいで、そのため半端に関連したロジックを追加しやすいです。
ロジックが追加されればされるほど、後で分割するのが難しくなります。
クラスが大きくなればなるほど、書き換えも大きくなります。
また、呼び出し元のクラスを修正することも起きシステム全体に悪影響を与える可能性があります。
5.2 ユーティリティ・クラスのスコープが大きすぎるか、定義されていない
アプリケーションのモデルに基づいてユーティリティ・クラスに名前をつけることが多いことです。
だから "PatientUtil "や "AppointmentHelper "などのクラスができるのです。
これは、スコープがそのドメインに関係するすべてのものに限定されることを意味します。
新しい機能は "PatientService "に属するのか、それとも "PatientUtil "に属するのか?これらのクラスはどちらも "Patient "ドメインの全体的な機能を担っているため、単純にはわからないです。
5.3 ユーティリティ・クラスはコードを隠すために存在する
アプリケーションの規模が大きくなり、複雑になってくると、クラスが大きくなりすぎていることに気づきます。
それに対処するために、私たちは単純にいくつかのメソッドを静的にしてユーティリティ・クラスに入れます。
5.4 ユーティリティクラスが有効な時は稀
また、ユーティリティ・クラスを扱う際のルールを記事で述べられていましたが、推奨されていないと見てここでは割愛します。
結論には以下のように書かれていましたし、ユーティリティクラスが有効な時は稀と記述もされていました。
ユーティリティ・クラスを書くことが実際に正当化されることは非常にまれであるため、ユーティリティ・クラスを書くときは、キーボードから手を離し、自分たちが何をしているのかを本当に考える瞬間のひとつであるべきです。
これは前進する時ではなく、むしろ私たちが解決しようとしている核心的な問題を振り返る時なのです。
ユーティリティ・クラスがの多くの場合は怠慢や静的機能がもたらす問題を理解していないことから生まれています
6. ユーティリティクラスではなくて個別で関数、定数宣言 & 個別でimportするべき
前の章でも述べたようにeslintで禁止されている背景、ユーティリティクラスの技術的負債のリスクの観点からも個別で関数、定数宣言 & 個別でimportするのが妥当かと思います。
7. 今後勉強したいこと
- ツリーシェイキング
- esmodule,commonjs
8. その他
CodeSmell:様々なプログラムのあるべきではない状態のこと
9. 最後に
- 「ユーティリティクラス」という語句すら知らなかったのでそういった所からこれからも知識を増やしていきたいと思いました。
- ユーティリティクラスが eslint で非推奨になっている背景を知れて勉強になりました。
- 今後の運用の為にも既存のユーティリティクラスを見直す、またデザインパターンで書き換えられるかの検討もしないといけないなと思いました。