このエントリーをはてなブックマークに追加

実技試験のやり直しを通して学んだこと

インターンとして入社した、大同大学 工学部 機械工学科3年の山本 空です。 今回は入社して最初に行われる、インターン選考の際に行われた実技試験のやり直し研修にて気が付いたことを述べていきます。

目次

  • ユーザに支障が出ないように修正する
  • コンピュータに任せるプログラムを書く
  • コンピュータに優しいプログラムを書く
  • 自分や他の方がコードを見たときに瞬間的に理解できるコードを書く

ユーザに支障が出ないように修正する

RPGゲームでラスボス討伐直前にアップデートのためゲームを中断させられ、アップデート後にはレベル1のスタート地点に戻されてしまう。 このようなことがあってはなりません。 コードを細かく修正するときにもコードをプログラムの仕様書通りの結果を維持する必要があります。 つまり、ただ修正すればいいというわけではなく、ユーザには気付いたときには使いやすくなっていたと思われるような修正を心がけることが大切だと思います。

コンピュータに任せるプログラムを書く

私たちは出来るだけサボるために、時間を無駄にしないためにコンピュータに頼っており、プログラムを書く時も同じで出来るだけコンピュータに任せて、私たちがサボれるようなプログラムを書くことが大切で、複雑な長々としたコードを書いてしまってたら、本末転倒です。 ただ自分の目的に応じたプログラムをかけるようなるのでなく、コンピュータに任せられるところは任せることを意識することも大切ではないでしょうか。

コンピュータに優しいプログラムを書く

先ほど述べた内容とは真逆のこと言ってるように思えるかもしれませんが、そうではなく出来る限りコンピュータにも優しくしてあげましょうね、ということです。 無駄な処理や計算をさせてしまうと、コンピュータと言えど応答時間に時間がかかったりしてしまいます。 そのようなことが起きないために無駄な処理を少なくし、簡単な処理が行えるプログラムを書くことも大切です。

自分や他の方がコードを見たときに瞬間的に理解できるコードを書く

自分が最近書いたコードを見直してみましょう。 1年後自分がそのコードを見て何が書いてあるか瞬時に分かりますか?自分じゃない一緒に仕事をしている仲間が見て、どこに何が書いてあるか理解できると思いますか? 数年前に書いたプログラムの修正を早急にするように頼まれた時や、自分が仕事を出来なくなって他の方にプログラムを引き継いでもらうときに、すぐにどこに何が書いてあるか分かるようにしておく必要があります。 近いうちのことだけでなく、先のことも見据えてプログラムを書く事も大切なことかもしれません。

いかがだったでしょうか。 今回書いたことは、どのような状況でも大切なことが書かれていると思うので、もし壁にぶつかったり、思い悩んだりしたときは初心を取り戻す意味も込めて見に来てください! 最後まで読んでくださり、ありがとうございました。:smile:


このエントリーをはてなブックマークに追加

はじめに

こんにちは。フロントエンドの開発を担当しています、松岡です。

コードレビューをしている時 x === undefined で比較してガードするケースをよく見かけます。

function something(x) {
  if (x === undefined) {
    return;
  }
  // 何らかの処理
  // ...
}

xnull だった場合に意図しない結果を生み出すためこのような比較はしないで欲しいですとコメントすると、たいてい「どうして?この関数に null が渡される事はないのに?」と不思議がられます。

レビューコメント用のちっさいテキストエリアでは説明しきれないので、なぜそのようなコメントをするのか私なりの持論をまとめてみました。

検証環境

記事内のサンプルコードは下記の環境で検証したものです。環境によりサンプルコードに記載したアウトプットが少し異なる事があります。ご了承ください。

  • Node v15
  • Chrome v91
  • Firefox v89

undefined とは何か

undefined は JavaScript のグローバル上に存在する変数です。in 演算子を使うとグローバルに undefined が存在している事が分かります。

// Node.js(ブラウザでのアウトプットは異なります)

// undefinedはグローバルに存在する
undefined in globalThis
> true

よく使う下記のようなコードは、グローバル上に存在する undefined という変数の値を代入する事を意味しています。

const x = undefined;

// これと同じ
const x = globalThis.undefined;

undefined | MDN

null とは何か

