SOLID
単一責任原則(SRP)
クリーンコードで述べられているように「クラスを変更する理由は1つだけであるべき」です。飛行機の手荷物が1つしか許されない時のように、クラスに多くの機能を詰め込みたくなる誘惑があります。しかしこの方法ではクラスの概念的な凝縮性が損なわれ、変更理由が複数発生します。クラスの変更頻度を最小化することは重要です。1つのクラスに過剰な機能が集中している場合、一部を修正した際にコードベース内の依存関係にあるモジュールに及ぼす影響を把握するのが困難になるためです。
悪い例:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}良い例:
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}開放閉鎖原則(OCP)
バートランド・メイヤーが提唱した「ソフトウェア実体(クラス、モジュール、関数等)は拡張に対して開いており、修正に対して閉じているべき」という原則です。具体的には、既存コードを変更せずに新機能を追加できる設計を意味します。
悪い例:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}良い例:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// transform response and return
});
}
}リスコフ置換原則(LSP)
複雑な定義ですが、端的に言えば「SがTのサブタイプである場合、T型のオブジェクトをS型で置換してもプログラムの正しさが維持されるべき」という原則です。
具体的には親クラスと子クラスが置換可能である状態を指します。数学的に正方形(Square)は長方形(Rectangle)の一種ですが、継承による「is-a」関係でモデル化すると問題が発生する古典的な例で説明されています。
悪い例:
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);良い例:
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);インターフェース分離原則(ISP)
JavaScriptには明示的なインターフェースが存在しませんが、この原則は型システムがない環境でも重要です。
ISPは「クライアントが使用しないインターフェースに依存することを強制されるべきではない」と規定します。JavaScriptではダックタイピングによる暗黙の契約がこれに該当します。
大規模な設定オブジェクトを必要とするクラスが良い例です。全ての設定を必須にせずオプション化することで「過剰なインターフェース」を防ぎ、クライアントが不要な依存関係を負わないようにします。
悪い例:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});良い例:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
options: {
animationModule() {}
}
});依存性逆転原則(DIP)
この原則は2つの重要な概念を規定します:
- 上位モジュールは下位モジュールに依存すべきではない(両者は抽象に依存すべき)
- 抽象は詳細に依存すべきではない(詳細が抽象に依存すべき)
AngularJSの依存性注入(DI)がこの原則の実装例です。厳密に同一ではありませんが、DIPは上位モジュールが下位モジュールの詳細を知らずにDIを通して結合度を低減します。結合度が高いとリファクタリングが困難になるため、この原則は重要です。
JavaScriptでは暗黙の契約(オブジェクト/クラスが公開するメソッドやプロパティ)が抽象化を実現します。下記の例ではInventoryTracker用のRequestモジュールがrequestItemsメソッドを持つことが契約条件です。
悪い例:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();良い例:
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();