エンジニアとしての強み

各カテゴリの詳細を確認できます

宣言性

命令型ではなく宣言型のプログラミングスタイルを採用するメリットと実践方法について紹介します。

オブジェクトの変更スコープを使用スコープよりも深いスコープとする
変更可能性を制限することでコードの予測可能性を高める

理由

仮に使用スコープ(生存期間)が長く、変更が同じスコープで可能となっている場合、オブジェクトの内容をコードから理解することが困難になってしまいます。

解決策:

  • constなどで不変となる変数宣言を行う

    ただし、参照型の場合、ここで不変制約が課されているのは参照に対してであり、メソッドを介した変更に対しては不変制約は課されません。

  • オブジェクトの作成が複雑な場合:

    関数型メソッド等を使用してスコープを分けます。

実装例:変更スコープの制限

// 悪い例:広いスコープでオブジェクトが変更可能
function processUserData(userData) {
  // userData オブジェクトが関数内で変更される
  userData.lastAccessed = new Date();
  
  if (userData.role === 'admin') {
    userData.permissions = ['read', 'write', 'delete'];
  } else {
    userData.permissions = ['read'];
  }
  
  // 他の処理...
  
  // さらに変更が加えられる
  if (userData.status === 'active') {
    userData.accessCount = (userData.accessCount || 0) + 1;
  }
  
  return userData;
}

// 良い例:変更スコープを限定し、新しいオブジェクトを返す
function processUserData(userData) {
  // 元のオブジェクトを変更せず、新しいオブジェクトを作成
  const now = new Date();
  
  // 権限の決定を別の関数に分離
  const permissions = determinePermissions(userData.role);
  
  // アクセスカウントの更新を別の関数に分離
  const accessCount = updateAccessCount(userData.status, userData.accessCount);
  
  // 新しいオブジェクトを作成して返す
  return {
    ...userData,
    lastAccessed: now,
    permissions,
    accessCount,
  };
}

// 権限を決定する純粋関数
function determinePermissions(role) {
  return role === 'admin' ? ['read', 'write', 'delete'] : ['read'];
}

// アクセスカウントを更新する純粋関数
function updateAccessCount(status, currentCount = 0) {
  return status === 'active' ? currentCount + 1 : currentCount;
}

// React での例
// 悪い例:コンポーネント内で直接状態を変更
function UserProfile({ user }) {
  const [userData, setUserData] = useState(user);
  
  const handleRoleChange = (newRole) => {
    // 直接状態を変更
    userData.role = newRole;
    
    // 変更に基づいて他のフィールドも更新
    if (newRole === 'admin') {
      userData.permissions = ['read', 'write', 'delete'];
    } else {
      userData.permissions = ['read'];
    }
    
    // 変更されたオブジェクトで状態を更新
    setUserData(userData); // 注意: これは参照が同じなので再レンダリングされない可能性がある
  };
  
  // ...
}

// 良い例:イミュータブルな状態更新
function UserProfile({ user }) {
  const [userData, setUserData] = useState(user);
  
  const handleRoleChange = (newRole) => {
    // 新しいオブジェクトを作成して状態を更新
    setUserData(prevData => ({
      ...prevData,
      role: newRole,
      permissions: newRole === 'admin' ? ['read', 'write', 'delete'] : ['read'],
    }));
  };
  
  // ...
}
純粋関数とする
副作用のない予測可能な関数を作成する

参照透過性(同じ引数に対して同じ値を返す)を持ち、副作用がない関数を純粋関数と呼びます。純粋関数であるメソッドを利用することで、引数以外の文脈に注意を払わずその関数のふるまいを考えることができる為、コードから動作が把握しやすいものとなります。

実装例:純粋関数

// 悪い例:副作用のある関数
let globalCounter = 0;

function calculateTotal(items) {
  // グローバル変数を変更する副作用
  globalCounter++;
  
  // 現在時刻に依存する副作用
  const now = new Date();
  console.log(`Calculation performed at ${now.toISOString()}`);
  
  let total = 0;
  for (const item of items) {
    total += item.price * item.quantity;
  }
  
  return total;
}

// 良い例:純粋関数
function calculateTotal(items) {
  return items.reduce((total, item) => total + (item.price * item.quantity), 0);
}

// 副作用を分離する例
function logCalculation(total) {
  const now = new Date();
  console.log(`Calculation performed at ${now.toISOString()}: ${total}`);
}

function incrementCounter() {
  globalCounter++;
  return globalCounter;
}

// 使用例
const items = [
  { name: 'Item 1', price: 10, quantity: 2 },
  { name: 'Item 2', price: 15, quantity: 1 },
];

// 純粋関数を使用
const total = calculateTotal(items);

// 副作用を分離
logCalculation(total);
incrementCounter();

// 関数型プログラミングの例:配列操作
// 悪い例:命令型で配列を変更
function doubleAndFilterEven(numbers) {
  const result = [];
  for (let i = 0; i < numbers.length; i++) {
    const doubled = numbers[i] * 2;
    if (doubled % 2 === 0) {
      result.push(doubled);
    }
  }
  return result;
}