JavaScript には undefined の他に、空を示す概念として null も存在します。

null はあらゆるオブジェクトの派生元です。Object.getPrototypeOf() で派生元を調べると null に行き着くことが分かります。

// Node.js(ブラウザでのアウトプットは異なります)

// 文字列・数値の派生元はオブジェクト
Object.getPrototypeOf("abc")
> {}
Object.getPrototypeOf(123)
> {}

// オブジェクトの派生元はnull
Object.getPrototypeOf({})
> [Object: null prototype] {}

よく使う下記のようなコードは null というリテラルを代入する事を意味しています。

const x = null;

null | MDN

リテラルとは、データ型を一意に特定することができる値の表現です。文字列リテラル、数値リテラルが分かりやすいのではないかと思います。

// 文字列リテラル
const s1 = "abc";
const s2 = '文字列';

// 数値リテラル
const n1 = 123456;
const n2 = 123_4567;

リテラルとグローバル変数がいまいち分からない場合は下記サイトのコラムを参考にしてみてください。

コラム undefinedはリテラルではない | JavaScriptPrimer

「宣言していない」「undefined の代入」は等価ではない

JavaScript では値を代入する前の変数や、何も返さない関数の戻り値を参照すると undefined が得られます。

let x;
x; // undefined

function fn() {}
fn(); // undefined

上記のケースにおいて undefined は JavaScript が「値が何も設定されていない」事を示すためのものです。開発者が初期化のために undefined を代入しても等価にはなりません。

const user1 = {
  name: "カルテット太郎",
};

const user2 = {
  name: "カルテット花子",
  nickname: "hanako",
};
user2.nickname = undefined;
Object.keys(user1); // ["name"]
Object.keys(user2); // ["name", "nickname"]

"nickname" in user1; // false
"nickname" in user2; // true

for (const i in user1) { console.log(i) }; // "name"
for (const i in user2) { console.log(i) }; // "name" "nickname"

「宣言していない」状態に戻すには delete 演算子を利用します。

delete user2.nickname;

Object.keys(user2); // ["name"]
"nickname" in user2; // false
for (const i in user2) { console.log(i) }; // "name"

delete | MDN

このように「宣言前の空の値が undefined 、宣言後の空の値が null 」と区別するのは誤った理解と言えます。

厳密等価と等価

JavaScript には明示的な型宣言はありませんが、内部では「データ型」というものがあります。

JavaScript のデータ型とデータ構造 | MDN

厳密等価(===)はデータ型を変換せず一致を検査します。

"" === 0 // false
"0" === 0 // false
null === undefined // false
undefined === 0 // false
null === 0 // false

厳密等価 | MDN

等価(==)はデータ型の変換後に一致を検査します。

"" == 0 // true
"0" == 0 // true
null == undefined // true
undefined == 0 // false
null == 0 // false

等価 | MDN

ESLint に等価演算子を禁止するためのルールがあるように、特に理由がなければ厳密等価を利用したほうがコードの可読性が上がるでしょう。

function something(n) {
  // n が "000" の時どのような結果になるのか曖昧
  // そもそもこの関数は文字列を許容するのか?
  return n == 0;
}
function something(n) {
  // 0 以外は全て false 判定になる事が明確
  return n === 0;
}

Disallow Null Comparisons (no-eq-null) | ESLint

厳密等価と等価の例から分かるように、冒頭の if 文は undefined のみを想定したガードです。

// x の値が null, 0, "" の時はガードの対象外
if (x === undefined) {
  return;
}

DOM によって意図せず変化する undefined

ここからは x === undefined でキャッチできないバリデーションについての説明です。まず、シンプルな HTML のフォームを例とします。

<form>
  <input type="text">
  <button type="submit">submit</button>
</form>

input に何も入力せずに submit ボタンをクリックした場合、下記のような undefined によるバリデーションは意味をなしません。なぜなら input.value は文字列で、何も入力しない場合に得られる値は空文字列の "" だからです。

document.querySelector('form').addEventListener("submit", (event) => {
  const input = document.querySelector('input');

  // このバリデーションで未入力を検出する事はできない
  if (input.value === undefined) {
    window.alert("入力されていません");
    event.preventDefault();
  }
});

