Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

tidy first?を読んだ

in short

  • 内容自体はとても良かったし勉強になった
  • だが、文章がおかしくて読みづらかったし、サンプルも図も不親切
  • 初心者が読んで100%理解できるのかは疑問(読まないよりは読んだほうが良いと思うが)

感想1: 読みづらかった

全体的な感想として、とても読みづらかった。 まず、(訳と原文のどちらの問題なのかわからないけど、) 言い回しが妙なところが多い。 読んでてかなりつっかえた。

あと、サンプルがどれも具体性がない。 ちょっと考えれば「あのことを言ってるのかな」とわかるが、経験の浅い初学者がこれで理解できるとは思えない。

図に関しても分かりづらい。 本文と合致してなかったり、何を意味してるのか提示されてない図が多かった。

結構多くの人がレビューに関わっていたみたいだし、前評判も良かっただけにとても残念だった。

感想2: 内容はとても良かった

読みづらくはあったが、言ってる内容自体はとても良かった。 何度も「確かに」と頷いたし、「そういうことだったんだ」と合点が言った部分もたくさんあった。

たとえば、17ページの以下。

ファイル中のコードを読み手(それぞれの書き手にたくさんの読み手がいることを忘れないように)が遭遇したいと思う順番に並べ替えよう。

基本的だけどとても大事なこと。読者目線を持つことは、プログラムだけでなく文章を書くうえでも大事なルールだ。

各部ごとの感想: 第I部は必読

第I部の整頓はどの章も重要だ。 個人的には特に15章の「冗長なコメントを削除する」ができるようになるとプログラマとしてかなりレベルが上がると思う。

ほかにも、11章の「ステートメントを小分けにする」も、ほかの書籍で言及されることは少ないけど僕もかなり大事だと思っている。プログラムコードも意味ごとに「段落」を作ると読みやすくなる。

第III部もよかった

第II部、第III部は特に読みづらい部分が多かった。でも、 第III部は大事な話も書いてあった (第II部はそんな目新しいことは書いてなかったかな)。

例えば、97ページでは、なるべく不可逆的な変更(もとに戻せない変更)を避け、可逆的な変更(もとに戻せる変更)で代替しようという話が出てくる。これは『Clean Architecture』(注:ここでは書籍名を指す)の15章に出てくる「選択肢を残しておく」とも共通している。可逆的な変更にはどんどん果敢に取り組んでいきたい。

結合、凝集のわかりやすい定義が書かれていた

101ページには結合の定義、113ページには凝集の定義が書いてある。これがとても分かりやすかった。今までふわふわしてたが、かなり腑に落ちた。

この変更の伝播特性は「結合(Coupling)」と名付けられた。1つの要素を変更するのに他の要素の変更が必要であれば、2つの要素は特定の変更に関して結合している。

(p. 101)

この結合の説明はとても明快でわかりやすい。 また、なぜ結合を減らすことがコードを読む、管理する上で大事なのかも理解しやすい。

結合した要素は同じ親要素の子要素同士であるべきだ。これが凝集(Cohesion)の1つめの意味合いだ。(中略)凝集の2つめの意味合いは、堆肥ではない要素(つまり結合していない要素)は別の場所に移すべきということだ。

(p. 113)

この凝集の説明もわかりやすい。結合した要素がバラバラに配置されているパターンはよくある(repository/FooRepo、model/FooModel みたいな)、package by layer)。これは凝集度が低い。それに対して、親が同じ状態(Foo/FooRepo、Foo/FooModel)は凝集度が高い。まとまり方が良いと言い換えても良い。親に視点を移して語るのはあまり見ないけど確かになと思った。

まとめ: 勉強にはなったけど、初学者向きかは疑問

勉強になったし考えの整理もできたので、個人的には読んで良かった。でも、文章が読みづらかったりサンプルコードや図が分かりづらいので、初学者が読んですぐ理解できるかは疑問だ。おそらく、多少経験があって行間を補完できる程度の想像力がないと効果が半減すると思う。

誰かtidy first?の行間を埋めて解説する30分くらいの発表してくれないかなぁ…それがあるととても良いなと思った。

rowspanを考慮してtable要素を2次元配列に変換する(第一正規化する)

rowspan、colspanがあるとtd:nth-of-type などがズレてしまう。

td:nth-of-type(2) に背景色をつけた図

html

<table>
  <tr>
    <td>col1,1</td>
    <td rowspan="3">col1,2</td>
    <td>col1,3</td>
  </tr>
  <tr>
    <td>col2,1</td>
    <td>col2,2</td>
  </tr>
  <tr>
    <td>col3,1</td>
    <td>col3,2</td>
  </tr>
  <tr>
    <td>col4,1</td>
    <td>col4,2</td>
    <td>col4,3</td>
  </tr>
