JavaScript: if/else地獄をリファクタリングで解消する(翻訳)
本記事は、私の近著『JavaScript for Rails Developers』から抜粋してWeb用に手を加えたものです。ぜひお買い求めください✌️。
コードを書くという作業は、直線的には進みません。真っ白なキャンバスをにらみつけていれば何を書けばよいかがわかるとは限りません。
本記事は、私が大規模なコードベースでメソッドを書いたときの手順をドキュメント化したいと思います。if
やelse
分岐だらけで見通しの悪い非常に手続き的に書かれたコードが、追いかけやすく素直にメンテできる、よりオブジェクト指向的なコードに進化する様子を本記事でご覧いただけます。
背景情報: このコードの機能は、私の著書で構築するコードエディタで、矢印キーで行を上下に移動可能にします。view
やlineAt
やdispatch
などの参照は、コードの背後で使われているCodeMirrorというライブラリ固有のものです。
とりあえずの注意点: リファクタリング作業では、コードの振る舞いを変更してはいけません。本記事の場合、コードの振る舞いの結果や、親コンテキストにおけるmoveLine
メソッドの使われ方を変更してはいけないという意味です。
それでは最初にリファクタリング開始前の初期バージョンのコードを見てみましょう。このコードが私の思考プロセスに沿っていて、その機能で実現したいことに注目していることがわかります。
- 矢印キーで指定された方向が上の場合、「duplicateがtrueでない」場合にのみ変更をディスパッチする
- 矢印キーで指定された方向が下の場合も同様だが、方向のみ異なる
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const document = view.state.doc
const selection = view.state.selection.main
const line = document.lineAt(selection.from)
const lineCount = document.lines
if (direction === "up") {
if (duplicate) {
const changes = {
from: line.from,
to: line.from,
insert: line.text + "\n"
}
view.dispatch({
changes,
selection: {
anchor: selection.anchor,
head: selection.head
}
})
} else if (line.number > 1) {
const prevLine = document.lineAt(line.from - 1)
const changes = {
from: prevLine.from,
to: line.to,
insert: line.text + "\n" + prevLine.text
}
view.dispatch({
changes,
selection: {
anchor: selection.anchor - prevLine.length - 1,
head: selection.head - prevLine.length - 1
}
})
}
return true
} else if (direction === "down") {
if (duplicate) {
const changes = {
from: line.to,
to: line.to,
insert: "\n" + line.text
}
view.dispatch({
changes,
selection: {
anchor: selection.anchor + line.length + 1,
head: selection.head + line.length + 1
}
})
} else if (line.number < lineCount) {
const nextLine = document.lineAt(line.to + 1)
const changes = {
from: line.from,
to: nextLine.to,
insert: nextLine.text + "\n" + line.text
}
view.dispatch({
changes,
selection: {
anchor: selection.anchor + nextLine.length + 1,
head: selection.head + nextLine.length + 1
}
})
}
return true
}
return false
}
リファクタリングの最初のステップは、個別の分岐からロジックを抽出して専用のメソッドに切り出すことです。こうすることで、コード内にあるパターンが見えてくるようになります。
- ディスパッチパターン(
view.dispatch()
) - オブジェクトの変更(
view.dispatch()
) - 選択範囲を計算する(
selection.anchor/head - length - 1
)
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const document = view.state.doc
const selection = view.state.selection.main
const currentLine = document.lineAt(selection.from)
const totalLines = document.lines
if (direction === "up") {
return moveLineUp(view, document, selection, currentLine, duplicate)
} else if (direction === "down") {
return moveLineDown(view, document, selection, currentLine, totalLines, duplicate)
}
return false
}
const moveLineUp = (view, document, selection, currentLine, duplicate) => {
if (duplicate) {
return duplicateLineUp(view, selection, currentLine)
} else if (currentLine.number > 1) {
return shiftLineUp(view, document, selection, currentLine)
}
return false
}
const moveLineDown = (view, document, selection, currentLine, totalLines, duplicate) => {
if (duplicate) {
return duplicateLineDown(view, selection, currentLine)
} else if (currentLine.number < totalLines) {
return shiftLineDown(view, document, selection, currentLine)
}
return false
}
const duplicateLineUp = (view, selection, currentLine) => {
const changes = {
from: currentLine.from,
to: currentLine.from,
insert: `${currentLine.text}\n`
}
view.dispatch({
changes,
selection: { anchor: selection.anchor, head: selection.head }
})
return true
}
const duplicateLineDown = (view, selection, currentLine) => {
const changes = {
from: currentLine.to,
to: currentLine.to,
insert: `\n${currentLine.text}`
}
view.dispatch({
changes,
selection: {
anchor: selection.anchor + currentLine.length + 1,
head: selection.head + currentLine.length + 1
}
})
return true
}
const shiftLineUp = (view, document, selection, currentLine) => {
const previousLine = document.lineAt(currentLine.from - 1)
const changes = {
from: previousLine.from,
to: currentLine.to,
insert: `${currentLine.text}\n${previousLine.text}`
}
view.dispatch({
changes,
selection: {
anchor: selection.anchor - previousLine.length - 1,
head: selection.head - previousLine.length - 1
}
})
return true
}
const shiftLineDown = (view, document, selection, currentLine) => {
const nextLine = document.lineAt(currentLine.to + 1)
const changes = {
from: currentLine.from,
to: nextLine.to,
insert: `${nextLine.text}\n${currentLine.text}`
}
view.dispatch({
changes,
selection: {
anchor: selection.anchor + nextLine.length + 1,
head: selection.head + nextLine.length + 1
}
})
return true
}
▶1つ前との差分(クリックで展開)
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const document = view.state.doc
const selection = view.state.selection.main
- const line = document.lineAt(selection.from)
- const lineCount = document.lines
+ const currentLine = document.lineAt(selection.from)
+ const totalLines = document.lines
if (direction === "up") {
- if (duplicate) {
- const changes = {
- from: line.from,
- to: line.from,
- insert: line.text + "\n"
- }
-
- view.dispatch({
- changes,
- selection: {
- anchor: selection.anchor,
- head: selection.head
- }
- })
+ return moveLineUp(view, document, selection, currentLine, duplicate)
+ } else if (direction === "down") {
+ return moveLineDown(view, document, selection, currentLine, totalLines, duplicate)
+ }
+
+ return false
+}
+
+const moveLineUp = (view, document, selection, currentLine, duplicate) => {
+ if (duplicate) {
+ return duplicateLineUp(view, selection, currentLine)
+ } else if (currentLine.number > 1) {
+ return shiftLineUp(view, document, selection, currentLine)
+ }
- } else if (line.number > 1) {
- const prevLine = document.lineAt(line.from - 1)
- const changes = {
- from: prevLine.from,
- to: line.to,
- insert: line.text + "\n" + prevLine.text
- }
-
- view.dispatch({
- changes,
- selection: {
- anchor: selection.anchor - prevLine.length - 1,
- head: selection.head - prevLine.length - 1
- }
- })
- }
-
- return true
-
- } else if (direction === "down") {
- if (duplicate) {
- const changes = {
- from: line.to,
- to: line.to,
- insert: "\n" + line.text
- }
-
- view.dispatch({
- changes,
- selection: {
- anchor: selection.anchor + line.length + 1,
- head: selection.head + line.length + 1
- }
- })
- } else if (line.number < lineCount) {
- const nextLine = document.lineAt(line.to + 1)
- const changes = {
- from: line.from,
- to: nextLine.to,
- insert: nextLine.text + "\n" + line.text
- }
+ return false
+}
+
+const moveLineDown = (view, document, selection, currentLine, totalLines, duplicate) => {
+ if (duplicate) {
+ return duplicateLineDown(view, selection, currentLine)
+ } else if (currentLine.number < totalLines) {
+ return shiftLineDown(view, document, selection, currentLine)
+ }
+
+ return false
+}
+
+const duplicateLineUp = (view, selection, currentLine) => {
+ const changes = {
+ from: currentLine.from,
+ to: currentLine.from,
+ insert: `${currentLine.text}\n`
+ }
+
+ view.dispatch({
+ changes,
+ selection: { anchor: selection.anchor, head: selection.head }
+ })
+
+ return true
+}
+
+const duplicateLineDown = (view, selection, currentLine) => {
+ const changes = {
+ from: currentLine.to,
+ to: currentLine.to,
+ insert: `\n${currentLine.text}`
+ }
+
+ view.dispatch({
+ changes,
+ selection: {
+ anchor: selection.anchor + currentLine.length + 1,
+ head: selection.head + currentLine.length + 1
+ }
+ })
- view.dispatch({
- changes,
- selection: {
- anchor: selection.anchor + nextLine.length + 1,
- head: selection.head + nextLine.length + 1
- }
- })
- }
+ return true
+}
+
+const shiftLineUp = (view, document, selection, currentLine) => {
+ const previousLine = document.lineAt(currentLine.from - 1)
+
+ const changes = {
+ from: previousLine.from,
+ to: currentLine.to,
+ insert: `${currentLine.text}\n${previousLine.text}`
+ }
+
+ view.dispatch({
+ changes,
+ selection: {
+ anchor: selection.anchor - previousLine.length - 1,
+ head: selection.head - previousLine.length - 1
+ }
+ })
+
+ return true
+}
+
+const shiftLineDown = (view, document, selection, currentLine) => {
+ const nextLine = document.lineAt(currentLine.to + 1)
+
+ const changes = {
+ from: currentLine.from,
+ to: nextLine.to,
+ insert: `${nextLine.text}\n${currentLine.text}`
+ }
+
+ view.dispatch({
+ changes,
+ selection: {
+ anchor: selection.anchor + nextLine.length + 1,
+ head: selection.head + nextLine.length + 1
+ }
+ })
- return true
- }
-
- return false
+ return true
}
次に、重複したディスパッチパターンを削除して、moveLine
メソッド内でディスパッチパターンを1回だけ使うようにしました。これにより、メソッドから返されるオブジェクトのパターンがさらに明確になりました。
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const document = view.state.doc
const selection = view.state.selection.main
const currentLine = document.lineAt(selection.from)
const totalLines = document.lines
let operation
if (direction === "up") {
operation = moveLineUp(document, selection, currentLine, duplicate)
} else if (direction === "down") {
operation = moveLineDown(document, selection, currentLine, totalLines, duplicate)
}
if (!operation) return false
view.dispatch({
changes: operation.changes,
selection: operation.selection
})
return true
}
const moveLineUp = (document, selection, currentLine, duplicate) => {
if (duplicate) {
return duplicateLineUp(selection, currentLine)
} else if (currentLine.number > 1) {
return shiftLineUp(document, selection, currentLine)
}
return null
}
const moveLineDown = (document, selection, currentLine, totalLines, duplicate) => {
if (duplicate) {
return duplicateLineDown(selection, currentLine)
} else if (currentLine.number < totalLines) {
return shiftLineDown(document, selection, currentLine)
}
return null
}
const duplicateLineUp = (selection, currentLine) => ({
changes: {
from: currentLine.from,
to: currentLine.from,
insert: `${currentLine.text}\n`
},
selection: {
anchor: selection.anchor,
head: selection.head
}
})
const duplicateLineDown = (selection, currentLine) => ({
changes: {
from: currentLine.to,
to: currentLine.to,
insert: `\n${currentLine.text}`
},
selection: {
anchor: selection.anchor + currentLine.length + 1,
head: selection.head + currentLine.length + 1
}
})
const shiftLineUp = (document, selection, currentLine) => {
const previousLine = document.lineAt(currentLine.from - 1)
return {
changes: {
from: previousLine.from,
to: currentLine.to,
insert: `${currentLine.text}\n${previousLine.text}`
},
selection: {
anchor: selection.anchor - previousLine.length - 1,
head: selection.head - previousLine.length - 1
}
}
}
const shiftLineDown = (document, selection, currentLine) => {
const nextLine = document.lineAt(currentLine.to + 1)
return {
changes: {
from: currentLine.from,
to: nextLine.to,
insert: `${nextLine.text}\n${currentLine.text}`
},
selection: {
anchor: selection.anchor + nextLine.length + 1,
head: selection.head + nextLine.length + 1
}
}
}
▶1つ前との差分(クリックで展開)
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const document = view.state.doc
const selection = view.state.selection.main
const currentLine = document.lineAt(selection.from)
const totalLines = document.lines
- if (direction === "up") {
- return moveLineUp(view, document, selection, currentLine, duplicate)
- } else if (direction === "down") {
- return moveLineDown(view, document, selection, currentLine, totalLines, duplicate)
- }
-
- return false
-}
-
-const moveLineUp = (view, document, selection, currentLine, duplicate) => {
- if (duplicate) {
- return duplicateLineUp(view, selection, currentLine)
- } else if (currentLine.number > 1) {
- return shiftLineUp(view, document, selection, currentLine)
- }
-
- return false
-}
-
-const moveLineDown = (view, document, selection, currentLine, totalLines, duplicate) => {
- if (duplicate) {
- return duplicateLineDown(view, selection, currentLine)
- } else if (currentLine.number < totalLines) {
- return shiftLineDown(view, document, selection, currentLine)
- }
-
- return false
-}
-
-const duplicateLineUp = (view, selection, currentLine) => {
- const changes = {
- from: currentLine.from,
- to: currentLine.from,
- insert: `${currentLine.text}\n`
- }
-
- view.dispatch({
- changes,
- selection: { anchor: selection.anchor, head: selection.head }
- })
-
- return true
-}
-
-const duplicateLineDown = (view, selection, currentLine) => {
- const changes = {
- from: currentLine.to,
- to: currentLine.to,
- insert: `\n${currentLine.text}`
- }
+ let operation
+
+ if (direction === "up") {
+ operation = moveLineUp(document, selection, currentLine, duplicate)
+ } else if (direction === "down") {
+ operation = moveLineDown(document, selection, currentLine, totalLines, duplicate)
+ }
+
+ if (!operation) return false
+
+ view.dispatch({
+ changes: operation.changes,
+ selection: operation.selection
+ })
+
+ return true
+}
+
+const moveLineUp = (document, selection, currentLine, duplicate) => {
+ if (duplicate) {
+ return duplicateLineUp(selection, currentLine)
+ } else if (currentLine.number > 1) {
+ return shiftLineUp(document, selection, currentLine)
+ }
+
+ return null
+}
+
+const moveLineDown = (document, selection, currentLine, totalLines, duplicate) => {
+ if (duplicate) {
+ return duplicateLineDown(selection, currentLine)
+ } else if (currentLine.number < totalLines) {
+ return shiftLineDown(document, selection, currentLine)
+ }
+
+ return null
+}
+
+const duplicateLineUp = (selection, currentLine) => ({
+ changes: {
+ from: currentLine.from,
+ to: currentLine.from,
+ insert: `${currentLine.text}\n`
+ },
+
+ selection: {
+ anchor: selection.anchor,
+ head: selection.head
+ }
+})
- view.dispatch({
- changes,
- selection: {
- anchor: selection.anchor + currentLine.length + 1,
- head: selection.head + currentLine.length + 1
- }
- })
-
- return true
-}
-
-const shiftLineUp = (view, document, selection, currentLine) => {
- const previousLine = document.lineAt(currentLine.from - 1)
-
- const changes = {
- from: previousLine.from,
- to: currentLine.to,
- insert: `${currentLine.text}\n${previousLine.text}`
- }
-
- view.dispatch({
- changes,
- selection: {
- anchor: selection.anchor - previousLine.length - 1,
- head: selection.head - previousLine.length - 1
- }
- })
-
- return true
+const duplicateLineDown = (selection, currentLine) => ({
+ changes: {
+ from: currentLine.to,
+ to: currentLine.to,
+ insert: `\n${currentLine.text}`
+ },
+
+ selection: {
+ anchor: selection.anchor + currentLine.length + 1,
+ head: selection.head + currentLine.length + 1
+ }
+})
+
+const shiftLineUp = (document, selection, currentLine) => {
+ const previousLine = document.lineAt(currentLine.from - 1)
+
+ return {
+ changes: {
+ from: previousLine.from,
+ to: currentLine.to,
+ insert: `${currentLine.text}\n${previousLine.text}`
+ },
+
+ selection: {
+ anchor: selection.anchor - previousLine.length - 1,
+ head: selection.head - previousLine.length - 1
+ }
+ }
}
-const shiftLineDown = (view, document, selection, currentLine) => {
+const shiftLineDown = (document, selection, currentLine) => {
const nextLine = document.lineAt(currentLine.to + 1)
- const changes = {
- from: currentLine.from,
- to: nextLine.to,
- insert: `${nextLine.text}\n${currentLine.text}`
- }
+ return {
+ changes: {
+ from: currentLine.from,
+ to: nextLine.to,
+ insert: `${nextLine.text}\n${currentLine.text}`
+ },
- view.dispatch({
- changes,
selection: {
anchor: selection.anchor + nextLine.length + 1,
head: selection.head + nextLine.length + 1
}
- })
-
- return true
+ }
}
今度は、duplicate
メソッドを方向のロジックに移動して、どう動くかを見てみたいと思います。
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const document = view.state.doc
const selection = view.state.selection.main
const currentLine = document.lineAt(selection.from)
const totalLines = document.lines
let operation
if (direction === "up") {
operation = moveLineUp(document, selection, currentLine, duplicate)
} else if (direction === "down") {
operation = moveLineDown(document, selection, currentLine, totalLines, duplicate)
}
if (!operation) return false
view.dispatch({
changes: operation.changes,
selection: operation.selection
})
return true
}
const moveLineUp = (document, selection, currentLine, duplicate) => {
if (currentLine.number === 1 && !duplicate) return null
if (duplicate) {
return {
changes: {
from: currentLine.from,
to: currentLine.from,
insert: `${currentLine.text}\n`
},
selection: {
anchor: selection.anchor,
head: selection.head
}
}
}
const previousLine = document.lineAt(currentLine.from - 1)
return {
changes: {
from: previousLine.from,
to: currentLine.to,
insert: `${currentLine.text}\n${previousLine.text}`
},
selection: {
anchor: selection.anchor - previousLine.length - 1,
head: selection.head - previousLine.length - 1
}
}
}
const moveLineDown = (document, selection, currentLine, totalLines, duplicate) => {
if (currentLine.number === totalLines && !duplicate) return null
if (duplicate) {
return {
changes: {
from: currentLine.to,
to: currentLine.to,
insert: `\n${currentLine.text}`
},
selection: {
anchor: selection.anchor + currentLine.length + 1,
head: selection.head + currentLine.length + 1
}
}
}
const nextLine = document.lineAt(currentLine.to + 1)
return {
changes: {
from: currentLine.from,
to: nextLine.to,
insert: `${nextLine.text}\n${currentLine.text}`
},
selection: {
anchor: selection.anchor + nextLine.length + 1,
head: selection.head + nextLine.length + 1
}
}
}
▶1つ前との差分(クリックで展開)
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const document = view.state.doc
const selection = view.state.selection.main
const currentLine = document.lineAt(selection.from)
const totalLines = document.lines
let operation
if (direction === "up") {
operation = moveLineUp(document, selection, currentLine, duplicate)
} else if (direction === "down") {
operation = moveLineDown(document, selection, currentLine, totalLines, duplicate)
}
if (!operation) return false
view.dispatch({
changes: operation.changes,
selection: operation.selection
})
return true
}
const moveLineUp = (document, selection, currentLine, duplicate) => {
- if (duplicate) {
- return duplicateLineUp(selection, currentLine)
- } else if (currentLine.number > 1) {
- return shiftLineUp(document, selection, currentLine)
- }
-
- return null
-}
-
-const moveLineDown = (document, selection, currentLine, totalLines, duplicate) => {
- if (duplicate) {
- return duplicateLineDown(selection, currentLine)
- } else if (currentLine.number < totalLines) {
- return shiftLineDown(document, selection, currentLine)
- }
-
- return null
-}
+ if (currentLine.number === 1 && !duplicate) return null
+
+ if (duplicate) {
+ return {
+ changes: {
+ from: currentLine.from,
+ to: currentLine.from,
+ insert: `${currentLine.text}\n`
+ },
+
+ selection: {
+ anchor: selection.anchor,
+ head: selection.head
+ }
+ }
+ }
+
+ const previousLine = document.lineAt(currentLine.from - 1)
-const duplicateLineUp = (selection, currentLine) => ({
- changes: {
- from: currentLine.from,
- to: currentLine.from,
- insert: `${currentLine.text}\n`
- },
+ return {
+ changes: {
+ from: previousLine.from,
+ to: currentLine.to,
+ insert: `${currentLine.text}\n${previousLine.text}`
+ },
- selection: {
- anchor: selection.anchor,
- head: selection.head
- }
-})
-
-const duplicateLineDown = (selection, currentLine) => ({
- changes: {
- from: currentLine.to,
- to: currentLine.to,
- insert: `\n${currentLine.text}`
- },
-
- selection: {
- anchor: selection.anchor + currentLine.length + 1,
- head: selection.head + currentLine.length + 1
- }
-})
-
-const shiftLineUp = (document, selection, currentLine) => {
- const previousLine = document.lineAt(currentLine.from - 1)
-
- return {
- changes: {
- from: previousLine.from,
- to: currentLine.to,
- insert: `${currentLine.text}\n${previousLine.text}`
- },
+ selection: {
+ anchor: selection.anchor - previousLine.length - 1,
+ head: selection.head - previousLine.length - 1
+ }
+ }
+}
+
+const moveLineDown = (document, selection, currentLine, totalLines, duplicate) => {
+ if (currentLine.number === totalLines && !duplicate) return null
+
+ if (duplicate) {
+ return {
+ changes: {
+ from: currentLine.to,
+ to: currentLine.to,
+ insert: `\n${currentLine.text}`
+ },
+
+ selection: {
+ anchor: selection.anchor + currentLine.length + 1,
+ head: selection.head + currentLine.length + 1
+ }
+ }
+ }
- selection: {
- anchor: selection.anchor - previousLine.length - 1,
- head: selection.head - previousLine.length - 1
- }
- }
-}
-
-const shiftLineDown = (document, selection, currentLine) => {
const nextLine = document.lineAt(currentLine.to + 1)
return {
changes: {
from: currentLine.from,
to: nextLine.to,
ここで行き詰まってしまいました。処理内容がまだまだ多いので理解するのが大変です。
そこで、今度はコードをクラス化してみたいと思います(リファクタリングのルールを1つ変更していますが、今は目をつぶりましょう)。
class Line {
constructor(view, direction, duplicate = false) {
this.view = view
this.direction = direction
this.duplicate = duplicate
}
move() {
if (!this.duplicate && this.#atBoundary) return false
const operation = this.duplicate
? this.#duplicate()
: this.#swap()
this.view.dispatch(operation)
return true
}
// private
#duplicate() {
return {
changes: this.#duplicateChanges,
selection: this.#newSelection
}
}
#swap() {
return {
changes: this.#swapChanges,
selection: this.#newSelection
}
}
get #document() {
return this.view.state.doc
}
get #selection() {
return this.view.state.selection.main
}
get #currentLine() {
return this.#document.lineAt(this.#selection.from)
}
get #moveUp() {
return this.direction === "up"
}
get #targetLine() {
return this.#document.lineAt(
this.#moveUp ? this.#currentLine.from - 1 : this.#currentLine.to + 1
)
}
get #atBoundary() {
return this.#moveUp
? this.#currentLine.number === 1
: this.#currentLine.number === this.#document.lines
}
get #selectionOffset() {
if (this.duplicate && this.#moveUp) return 0
const lineLength = this.duplicate ? this.#currentLine.length : this.#targetLine.length
return (lineLength + 1) _ (this.#moveUp ? -1 : 1)
}
get #duplicateChanges() {
return {
from: this.#moveUp ? this.#currentLine.from : this.#currentLine.to,
to: this.#moveUp ? this.#currentLine.from : this.#currentLine.to,
insert: this.#moveUp ? `${this.#currentLine.text}\n` : `\n${this.#currentLine.text}`
}
}
get #swapChanges() {
return {
from: this.#moveUp ? this.#targetLine.from : this.#currentLine.from,
to: this.#moveUp ? this.#currentLine.to : this.#targetLine.to,
insert: this.#moveUp
? `${this.#currentLine.text}\n${this.#targetLine.text}`
: `${this.#targetLine.text}\n${this.#currentLine.text}`
}
}
get #newSelection() {
return {
anchor: this.#selection.anchor + this.#selectionOffset,
head: this.#selection.head + this.#selectionOffset
}
}
}
export const moveLine = (view, direction, { duplicate = false } = {}) => {
const line = new Line(view, direction, duplicate)
return line.move()
}
▶1つ前との差分(クリックで展開)
-export const moveLine = (view, direction, { duplicate = false } = {}) => {
- const document = view.state.doc
- const selection = view.state.selection.main
- const currentLine = document.lineAt(selection.from)
- const totalLines = document.lines
-
- let operation
-
- if (direction === "up") {
- operation = moveLineUp(document, selection, currentLine, duplicate)
- } else if (direction === "down") {
- operation = moveLineDown(document, selection, currentLine, totalLines, duplicate)
- }
+class Line {
+ constructor(view, direction, duplicate = false) {
+ this.view = view
+ this.direction = direction
+ this.duplicate = duplicate
+ }
+
+ move() {
+ if (!this.duplicate && this.#atBoundary) return false
+
+ const operation = this.duplicate
+ ? this.#duplicate()
+ : this.#swap()
- if (!operation) return false
+ this.view.dispatch(operation)
- view.dispatch({
- changes: operation.changes,
- selection: operation.selection
- })
+ return true
+ }
+
+ // private
- return true
-}
-
-const moveLineUp = (document, selection, currentLine, duplicate) => {
- if (currentLine.number === 1 && !duplicate) return null
-
- if (duplicate) {
- return {
- changes: {
- from: currentLine.from,
- to: currentLine.from,
- insert: `${currentLine.text}\n`
- },
+ #duplicate() {
+ return {
+ changes: this.#duplicateChanges,
+ selection: this.#newSelection
+ }
+ }
+
+ #swap() {
+ return {
+ changes: this.#swapChanges,
+ selection: this.#newSelection
+ }
+ }
- selection: {
- anchor: selection.anchor,
- head: selection.head
- }
- }
- }
-
- const previousLine = document.lineAt(currentLine.from - 1)
-
- return {
- changes: {
- from: previousLine.from,
- to: currentLine.to,
- insert: `${currentLine.text}\n${previousLine.text}`
- },
+ get #document() {
+ return this.view.state.doc
+ }
+
+ get #selection() {
+ return this.view.state.selection.main
+ }
+
+ get #currentLine() {
+ return this.#document.lineAt(this.#selection.from)
+ }
+
+ get #moveUp() {
+ return this.direction === "up"
+ }
- selection: {
- anchor: selection.anchor - previousLine.length - 1,
- head: selection.head - previousLine.length - 1
- }
+ get #targetLine() {
+ return this.#document.lineAt(
+ this.#moveUp ? this.#currentLine.from - 1 : this.#currentLine.to + 1
+ )
}
-}
-
-const moveLineDown = (document, selection, currentLine, totalLines, duplicate) => {
- if (currentLine.number === totalLines && !duplicate) return null
-
- if (duplicate) {
- return {
- changes: {
- from: currentLine.to,
- to: currentLine.to,
- insert: `\n${currentLine.text}`
- },
-
- selection: {
- anchor: selection.anchor + currentLine.length + 1,
- head: selection.head + currentLine.length + 1
- }
- }
- }
-
- const nextLine = document.lineAt(currentLine.to + 1)
-
- return {
- changes: {
- from: currentLine.from,
- to: nextLine.to,
- insert: `${nextLine.text}\n${currentLine.text}`
- },
-
- selection: {
- anchor: selection.anchor + nextLine.length + 1,
- head: selection.head + nextLine.length + 1
- }
- }
-}
+
+ get #atBoundary() {
+ return this.#moveUp
+ ? this.#currentLine.number === 1
+ : this.#currentLine.number === this.#document.lines
+ }
+
+ get #selectionOffset() {
+ if (this.duplicate && this.#moveUp) return 0
+
+ const lineLength = this.duplicate ? this.#currentLine.length : this.#targetLine.length
+
+ return (lineLength + 1) _ (this.#moveUp ? -1 : 1)
+ }
+
+ get #duplicateChanges() {
+ return {
+ from: this.#moveUp ? this.#currentLine.from : this.#currentLine.to,
+ to: this.#moveUp ? this.#currentLine.from : this.#currentLine.to,
+ insert: this.#moveUp ? `${this.#currentLine.text}\n` : `\n${this.#currentLine.text}`
+ }
+ }
+
+ get #swapChanges() {
+ return {
+ from: this.#moveUp ? this.#targetLine.from : this.#currentLine.from,
+ to: this.#moveUp ? this.#currentLine.to : this.#targetLine.to,
+ insert: this.#moveUp
+ ? `${this.#currentLine.text}\n${this.#targetLine.text}`
+ : `${this.#targetLine.text}\n${this.#currentLine.text}`
+ }
+ }
+
+ get #newSelection() {
+ return {
+ anchor: this.#selection.anchor + this.#selectionOffset,
+ head: this.#selection.head + this.#selectionOffset
+ }
+ }
+}
+
+export const moveLine = (view, direction, { duplicate = false } = {}) => {
+ const line = new Line(view, direction, duplicate)
+
+ return line.move()
+}
おぉ、このクラスは実に読みやすくなりました!
リファクタリング前は、メソッドの中身を端から読んでいって処理を理解しなければなりませんでしたが、このクラスではmove
メソッドに注目するだけで様子がわかります。処理をもっと詳しく追いたければ、private
以下のゲッターメソッドを深堀りしていけばよいのです。もちろんロジックの大半は変わっていませんが、より小さなメソッドにカプセル化されているおかげで「客観的に」理解しやすくなっています。
このクラスが実際に動く理由は、コードの最後でエクスポートしているmoveLine
がLine
クラスのmove
メソッドを返しているからです。これによって、その親でもmoveLine
としてインポートされるようになります。
これは、「数行以上の長いコードを書くときはクラスを書く必要がある可能性が高い」というアドバイスに最初から私自身が従っていれば、そもそもリファクタリングをせずに済んだという典型的な例です。
JavaScriptを2番目に大好きな言語にしませんか?ぜひ『JavaScript for Rails Developers』をどうぞ。
概要
元サイトの許諾を得て翻訳・公開いたします。
利便性のため、コードのdiffも追加しています。