また何らかのフラグによって DOM を出し分けている場合、存在しない要素に対して querySelectornull を返却します。そのため undefined によるバリデーションは同じく意味をなしません。

if (xxx === true) {
  document.querySelector('form').appendChild(
    document.createElement("input")
  );
}

document.querySelector('form').addEventListener("submit", (event) => {
  const input = document.querySelector('input');

  // input は null のため、このバリデーションで submit を中断する事はできない
  if (input === undefined) {
    window.alert("入力されていません");
    event.preventDefault();
  }
});

さらに input に初期値として undefined を与えた場合 "undefined" という文字列が画面に表示されてしまいます。これを嫌って開発者が null を与えているかもしれません。

// inputに "undefined" という文字列が現れる
const initialValue = undefined;
document.querySelector("input").value = initialValue;

// inputに "" という文字列が現れる
const initialValue = null;
document.querySelector("input").value = initialValue;

上記のようなコードを変数に保存して使い回す場合、初期値として与えた undefinednull"" に変化しているかもしれません。

// データの初期化
let data = {
  inputValue: undefined;
};

// 画面にレンダリング
render(data);

// ユーザーが入力した値を取得
data = getUserInputData();

if (data.inputValue === undefined) {
  throw new Error("入力してください");
}

// 保存
persistUser(data);

コードによって意図せず変化する undefined

平均値を算出するコードを例とします。

function calcAverage(...array) {
  return array.reduce((a, b) => a + b, 0) / array.length;
}

calcAverage(30, 35, 45, 26); // 34

数値の列挙には undefined を想定するものとします。この場合 NaN が返却されるため isNaN によるバリデーションを追加しました。

const result = calcAverage(30, 35, 45, 26, undefined); // NaN

if (isNaN(result)) {
  throw new Error("意図しないパラメータが与えられました");
}

ところが今度は数値の列挙に何らかの理由で null が紛れ込みました。 null は数値の 0 とみなされるため、計算結果は NaN にはなりません。計算結果は数値で返却されるため、何も問題が起こっていないかのように計算処理は続行します。

const result = calcAverage(30, 35, 45, 26, null); // 27.2

// このバリデーションではエラーを検出できない
if (isNaN(result)) {
  throw new Error("意図しないパラメータが与えられました");
}

「何らかの理由」というのがイメージしづらいかもしれませんが null は API レスポンスや開発者による初期化処理など、さまざまな箇所で発生します。

// ==============================
// API レスポンスの仕様の取り決め
// ==============================
[User Response] {
  id: number
  name: string
  age: number (optional)
}

- `GET /users` [User Response] のコレクションを返す
- `GET /users/:id` [User Response] を単体のデータとして返す

バックエンドが Node.js で開発されているようなケースを除き、API の開発者はオプショナルの二通りの表現方法「項目そのものがない事」または「項目は存在し値が null である事」によって JavaScript にどんな影響を与えるのか知り得ないでしょう。リリース当初 undefined だったレスポンスの値が、バックエンドのリファクタやライブラリの差し替えによって null に変わるかもしれません。もし null が与えられたらどう振る舞うのか、想定しておいても損はしないはずです。

fetch(`/users/${id}`).then(response => {
  const json = response.json();

  return {
    id: Number(json.id),
    name: json.name,
    age: json.age === undefined ? 0 : json.age,
  };
});

// リリース当初は user.age は undefined だったため、0 で初期化されていた
// いつの間にか user.age は null に変わり、0 で初期化されなくなった

面倒だから全部 null で初期化しちゃえば?

undefined が意図せず null に変化するのなら、最初から全部 null で初期化すれば解決するのかと言うと、そうでもありません。例えば Array.find は該当の要素が見つからない場合に undefined を返却します。

let user = null;

function findUser(id) {
  [
    { id: 1, name: "a" },
    { id: 2, name: "b" },
  ].find(user => user.id === id);
}

user = findUser(1000); // undefined

// このバリデーションは意味をなさない
if (user === null) {
  throw new Error("ユーザーが見つかりません");
}

TypeScript のような型のある言語を使えば、フロントエンドのコード内のミスを防ぐ事はできるでしょう。

interface User {
  id: number;
  name: string;
  optionalValue?: number;
  nullableValue: number | null;
}