</table>

2列目に色をつけたいのに、col2,2col3,2にも色がついてしまっている。本来はこうなってほしい↓

2列目だけに色がついている図

これをするには第一正規化してやればよい。正規化する関数 normalizeTable の実装はこちら。

const normalizeTable = (table: HTMLTableElement) => {
  const rows = table.querySelectorAll<HTMLTableRowElement>('tr');
  const tableCells = Array.from(rows).map((row) =>
    Array.from(
      row.querySelectorAll<
        HTMLTableDataCellElement | HTMLTableHeaderCellElement
      >('td, th'),
    ),
  );

  for (let r = tableCells.length - 1; r >= 0; r--) {
    const row = tableCells[r];
    if (!row) continue;
    for (let c = row.length - 1; c >= 0; c--) {
      const cell = row[c];
      if (!cell) continue;
      if (cell.rowSpan === 1 && cell.colSpan === 1) continue;
      for (let i = 0; i < cell.rowSpan; i++) {
        for (let j = 0; j < cell.colSpan; j++) {
          if (i === 0 && j === 0) continue;
          tableCells[r + i]?.splice(c + j, 0, cell);
        }
      }
    }
  }
  return tableCells;
};

tableCells[row][col] には見た目の位置どおりのtd要素が入っている(rowspanが指定されたtdは複数箇所に入ってる)。あとは、normalizeTable を使って適当にクラスを振ってやればよい。

const cells = normalizeTable(document.querySelector('table'))
for (const row of cells) {
  for (let i = 0; i < row.length; i++) {
    row[i].classList.add(`col${i}`);
  }
}

これで td.col1 に背景色をつけてやれば意図した通りにうごく

転職活動の結果報告 + 転職の動機とか

ひとまず結果が出揃ったので報告です(※まだ転職先は決定してないです)。

合計12社受けました。 内訳がこちら↓

  • 1次落ち: 4社 (内訳: エージェント2社、スカウト1社、自己応募1社)
  • 2次落ち: 1社 (スカウト1社)
  • 最終落ち: 2社 (スカウト2社)
  • 内定: 5社 (スカウト4社、エージェント1社)

カジュアル面談を合わせると 20は超えてるはず。めちゃくちゃ受けたな…

時系列

  • 去年11月: カジュアル面談を受け始める
  • 今年1月: 選考を受け始める
  • 2月中旬: 退職時期を決める
  • 2月末: 受けていた5社すべてに落ちる (最終2社、1次3社)
  • 3月: 7社受け始める
  • 4月: 初内定
  • 4月18日: 最後の選考結果が出る (5社内定)

2月までに受けていた5社が全滅して、結構驚きました。もっとすんなり決まるだろうと油断してた ので(スカウトも結構もらってたし)。退職時期も先に決めていたため、このときはかなり焦りました。

全落ちを受けて反省した(別記事)ので、後半7社では 「1、2社から内定きたら御の字だな」 と思いながら受け始めました。でも今度は7社中5社から内定をいただけました。 じゃあ前半の惨敗はなんだったんだよ。

所感: スカウトだと通りやすい

やっぱり スカウトされた選考が通りやすかった ですね。おそらく「入ってもらうならココ!」と最初からポジションがイメージされていた、というのも大きいです。

スカウトされた中では1社だけ1次で落ちましたが、そこはポジションが全然噛み合ってなかったです(インフラまで範囲だった)。噛み合えばそこもいけてたかも。

エージェント経由で受けたところは本当につんつるてん でした。1社しか通らなかったです。なんでなのかは結局はっきりとは分かりませんでした。純粋に僕が面接下手くそでスカウトという下駄履かせてもらわないとダメ説はあります。

今回の転職の動機

今回の転職の動機についても書いておきます。でもこれが とてもややこしいんですよね… 選考でも聞かれたし、周りからもめちゃくちゃ聞かれたんですが、毎回答えるのに困りました。

端的にまとめると、

  • 最終的なモチベーションは 「新潟にUターンしたいから」
  • 転職を考えたきっかけは 「 LY社(現職)の週1出社」 (に関連する意思決定フローの不透明さ、モチベーションに関する説明不足、施策の妥当性への納得感の欠如など)

転職のきっかけ: 週1出社

去年12月にLY社(現職)が週1出社を決定したのを受け、カジュアル面談を受け始めました。 正直そのときはメチャクチャ会社にブチギレてました。 もう1秒たりとも居てたまるかってくらい。出社させるモチベもわからないしやり方も納得できなかったです。出社したくないというより、社員のことを考えてなさそうに見えたのが大きかったです。

