当記事では、3週間かけてJavaScriptでライブラリ形式のエディターを作った感想とか、エディターづくりの試みから得た知見のようなものを書きなぐっています。
当該エディターのソースコードは以下GitHubリポジトリに置いてあります。
動作確認はこちらからどうぞ。
成果物の概要
今回作ったのは任意のHTML要素にエディターを実装するライブラリです。
import {
TOMEditor
} from "./tom-editor.mjs";
const tomEditorContainer = document.querySelector(".tom-editor-container");
const tomEditor = new TOMEditor(tomEditorContainer);
このような具合に書くことで、上記例での tomEditorContainer
内にエディターが実装されます。
意匠や挙動はVisual Studio Code(厳密にはMonaco Editor)を参考にしています。
制作背景
当エディターは、私が技術学習目的で作っている 独自言語で書かれたソースコードをCommand Promptで動作可能なソースコードに変換するWebアプリ というネタアプリ内で使うことを目的に制作しました。
Advanced Commandprompt Transpiler 動作確認ページ
当初、当該アプリではユーザーからの入力を受けとるのに <textarea>
タグを使っていました。ただ、 <textarea>
タグには本当に最低限の機能しか実装されていないうえ、操作性があまりよくないのが前々から気になっていました。
適当なエディターライブラリにでも置きかえてみようかしらん、と思案していたのですが、どうせならばエディターを自作してみようと思いなおし、エディターの自作に踏みきりました。
制作過程
以前からエディターづくり自体には興味があり、すこしだけエディターの仕組みというものを調べていました。いわく、キーボードの操作を検知して押されたキーに応じた処理を実行するだとか、マウスクリックの座標からキャレットを置く場所を決めるだとか、そういったことを曖昧にですが見聞きしていました。
どのように実装するのかは明確ではありませんが面倒くさい処理になるであろうことは明白でしたので、当初の予定では入力処理やキャレットの移動処理などの基幹となる処理は <textarea>
タグにまかせて、行番号の表示・制御といった周辺の処理をJavaScriptで実装しようという腹積もりでした。
ただ、この方法は思うようにいかず、けっきょくユーザーの操作を EventTarget.addEventListener
メソッドで拾って、各イベントに対応した処理を走らせていくという泥くさい処理を書くはめになってしまいました。
その後もなんやかんやとあって、あるていどの形になるまでに3週間もかかりました。
試みを通して得たこと
今回の試みを通して、いくつか思うところがありましたので書きながら考えをまとめます。
仕様は固めたほうがよいが、そもそも経験不足では固められないから模倣する
前述のように、今回のエディターづくりには3週間という日数を要しました。完全週休2日制の業務として考えれば1ヶ月に相当する期間です。この日数が長いのか短いのか、私には判断がつきません。
ただ、この日数の数日から1週間ほどは実装にかかったというよりも、どのようなエディターを作るかの仕様決めで悩んでいた気がします。仕様さえ早くに決まっていれば、いくらかでも完成が早まったことでしょう。とはいえ、なにぶんエディターづくりは初めての試みであり、何もかもが手探りだったのもまた事実です。エディターづくりに役立つ技術が何かを分かっていませんでしたし、どういう風に組むかも分かっていませんでした。ですので、仕様を定められなかったこと自体は仕方がなかったと思っています。
それに、これは一応完成にまで持っていった今だからこそ言えるのですが、今回の試みを始める前の私が今現在手元にあるソースコードにつながるような仕様書や設計書が書けたかというと、その可能性はかぎりなく0に近いと思うのです。 そもそも開発経験がないものを設計したり要件定義することは難しい というのはあると思うのです。
では、経験がないうちからは行き当たりばったりで突きすすむしかないのかというと、そんなことはありません。「守破離」という有名な言葉があるように 既存作品を模倣・踏襲する ことで、あるていど効率のよい開発を行うことができるはずです。もちろん、模倣といっても丸パクリという意味ではありません。自分が作ろうとしているモノに一番近い仕様の成果物を触ってみたり、ソースコードを読んでみたりして「あぁ、こういう感じになるわけね」と処理の全体像を概観するのがよいという話です。実際、今回作ったエディターライブラリはMonaco Editorを参考にしていますが当初は「Monaco Editorみたいな感じのやつにしようかなぁ」ていどの参考具合でした。それを途中から「迷ったらMonaco Editorの真似をする」と決めてからは実装が円滑に進むようになりました。
こう言うと「いや、それでは身にならないのではないか? イチから考えぬいて完成に持っていくことこそが大事なのではないか?」と思われるかもしれません。実際私にもそのような考えがないわけではありません。ただ、熟練者によって作られたプログラムをソースコードを読んだり、触って挙動を確認したていどで再現できるかというとそんなことはないと思うのです。ソースコードを丸々抽出するようなことさえしなければ、真似して作るだけで得られるものは大きいと考えます。
まさに「見るは易く実装するのは難し」ということで、最初のうちはガタガタ言わずに有名どころの処理を真似して実装していくくらいの気概のほうが早く成長できるでしょう。とはいえ、模倣しすぎては身になりませんし模倣しなかったら無駄に時間がかかってしまうので、そこのところのバランスをとるのが難しいところですね。
安易に最適化するのをやめる
最適化というと個人的には「アーキテクチャ」とか「コンテキスト」とか「ランタイム」などと同程度にはフワッとした言葉だと思うのですが、それはさておき、ここでは あるシステムがもたらす効用と挙動をそのままにソースコードの内容を整理する、あるいは圧縮する(ソースコード中の命令の数を減らすこととほぼ同義)作業 と定義します。
最適化を実施するとソースコードの見通しがよくなります。命令が100あるソースコードと1つしかないソースコードとでは後者のほうが基本的には読みやすいからです。ソースコードの見通しがよくなればエンジニアがシステムの全体像を把握するための労力を抑えることができます。労力が抑えられれば機能を追加しやすくなったり、不具合を修正しやすくなったり、デグレードしにくくなったりと良いことづくめです。もちろん、最適化をするには人員と時間、それにデグレードを起こさないための慎重な検査が必要ですが、それらを許容できるのであれば、どのようなシステムであっても最適化をすることはシステムの品質を高めてくれます。なかでも規模の大きいシステムや中長期的に継続して改良していくシステムほど最適化をすることの利点が大きくなるでしょう。
ただし、 実装段階、とくに実装初期の段階での最適化は推奨できません 。理由は2つあります。
まず、 実装初期の最適化は最適化じゃない からです。私は以前よく自炊をしていました。色々作りましたが、なんといっても牛脂で炒めたチャーハンが一番好きでした。その日の気分や冷蔵庫の具材によってさまざまなチャーハンを作るのですが、最後には必ず塩・胡椒・味の素で味を調えるようにしていました。これがまさに最適化だと思うのです。味を調えるのは完成直前か、あるていど調理が進んでからでないといけないのです。
それはシステムにも言えます。ちょっと書いては最適化、ちょっと書いては最適化。そんなことをしていては無駄に時間がかかります。というか、それって最適化じゃなくて手戻りなんですよね。手戻りしながら実装しているのを最適化という、なんだかそれっぽい言葉で置きかえているだけの話なんです。
また、これが2つ目の理由なのですが、 実装初期段階で最適化しなくてはならない状況に追いこまれていること自体がおかしい のです。だって、実装する前に設計がされていて、設計する前に要件定義がされていれば実装初期に最適化を実施する必要はまるでないはずだからです。
にもかかわらず実装を始めたばかりのタイミングで最適化が必要ということはそういった準備できていないか、準備していないかのどちらかしかないのです。であれば、そのタイミングですべきことは最適化じゃなくて仕様を見直すことなんです。エディターやIDEを閉じて仕様を詰める作業に戻るべきなのです。
やるべきことは小さく区切る
私は意思の弱い人間です。今日こそは洗おうと思っていた敷布団を敷きっぱなしにしていたり、カフェイン断ちをしようと心に決めたはずなのに気づけば朝昼晩とコーヒーを淹れてしまっているほどに意志の弱い人間です。今回のエディターづくりでも何度もやめようと思いました。最初は <textarea>
タグを利用してエディターを作るという目論見が失敗して失望したとき。2度目は半角英数字の入力処理がようやく完成したと歓喜した直後に、現在の処理方法では日本語入力ができないことにきづいて絶望したとき。その後にも、やめようと思った瞬間が繰りかえし訪れました。
そのたびに思いだしたのは「 タスクは小さく区切れ 」という言葉でした。どこで聞いたのかは思いだせないのですが色々な人が言っていますのでどこかで聞いたのでしょう。
我々はしばしば「難しい」という表現を発します。「Aをするのは難しい」「Bの方法では難しい」「Cならば難しくない」といった具合によく使う表現です。この言葉の裏には、すくなくとも2つの異なる思いが隠れています。1つは「どうすれば良いのか分かっているし、実際やろうと思えばできるけれど時間的な制約などから求められている条件内に完遂する自信がないから難しい」というもの。もう1つが「やりたいことは分かるのだけれども、どうすれば目標にたどりつけるのが分からないから難しい」というものです。
プログラミングではこの2つの「難しい」をどちらも経験することになります。そして、心理的に苦しいのは後者の「難しい」です。なまじ目指すべきところが見えているがゆえに苦しい。自身の実装技量のお粗末さに悲しくなる。そして作業を放棄したくなる。
ただ、そんな風に悩んでいるときはタスクを区切れていないことが多いのです。「Aという機能を実装できない……」と苦しんでいるならば「A」という作業単位が大きすぎるのではないかということに目を向けなければなりません。システム全体をいくつかのクラスに分けるように、処理を関数に切りだすように、「A」を「A1」「A2」「A3」というぐあいに分けて実装していくと案外なんとかなるものです。
抽象化の度合いは揃える
ソースコード、あるいはシステムというものは上手に抽象化されていると内容を理解しやすくなるそうです。適切な名前の識別子をつけようだとか、共通の処理は一纏めにしようだとか、オブジェクトの責任範囲がうんたらかんたらと色々な抽象化の技法が提案されています。私も未熟者なりに、そういったことを意識してソースコードを書いてきましたが今回の試みで 抽象化の度合いを「階層」ごとに揃えることを意識している ことに気がつきました。
変数・定数・関数・構造体・クラス・インターフェース・モジュール、なんでもいいですが、ほとんどのプログラミング言語には抽象化を行うための手段・媒体・機能が複数組みこまれています。我々エンジニアは必要な箇所に必要な機能をあてることでソースコードの抽象化を実現しています。そして、それら機能によって抽象化されたプログラムには時として階層構造を見いだすこともできます。たとえば、今回作ったエディタープログラムは以下のような階層構造を意識しながらコーディングを進めていきました。
(※エディターを呼び出す外部プログラム)
└ ■ TOMEditor : エディター本体を表すクラス
├ ■ LineNumberArea : 行番号を表示する領域を制御するクラス
├ ■ TextArea : ユーザーから入力された文章を表示する領域を制御するクラス
├ ■ VirticalScrollbarArea : 垂直方向のスクロールバーを表示する領域を制御するクラス
├ ■ HorizontalScrollbarArea : 平方向のスクロールバーを表示する領域を制御するクラス
├ ■ Caret : キャレットを制御するクラス
└ ■ DecorationUnderLine : キャレットが配置されている行の下に引かれる線を制御するクラス
この分けかたが適切なのか、この図のとおりにプログラムが組まれているのかということはひとまず脇に置いておくとして――このように階層構造を意識してコーディングをすることで、どこに、どこのようなプログラム書かれているのかということを把握する助けになりますし、実装を進めていくうえでもどこを弄るべきなのかの役に立つことは同意いただけると思います。
そして、ここからが本題なのですが、階層化するときは抽象化される要素が属する空間や階層ごとに同じ抽象化度合いを保っている必要があると思うのです。実際私が当エディタープログラムを上記の6要素に分けたのは、それらがエディターからみて同等の関係にあると判断したためです。
これがたとえば以下のような状態であれば分かりにくいこと間違いありません。
(※エディターを呼び出す外部プログラム)
└ ■ TOMEditor : エディター本体を表すクラス
├ ■ LineNumberArea : 行番号を表示する領域を制御するクラス
│
├ ■ TextBackgroundColor : 入力された文章の背景色を制御するクラス
├ ■ TextColor : 入力された文章の色を制御クラス
├ ■ TextLineHeight : 入力された文章の行間を制御するクラス
├ ■ TextFontFamily : 入力された文章のフォントの種類を制御クラス
│
├ ■ VirticalScrollbarArea : 垂直方向のスクロールバーを表示する領域を制御するクラス
├ ■ HorizontalScrollbarArea : 平方向のスクロールバーを表示する領域を制御するクラス
├ ■ Caret : キャレットを制御するクラス
└ ■ DecorationUnderLine : キャレットが配置されている行の下に引かれる線を制御するクラス
「いや、文章のところだけそんな細かく分ける必要あります!?」と私ならツッコんでしまうでしょう。仮に分けたほうがよいとしても適当なクラスから制御するなど、ほかにもっとやり方があるはずです。