// 良い例:関数型で純粋関数を使用
function doubleAndFilterEven(numbers) {
  return numbers
    .map(num => num * 2)
    .filter(num => num % 2 === 0);
}

// React での例
// 悪い例:副作用を含むコンポーネント
function UserList() {
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    // 直接 API を呼び出す
    fetch('/api/users')
      .then(response => response.json())
      .then(data => setUsers(data));
  }, []);
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 良い例:カスタムフックに副作用を分離
function useUsers() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchUsers = async () => {
      try {
        const response = await fetch('/api/users');
        const data = await response.json();
        
        if (isMounted) {
          setUsers(data);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          setError(err);
          setLoading(false);
        }
      }
    };
    
    fetchUsers();
    
    return () => {
      isMounted = false;
    };
  }, []);
  
  return { users, loading, error };
}

function UserList() {
  const { users, loading, error } = useUsers();
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
引数をオブジェクトとして定義する
関数の引数を明確にする

引数を多く定義した場合、引数がどのような順番で定義されているか確認する必要が生じてしまいます。特に同じプリミティブ型で引数が連続で定義されている場合、最悪ランタイムエラーすら生じず、間違いの発見がかなり後になってしまう可能性があります。

実装例:オブジェクトとしての引数定義

// 悪い例:多数の引数
function createUser(firstName, lastName, email, password, age, isAdmin, country, city, zipCode, street) {
  // 引数が多すぎて、呼び出し側で順序を間違える可能性が高い
  // ...
}

// 呼び出し例(どの引数が何を表しているのか分かりにくい)
createUser('John', 'Doe', 'john@example.com', 'password123', 30, false, 'USA', 'New York', '10001', '123 Main St');

// 良い例:オブジェクトとしての引数
function createUser({
  firstName,
  lastName,
  email,
  password,
  age,
  isAdmin = false,
  address = {}
}) {
  // 名前付き引数として扱えるため、順序を気にする必要がない
  // デフォルト値も設定しやすい
  // ...
}

// 呼び出し例(各引数の意味が明確)
createUser({
  firstName: 'John',
  lastName: 'Doe',
  email: 'john@example.com',
  password: 'password123',
  age: 30,
  address: {
    country: 'USA',
    city: 'New York',
    zipCode: '10001',
    street: '123 Main St'
  }
});

// TypeScriptでの型定義を使った例
interface CreateUserParams {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
  age: number;
  isAdmin?: boolean;
  address?: {
    country?: string;
    city?: string;
    zipCode?: string;
    street?: string;
  };
}

function createUser(params: CreateUserParams) {
  // 型安全な実装
  // ...
}

// React での例
// 悪い例:多数のprops
function UserProfile(props) {
  return (
    <div>
      <h1>{props.firstName} {props.lastName}</h1>
      <p>Email: {props.email}</p>
      <p>Age: {props.age}</p>
      <p>Country: {props.country}</p>
      <p>City: {props.city}</p>
      {/* ... */}
    </div>
  );
}

// 良い例:オブジェクトとしてのprops
function UserProfile({ user, address }) {
  return (
    <div>
      <h1>{user.firstName} {user.lastName}</h1>
      <p>Email: {user.email}</p>
      <p>Age: {user.age}</p>
      <p>Country: {address.country}</p>
      <p>City: {address.city}</p>
      {/* ... */}
    </div>
  );
}

// 呼び出し例
<UserProfile 
  user={{
    firstName: 'John',
    lastName: 'Doe',
    email: 'john@example.com',
    age: 30
  }}
  address={{
    country: 'USA',
    city: 'New York',
    zipCode: '10001',
    street: '123 Main St'
  }}
/>
コメントをメソッド名に変更
自己説明的なコードを書く

仕様変更をコメントに記載しメソッドを変えないケースがありますが、参照元での対応が保証されません(メソッド名を変更した場合、対応しなければエラーとなります)。コメントは参照元のプログラムでは明示されないため、メソッド名が抽象的でそこから想像することが難しい仕様がある場合、これはコードより明らかでありません。

実装例:自己説明的なメソッド名

// 悪い例:コメントに依存した実装
/**
 * ユーザーを処理する
 * 注意: このメソッドは管理者権限を持つユーザーのみを処理します
 * 注意: 非アクティブなユーザーは無視されます
 */
function processUser(user) {
  if (!user.isAdmin || !user.isActive) {
    return;
  }
  
  // ユーザー処理ロジック
}

// 呼び出し例
processUser(user); // コメントを読まないと何が起こるか分からない

// 良い例:自己説明的なメソッド名
function processActiveAdminUser(user) {
  if (!user.isAdmin || !user.isActive) {
    return;
  }
  
  // ユーザー処理ロジック
}

// 呼び出し例
processActiveAdminUser(user); // メソッド名から何が起こるか明確

// 悪い例:コメントで説明された複雑なロジック
function calculate(a, b, type) {
  // type が 1 の場合は加算
  // type が 2 の場合は減算
  // type が 3 の場合は乗算
  // type が 4 の場合は除算
  switch (type) {
    case 1: return a + b;
    case 2: return a - b;
    case 3: return a * b;
    case 4: return a / b;
    default: throw new Error('Invalid type');
  }
}

// 良い例:明示的な関数名
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
function multiply(a, b) { return a * b; }
function divide(a, b) { return a / b; }

// または、オブジェクトとして関数をグループ化
const calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b
};