const user1: User = {
  id: 1,
  name: "a",

  // 型定義に違反する値は代入できない
  optionalValue: null, // [ERROR] optional に null は代入できない
  nullableValue: undefined, // [ERROR] nullable に undefined は代入できない
};

ただし型アサーション( as によるキャスト)や API レスポンスなど、ランタイム(TypeScript から JavaScript にトランスパイルした後のコード)で代入される値については防ぎようがありません。

// DOM を経由した値など as でキャストする例
const onChangeEventHandler = (target) => {
  const value = target as string;
}

// 実際には HTMLElement が代入されたとしてもコンパイルエラーは発生しない
console.log(value); // HTMLInputElement {...}
// API レスポンスの値をそのまま使用する例
function parse(response): { id: number, value: number } {
  return {
    id: response.id,
    value: response.value
  };
}

// 期待する number でなく string が代入されたとしてもコンパイルエラーは発生しない
console.log(response); // { id: "0001", value: "1,000円" }

つまり undefined または null どちらに統一しようが、代入される値を束縛する事はできません。厳密等価による判定は、局所的なガードになってしまうのです。

function getUser(id) {}
function parse(user) {}
function render(user) {}

const user = parse(getUser(1));

render(user).then(result => {
  // DOM を経由した場合、空文字列かもしれない
  if (result === "") {}

  // Array.find を経由した場合、undefined かもしれない
  if (result === undefined) {}

  // 初期化ロジックを経由した場合、null かもしれない
  if (result === null) {}
});

結局どうして欲しいのか

ここからは、抜け漏れをできるだけ防ぐために気をつけるポイントを紹介します。TypeScript のコードを紹介していますが、それぞれのポイントで言いたい事は JavaScript でも同じです。

宣言する型の範囲はできるだけ狭くする

開発者が自分自身のコードに課す型宣言は、できるだけ範囲を狭くしてください。

// Optional によって null でなく undefined を扱う事を宣言
interface Params {
  url: string;
  queryStrings?: { [key:string]: string }[]; // undefined を使う事を宣言
}
function request(params: Params) {}