ただ、最終選考で某社の社長とお会いして話したときに、LY社の週一出社のモチベーションについてウワサを聞いて、モチベについては一応納得しました (たぶん社外秘にあたるので詳細は伏せます)。聞いた内容的には社員を考えてない訳では無さそうでした。まあ方法論に関してはまだ納得してないですが…

というわけなので、いまは LY社には全然キレてません。 なんなら良い会社ではあったなと思います(出社の件はもっとやりようがあったと思いますが)。僕の政治的思想(アンチ東京一極集中)と合わなくなったのもあって離れる決意をしただけで、馴染むのであれば 全然オススメできる会社です。 けっこう伸び伸びと働けましたし、ここまで大きく成長できたのは絶対にLINE社に入れたおかげです。

最終的なモチベーション: 新潟にUターンしたいから

というわけで転職のモチベとしては「地元に戻る」のが一番大きいんですが、実は 当初は全然Uターンを考えてませんでした。 転職するにしても都内のままと思ってました。でも、転職という選択肢が頭にある状態で正月に帰省したとき、 「新潟めっちゃいいな…」 と思いました。これが大きかったです。正月の帰省がなかったら、週一出社のモチベに納得した時点で転職をやめていたかもしれません。

東京という街が嫌い(独身の街だから)という思想もUターンと噛み合いました。もう十分東京で遊んだなと思うので、これ以上はいても仕方ないです。たぶん向こう5年は新潟います。新潟いいですよ。めっちゃ住みやすい。

出社動機まとめ

以上をまとめると、

  1. 当初は週一出社にブチギレていたから転職しようと思っていたが、
  2. 選考の途中で週一出社のモチベーションについて知って納得して、
  3. 最後は「新潟へUターンしたい」のがモチベーションになった

これが今回の転職の動機の全容です。

給与上げたいとか環境変えたいみたいな理由は殆どないです。現職がちょっと上振れているので、どこを選んでも待遇面は多少下がると思います。

まとめ: 今後について

前半はメンタルがボロボロになってきつかったですし、後半も週に5回選考があったりかなり忙しくて大変でした。もうしばらくは転職活動なんてしたくない……

今後はひとまずGW前後を目安に会社を決めて、新潟での物件を選んで、7月あたりに引っ越しって感じで予定してます。5月から1ヶ月半は有給消化期間ですが、とはいえ意外と余裕がないです。ワタワタしそう。

あと、おそらく転職関連の記事はこれが最後です。入社エントリも退職エントリも今のところ書くつもりないので。

機能別のECMAバージョン対応表

ある機能がどのECMAバージョンで入ったのかの一覧表。ある程度グルーピングをしてある。

2025年現在のproposalsのfinished-proposals.mdを参照している。

(なお、全部確認はしたはずだが、間違ってる可能性もある。抜け等あったらコメントお願いします🙏)

Array

Atomic

Class

Function

Module

Object

Operator

Promise

RegExp

String

TypedArray

Other

propsが変更されたときにすべてのstateをリセットするHOCを作る

react.devの「そのエフェクトは不要かも」のprops が変更されたときにすべての state をリセットするに書かれている解決策が微妙に感じたので、改善案を提案します。

そもそも: 公式の解決策がなぜ微妙に感じるのか?

props が変更されたときにすべての state をリセットするでは、propが変更されたときに必ずstateをリセットする制約を作るために、子コンポーネントへkeyを設定するだけのラッパーコンポーネントを作ろう と提案してます。

↓(修正前) userId propが更新されたとき、useEffectを使って comment stateをリセットしているコンポーネント

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

↓(修正後) userId propが更新されたとき、keyを使って comment stateをリセットしているコンポーネント

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}

…まあ言いたいことは理解できるんですが、 コンポーネントを分けなきゃいけないところが微妙に感じます。

  • Profileを単体でみたとき「userIdが変わったらcommentがリセットされる」という事情が読み取れない
  • 分割後の子コンポーネントの名前の付け方が難しい

このように 公式の解決策には問題があります。 なので、代わりにHOCを作ればいいんじゃないか?というのが本記事の提言です。

別の解決策: withResetOnPropChangeというHOCを作る

要はkeyを指定するだけのラッパーを作れたらよいのですから、それをする高階コンポーネント(Higher-Order Component)を作ればよさそうです。こんな感じです↓

const withResetOnPropChange =
  <Props extends Record<string, unknown>>(Component: FC<Props>, key: string) =>
  (props: Props) => <Component key={props[key] as string} {...props} />;

これを使うと上のProfilePageはこのように書けます。

export default withResetOnPropChange(function ProfilePage({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}, 'userId');

コンポーネントの分割もなくなり、コードの意図もシンプルに読み解けるようになりました。個人的にはかなり良いんじゃないかなと思ってます。