// 使用例
calculator.add(5, 3); // 明確に何をしているかが分かる
空のコンストラクタ+set関数の組み合わせの廃止
オブジェクト生成を明確にする

実際に作成されるオブジェクトの内容がコードから理解しづらくなります。

実装例:明確なオブジェクト生成

// 悪い例:空のコンストラクタとセッターの組み合わせ
class User {
  constructor() {
    // 空のコンストラクタ
    this.firstName = '';
    this.lastName = '';
    this.email = '';
    this.isAdmin = false;
  }
  
  setFirstName(firstName) {
    this.firstName = firstName;
    return this;
  }
  
  setLastName(lastName) {
    this.lastName = lastName;
    return this;
  }
  
  setEmail(email) {
    this.email = email;
    return this;
  }
  
  setIsAdmin(isAdmin) {
    this.isAdmin = isAdmin;
    return this;
  }
}

// 使用例
const user = new User()
  .setFirstName('John')
  .setLastName('Doe')
  .setEmail('john@example.com')
  .setIsAdmin(false);

// 問題点:
// 1. どのプロパティが必須で、どれがオプションなのか不明確
// 2. 不完全なオブジェクトが作成される可能性がある
// 3. オブジェクトの状態が変化する過程が追跡しづらい

// 良い例:明確なコンストラクタと不変オブジェクト
class User {
  constructor(firstName, lastName, email, isAdmin = false) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.email = email;
    this.isAdmin = isAdmin;
    
    // オブジェクトを不変にする(オプション)
    Object.freeze(this);
  }
  
  // 変更が必要な場合は新しいオブジェクトを返す
  withAdmin(isAdmin) {
    return new User(this.firstName, this.lastName, this.email, isAdmin);
  }
  
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

// 使用例
const user = new User('John', 'Doe', 'john@example.com');
const adminUser = user.withAdmin(true);

// TypeScriptでの例
interface UserParams {
  firstName: string;
  lastName: string;
  email: string;
  isAdmin?: boolean;
}

class User {
  readonly firstName: string;
  readonly lastName: string;
  readonly email: string;
  readonly isAdmin: boolean;
  
  constructor(params: UserParams) {
    this.firstName = params.firstName;
    this.lastName = params.lastName;
    this.email = params.email;
    this.isAdmin = params.isAdmin ?? false;
  }
  
  withAdmin(isAdmin: boolean): User {
    return new User({
      ...this,
      isAdmin
    });
  }
}

// 使用例
const user = new User({
  firstName: 'John',
  lastName: 'Doe',
  email: 'john@example.com'
});
モジュールのimportは最小限に
依存関係を明確にする

依存関係が不明確になる

あるモジュールを削除したい場合、そのモジュールを使用しているモジュールを特定する必要があります。多くのツールではimport文でファイル検索を行いますが、無駄なimportが多くある場合、これを特定することができません。

実装例:最小限のimport

// 悪い例:不要なインポート
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Button, Input, Form, Select, Checkbox, Radio, Switch, DatePicker } from 'some-ui-library';
import { formatDate, formatCurrency, formatNumber, formatPercentage } from '../utils/formatters';
import { validateEmail, validatePassword, validateName } from '../utils/validators';
import { useUser, useAuth, usePermissions } from '../hooks';

function UserProfileForm() {
  // 実際には useState と useEffect しか使っていない
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // ユーザー情報を取得
  }, []);
  
  // Button と Input しか使っていない
  return (
    <Form>
      <Input value={user?.name} />
      <Button>保存</Button>
    </Form>
  );
}

// 良い例:必要なものだけをインポート
import React, { useState, useEffect } from 'react';
import { Button, Input } from 'some-ui-library';
import { formatDate } from '../utils/formatters';
import { validateEmail } from '../utils/validators';
import { useUser } from '../hooks';

function UserProfileForm() {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // ユーザー情報を取得
  }, []);
  
  return (
    <form>
      <Input value={user?.name} />
      <Button>保存</Button>
    </form>
  );
}

// さらに良い例:必要に応じて動的インポート
import React, { useState, useEffect, lazy, Suspense } from 'react';
import { Button, Input } from 'some-ui-library';

// 必要になったときだけロードする重いコンポーネント
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function UserProfileForm() {
  const [user, setUser] = useState(null);
  const [showHeavyComponent, setShowHeavyComponent] = useState(false);
  
  // ...
  
  return (
    <form>
      <Input value={user?.name} />
      <Button onClick={() => setShowHeavyComponent(true)}>詳細を表示</Button>
      
      {showHeavyComponent && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent user={user} />
        </Suspense>
      )}
    </form>
  );
}