request({
  url: "http://example.com",
  queryStrings: null, // [ERROR] 誤った null の代入
);
// NG
// undefined でも null でも何でも代入できてしまう曖昧さ
interface Params {
  url: string;
  queryStrings: any;
}

// NG
// Optional を使い過ぎると何が必要なのか分からなくなる
interface Params {
  id?: string;
  url?: string;
  queryStrings?: object;
}

受け取る値の範囲はできるだけ広くする

ランタイムで値が代入される箇所、または複数のロジックを経由して到達した箇所では、できるだけ広い範囲の値を扱えるようにしてください。

// undefined / null / 空文字列 どれを受け取っても同じ結果になる
// (0 も対象になるため数値の扱いには気をつける事)
function something(value: string | null) {
  if (!value) {
    throw new Error("パラメータが不正です");
  }
}
// undefined / null を等価に扱う例
interface Param {
  foo?: {
    bar: { value: number } | null
  }
}

function something(param: Param): number {
  // Optional chaining(?.) 
  // undefined / null どちらでもオブジェクトのプロパティを辿れる
  let x = param.foo?.bar?.value;

  // Logical nullish assignment(??=) 
  // undefined / null いずれかであれば右辺の値を代入する
  x ??= 1;

  return x + 100;
}
// NG
// undefined / null が考慮されていない
function something(value: string) {
  if (value === "") {
    throw new Error("パラメータが不正です");
  }
}

暗黙の型を避ける

暗黙の型を返却せず、どのような型を返却するかを明確に宣言してください。

interface User {
  id: number;
  address: string | null;
}

// undefined を null に変換するコードが強制される
function pickupAddress(user: User, users: User[]): string | null {
  return (users.find(user => user.id)?.address) || null;
}
// NG
// 戻り値の型を宣言していない
// string | null | undefined と推論される
function pickupAddress(user: User, users: User[]) {
  return users.find(user => user.id)?.address;
}

// 型宣言が面倒になるので後続の処理が any や Optional になりがち
const address = pickupAddress();

終わりに

undefined null をきちんと区別したコードを書くべきとする持論もあるでしょう。

経験に基づく主張ですが、どのような場面で変数の値が undefined null に変化するのか熱心に気を配っていても、想定外の値は混入します。そして、それが原因で想定外のエラーが発生した場合、不利益を被るのはシステムを利用しているユーザーです。デバッグすると想定外に何故か現れる null 値、そしてコードを追いかけると見つける if (x === undefined) によるバリデーション。システムリリース後の不具合報告で、何回もこのパターンに出くわした事があります。

undefinednull がそれぞれ「未登録」や「ユーザーによる値の消去」など意味を持つのであれば話は違いますが、ほとんどのケースにおいて undefinednull は「空」という等価な意味合いではないでしょうか?

x === undefined の比較をしないでください。

これは JavaScript を知り尽くしたような顔をしてミスを指摘しているのではなく、提案としてのレビューコメントです。想定外に紛れ込む null を考慮しておいたほうがメリットが大きいと思うのですが、いかがでしょうか?どうか、ご検討いただければと思います。


このエントリーをはてなブックマークに追加

先週SymfonyWorld 2021 Summer Editionのトークで @TobiasNyholm さんが紹介していたRuntimeコンポーネントを早速使ってみたのでご紹介します!

Runtimeコンポーネントとは

Symfony5.3から導入された新しいコンポーネントです。
実行環境(php-fpm, AWS Lambda, PaaS, swoole, …)ごとのフロントコントローラの違いをクラスとして提供してくれます。
https://symfony.com/doc/current/components/runtime.html

Runtimeコンポーネントで何が嬉しいか?

Runtimeコンポーネント導入前まで、 symfony new して作ったSymfonyプロジェクトは、スタンダードなApache・nginxで動かすことを前提にした public/index.php だけが配布されていました。 それ以外の特殊な実行環境(たとえば「SymfonyをAWS Lambdaで」とか)でSymfonyを動かそうと思ったら、 $_SERVER を自前でセットする等のやり方をググって、自力で public/index.phpbin/console をカスタマイズする必要がありました。

Runtimeコンポーネントは各実行環境ごとのカスタマイズ内容を、再利用可能な形でSymfonyコンポーネントとして提供してくれるため、自力でカスタマイズを適用する必要はなくなりました :relaxed:

既存のSymfonyプロジェクトにRuntimeコンポーネントを導入する方法

前提として、Symfony5.3まで更新してあるプロジェクトが対象です。

symfony/runtime導入

composer require symfony/runtime

vendor/autoload_runtime.php作成, public/index.phpの更新

composer recipes:install symfony/framework-bundle -v --force

config/framework.yamlに自分の設定を書き込んでいる場合は、設定が初期状態まで巻き戻ってしまうため、git diffを見て自分の設定した部分は復元しましょう。

bin/consoleの更新

composer recipes:install symfony/console -v --force



必要な変更をコミット後、 symfony server:start してみると、今まで通りにアプリケーションが動くことを確認できると思います(私はできました)。
vendor/autoload_runtime.php を見てみると、デフォルトのRuntimeとして Symfony\Component\Runtime\SymfonyRuntime が指定されていること、Runtimeを変更したい場合は $_SERVER['APP_RUNTIME'] を変更すれば良いことがわかります。

<?php

// autoload_runtime.php @generated by Symfony Runtime

if (true === (require_once __DIR__.'/autoload.php') || empty($_SERVER['SCRIPT_FILENAME'])) {
    return;
}

if (PHP_VERSION_ID < 80000 && in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
    ob_start();
    $app = require $_SERVER['SCRIPT_FILENAME'];
    ob_end_clean();
} else {
    $app = require $_SERVER['SCRIPT_FILENAME'];
}

if (!is_object($app)) {
    throw new TypeError(sprintf('Invalid return value: callable object expected, "%s" returned from "%s".', get_debug_type($app), $_SERVER['SCRIPT_FILENAME']));
}

$runtime = $_SERVER['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime';
$runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? []) + [
  'project_dir' => dirname(__DIR__, 1),
]);

[$app, $args] = $runtime
    ->getResolver($app)
    ->resolve();

$app = $app(...$args);

exit(
    $runtime
        ->getRunner($app)
        ->run()
);

感想

ますますコアなアプリケーション部分の開発に集中できる方向に進化してくれるSymfonyがやっぱり大好きだな〜と思いました。