0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Day 24: 既存フレームワーク(Angular)との比較

Last updated at Posted at 2025-12-23

この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの14日目です。
Structiveについて詳しくはこちらより

前回のおさらい

Advent CalendarのDay 24、最終日です。これまでReact(Day 22)、Vue(Day 23)との比較を行ってきました。今日は、エンタープライズ向けフレームワークとして確固たる地位を築いているAngularとの比較を行います。

Angularは、TypeScriptファースト、フルスタックフレームワーク、そして強力な依存性注入(DI)システムを特徴としています。Structiveのシンプルさと、Angularの包括的なアプローチを比較することで、それぞれの設計思想の違いが明確になるでしょう。

Angularでの実装

同じショッピングカート機能をAngular(v17以降のスタンドアロンコンポーネント)で実装してみましょう。

コンポーネント実装

// shopping-cart.component.ts
import { Component, computed, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

interface Product {
  id: number;
  name: string;
  price: number;
}

interface CartItem {
  productId: number;
  quantity: number;
}

interface EnrichedCartItem extends CartItem {
  product: Product;
  price: number;
}

@Component({
  selector: 'app-shopping-cart',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './shopping-cart.component.html',
  styleUrls: ['./shopping-cart.component.css']
})
export class ShoppingCartComponent {
  private readonly TAX_RATE = 0.1;
  readonly MIN_QUANTITY = 1;

  products: Product[] = [
    { id: 1, name: "Laptop", price: 999.99 },
    { id: 2, name: "Smartphone", price: 499.99 },
    { id: 3, name: "Headphones", price: 199.99 },
    { id: 4, name: "Smartwatch", price: 149.99 },
    { id: 5, name: "Tablet", price: 299.99 },
    { id: 6, name: "Camera", price: 599.99 },
    { id: 7, name: "Printer", price: 89.99 },
    { id: 8, name: "Monitor", price: 249.99 },
    { id: 9, name: "Keyboard", price: 49.99 },
    { id: 10, name: "Mouse", price: 29.99 }
  ];

  private productById = new Map(
    this.products.map(p => [p.id, p])
  );

  // Signalsで状態管理
  cartItems = signal<CartItem[]>([
    { productId: 1, quantity: 1 },
    { productId: 5, quantity: 2 }
  ]);

  selectedProductId = signal<number>(this.products[0].id);

  // 計算されたSignal: カートアイテムに商品情報を追加
  enrichedCartItems = computed<EnrichedCartItem[]>(() => {
    return this.cartItems().map(item => {
      const product = this.productById.get(item.productId)!;
      return {
        ...item,
        product,
        price: product.price * item.quantity
      };
    });
  });

  // 計算されたSignal: 合計金額
  totalPrice = computed(() => {
    return this.enrichedCartItems().reduce(
      (sum, item) => sum + item.price, 
      0
    );
  });

  // 計算されたSignal: 税額
  tax = computed(() => this.totalPrice() * this.TAX_RATE);

  // 計算されたSignal: 総合計
  grandTotal = computed(() => this.totalPrice() + this.tax());

  // 税率(パーセント表示用)
  taxRatePercent = this.TAX_RATE * 100;

  // カートに追加
  handleAddToCart(): void {
    const currentItems = this.cartItems();
    const existingIndex = currentItems.findIndex(
      item => item.productId === this.selectedProductId()
    );

    if (existingIndex >= 0) {
      // 既存アイテムの数量を増やす
      const updatedItems = [...currentItems];
      updatedItems[existingIndex] = {
        ...updatedItems[existingIndex],
        quantity: updatedItems[existingIndex].quantity + 1
      };
      this.cartItems.set(updatedItems);
    } else {
      // 新しいアイテムを追加
      this.cartItems.set([
        ...currentItems,
        { productId: this.selectedProductId(), quantity: 1 }
      ]);
    }
  }

  // 数量変更
  handleQuantityChange(index: number, newQuantity: number): void {
    const currentItems = this.cartItems();
    const updatedItems = [...currentItems];
    updatedItems[index] = {
      ...updatedItems[index],
      quantity: newQuantity
    };
    this.cartItems.set(updatedItems);
  }

  // カートから削除
  handleDelete(index: number): void {
    if (!window.confirm("Are you sure you want to delete this item?")) {
      return;
    }
    const currentItems = this.cartItems();
    this.cartItems.set(currentItems.filter((_, i) => i !== index));
  }

  // 選択された商品IDの変更を追跡
  onProductSelectionChange(event: Event): void {
    const select = event.target as HTMLSelectElement;
    this.selectedProductId.set(Number(select.value));
  }
}

テンプレート

<!-- shopping-cart.component.html -->
<section>
  <label>
    Select Product:
    <select 
      [value]="selectedProductId()" 
      (change)="onProductSelectionChange($event)"
    >
      <option 
        *ngFor="let product of products" 
        [value]="product.id"
      >
        {{ product.name }} - ${{ product.price.toFixed(2) }}
      </option>
    </select>
  </label>
  <button type="button" (click)="handleAddToCart()">Add to Cart</button>
</section>

<section>
  <h2 *ngIf="cartItems().length > 0; else emptyCart">Your Shopping Cart</h2>
  <ng-template #emptyCart>
    <h2>Your cart is empty.</h2>
  </ng-template>

  <table>
    <colgroup>
      <col style="width: 5%;">
      <col style="width: 35%;">
      <col style="width: 15%;">
      <col style="width: 15%;">
      <col style="width: 20%;">
      <col style="width: 10%;">
    </colgroup>
    <thead>
      <tr>
        <th>No.</th>
        <th>Product</th>
        <th>Unit Price($)</th>
        <th>Quantity</th>
        <th>Price($)</th>
        <th>Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let item of enrichedCartItems(); let i = index; trackBy: trackByProductId">
        <td>{{ i + 1 }}</td>
        <td class="left">{{ item.product.name }}</td>
        <td class="right">${{ item.product.price.toFixed(2) }}</td>
        <td>
          <input 
            type="number" 
            [min]="MIN_QUANTITY"
            [value]="item.quantity"
            (input)="handleQuantityChange(i, +($any($event.target).value))"
          />
        </td>
        <td class="right">${{ item.price.toFixed(2) }}</td>
        <td>
          <button type="button" (click)="handleDelete(i)">Delete</button>
        </td>
      </tr>
      <tr>
        <td colspan="4" class="right"><strong>Total ($):</strong></td>
        <td class="right">{{ totalPrice().toFixed(2) }}</td>
        <td></td>
      </tr>
      <tr>
        <td colspan="4" class="right">
          <strong>Tax ({{ taxRatePercent.toFixed(0) }}%) ($):</strong>
        </td>
        <td class="right">{{ tax().toFixed(2) }}</td>
        <td></td>
      </tr>
      <tr>
        <td colspan="4" class="right"><strong>Grand Total ($):</strong></td>
        <td class="right">{{ grandTotal().toFixed(2) }}</td>
        <td></td>
      </tr>
    </tbody>
  </table>
</section>

スタイル

/* shopping-cart.component.css */
section {
  margin-bottom: 2em;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1em;
}

table, th, td {
  border: 1px solid #ccc;
}

td {
  padding: 0.5em;
  text-align: center;
}

.left {
  text-align: left;
}

.right {
  text-align: right;
}

input[type=number] {
  width: 4em;
  text-align: right;
}

StructiveとAngularの比較

1. 状態管理システム

Structive:

  • Proxyベースのリアクティビティ
  • クラスプロパティとgetterで状態を定義
  • 構造パスで統一的にアクセス
export default class {
  cart = { items: [...] };
  
  get "cart.totalPrice"() {
    return this.$getAll("cart.items.*.price", [])
      .reduce((sum, value) => sum + value, 0);
  }
}

Angular:

  • Signals(Angular 16+)またはRxJS Observable
  • signal()で状態を作成、computed()で派生状態
  • 関数呼び出しでアクセス(cartItems()
cartItems = signal<CartItem[]>([...]);

totalPrice = computed(() => {
  return this.enrichedCartItems().reduce(
    (sum, item) => sum + item.price, 
    0
  );
});

類似点:

  • どちらも自動的な依存関係追跡
  • 派生状態の自動更新
  • 細かい粒度での更新

相違点:

  • Angularは明示的なsignal()computed()
  • Structiveは構造パスという文字列ベース
  • Angularは型安全性が高い(TypeScript)

2. テンプレート構文

Structive:

  • カスタムテンプレート構文
  • {{ }}{{ for: }}{{ if: }}
  • data-bindで双方向バインディング
{{ for:cart.items }}
  <td>{{ cart.items.*.product.name }}</td>
  <input data-bind="valueAsNumber: cart.items.*.quantity">
{{ endfor: }}

Angular:

  • 構造ディレクティブ(*ngFor, *ngIf
  • プロパティバインディング([property])、イベントバインディング((event)
  • 双方向バインディング([(ngModel)]
<tr *ngFor="let item of enrichedCartItems(); let i = index">
  <td>{{ item.product.name }}</td>
  <input 
    type="number"
    [value]="item.quantity"
    (input)="handleQuantityChange(i, +($any($event.target).value))"
  />
</tr>

類似点:

  • どちらも宣言的なテンプレート
  • 条件分岐とループのサポート

相違点:

  • Angularは標準HTML属性として構造ディレクティブを記述
  • Angularはプロパティとイベントを明確に分離([]()
  • Structiveは*でループ要素にアクセス、Angularはlet itemで変数宣言

3. 型安全性

Structive:

  • JavaScriptベース(TypeScript対応は可能だが、構造パスは文字列)
  • 実行時のエラー検出
  • 構造パスの誤りは実行時に判明
// タイポがあっても、実行時まで気づかない
this["cart.itmes.*.quantity"] = 5; // itmes → items

Angular:

  • TypeScriptファースト
  • コンパイル時の型チェック
  • 厳格な型定義によるエラーの早期発見
// コンパイル時にエラーを検出
interface CartItem {
  productId: number;
  quantity: number;
}

cartItems = signal<CartItem[]>([...]);
// 型に合わないデータを入れるとコンパイルエラー

4. 依存性注入(DI)

Structive:

  • DI機能なし
  • シンプルなクラスインスタンス
  • サービスは通常のクラスとしてインポート
export default class {
  // サービスは直接インポートして使用
  productService = new ProductService();
}

Angular:

  • 強力なDIシステム
  • @Injectable()デコレーターでサービスを定義
  • コンストラクタインジェクション
@Injectable({ providedIn: 'root' })
export class ProductService {
  // サービスロジック
}

@Component({...})
export class ShoppingCartComponent {
  constructor(private productService: ProductService) {
    // DIコンテナから自動的に注入される
  }
}

相違点:

  • Angularは大規模アプリ向けのDIシステム
  • テスタビリティの向上(モックの注入が容易)
  • Structiveはシンプルだが、大規模化時は工夫が必要

5. イベントハンドリング

Structive:

  • data-bindで構造パスを指定
  • ループコンテキスト($1)自動取得
  • メソッド内で構造パスを使って操作
onDeleteItemFromCart(event, $1) {
  this["cart.items"] = this["cart.items"].toSpliced($1, 1);
}
<button data-bind="onclick: onDeleteItemFromCart">Delete</button>

Angular:

  • (event)構文でイベントバインディング
  • 明示的にパラメータを渡す
  • $eventで元のイベントオブジェクトにアクセス
handleDelete(index: number): void {
  const currentItems = this.cartItems();
  this.cartItems.set(currentItems.filter((_, i) => i !== index));
}
<button type="button" (click)="handleDelete(i)">Delete</button>

6. 双方向バインディング

Structive:

  • 自動判定で双方向バインディング
  • data-bindだけで完結
<input data-bind="valueAsNumber: cart.items.*.quantity">

Angular:

  • [(ngModel)]で双方向バインディング(FormsModuleが必要)
  • または[property](event)の組み合わせ
<!-- 双方向バインディング -->
<input [(ngModel)]="item.quantity">

<!-- または明示的に -->
<input 
  [value]="item.quantity"
  (input)="handleQuantityChange(i, +($any($event.target).value))"
/>

コード量と複雑度の比較

項目 Structive Angular
総コード行数 約110行 約180行
状態定義 クラスプロパティ signal/computed
型定義 不要(JSの場合) 必須(interface)
インポート 最小限 モジュール、decorator等
ボイラープレート 少ない 多い(decorator、型定義)
学習曲線 構造パスの理解 Angular全体のエコシステム

Angularは約180行とより多くのコードが必要ですが、これは型安全性明示性のトレードオフです。

状態更新の比較

カートへの商品追加:

// Structive
onAddProductToCart() {
  if (existingIndex >= 0) {
    this[`cart.items.${existingIndex}.quantity`] += 1;
  } else {
    this["cart.items"] = this["cart.items"].concat(newCartItem);
  }
}

// Angular
handleAddToCart(): void {
  const currentItems = this.cartItems();
  const existingIndex = currentItems.findIndex(
    item => item.productId === this.selectedProductId()
  );
  
  if (existingIndex >= 0) {
    const updatedItems = [...currentItems];
    updatedItems[existingIndex] = {
      ...updatedItems[existingIndex],
      quantity: updatedItems[existingIndex].quantity + 1
    };
    this.cartItems.set(updatedItems);
  } else {
    this.cartItems.set([
      ...currentItems,
      { productId: this.selectedProductId(), quantity: 1 }
    ]);
  }
}

Angularはイミュータブルな更新とSignalの.set()メソッドで明示的です。

パフォーマンスの比較

Structive:

  • 構造パス単位でピンポイント更新
  • 仮想DOMなし
  • 最小限の再計算

Angular:

  • Signals(v16+)による細かい粒度の変更検知
  • OnPush変更検知戦略で最適化
  • Zone.jsによる自動変更検知(オプション)
  • コンパイル時最適化(AOT)

どちらも高いパフォーマンスを発揮しますが:

  • Structiveは「シンプルさ」で効率化
  • Angularは「高度な最適化技術」で効率化

開発体験の比較

Structive

長所:

  • 学習コストが低い(構造パスという一つの概念)
  • ボイラープレートが少ない
  • UIと状態の一貫性(同じ構造パス)
  • 小〜中規模プロジェクトで素早く開発

短所:

  • 型安全性が低い(文字列ベース)
  • 大規模プロジェクト向けの機能が不足(DI、モジュールシステム等)
  • エコシステムが未成熟
  • エンタープライズ向けの実績がない

Angular

長所:

  • TypeScriptによる高い型安全性
  • 強力なDIシステム
  • 包括的なフレームワーク(ルーティング、HTTP、Forms等すべて揃っている)
  • 大規模チーム開発向けの設計
  • エンタープライズでの豊富な採用実績
  • Angular CLI による強力な開発支援
  • 明確なベストプラクティスとスタイルガイド

短所:

  • 学習曲線が急(RxJS、DI、デコレーター等)
  • ボイラープレートが多い
  • 小規模プロジェクトにはオーバースペック
  • バージョンアップによる破壊的変更の歴史

エコシステムとツールの比較

Structive

  • 現状: コア機能に集中
  • ツール: 基本的なローダー
  • コミュニティ: 成長段階
  • ドキュメント: 概念説明が中心

Angular

  • 成熟度: 10年以上の開発とGoogle のバックアップ
  • 公式ツール:
    • Angular CLI(プロジェクト生成、ビルド、テスト)
    • Angular Material(UIコンポーネントライブラリ)
    • Angular CDK(コンポーネント開発キット)
  • エコシステム:
    • Nx(モノレポ管理)
    • NgRx(状態管理)
    • Universal(SSR)
  • 企業採用: Google、Microsoft、Forbes等多数

設計思想の違い

Structive: シンプリシティ

  • 哲学: 「UIと状態は同じデータの異なる表出」
  • 焦点: 構造パスという単一概念での統一
  • 対象: 小〜中規模の高速開発
  • 学習: 数時間〜数日で習得可能

Angular: エンタープライズファースト

  • 哲学: 「包括的で意見の強いフレームワーク」
  • 焦点: 大規模チーム開発の標準化
  • 対象: エンタープライズアプリケーション
  • 学習: 数週間〜数ヶ月の学習期間

3つのフレームワークとの比較まとめ

Day 22〜24の比較を総括すると:

項目 構造パス React Vue Angular
記述スタイル 構造パス中心 JSX/関数型 テンプレート テンプレート/TS
状態管理 Proxyとgetter Hooks ref/computed Signals
型安全性
学習曲線 緩やか 中程度 緩やか
ボイラープレート 最小
エコシステム 未成熟 非常に豊富 豊富 包括的
企業採用 なし 非常に多い 多い エンタープライズ
適用規模 小〜中 小〜大 小〜大 中〜大

Structiveの位置づけ:

  • シンプルさ: 4つの中で最もシンプル
  • 一貫性: 構造パスという統一概念
  • 効率性: 直接DOM操作によるパフォーマンス
  • ニッチ: 小規模プロジェクトや学習用途

実際のプロジェクト選択の指針

Structiveを選ぶ場合

  • プロジェクト規模: 小〜中規模
  • 開発速度: 高速プロトタイピング
  • チーム: 小規模(1〜5人)
  • 要件: シンプルさとパフォーマンス重視

Reactを選ぶ場合

  • プロジェクト規模: 小〜大規模
  • エコシステム: 豊富なライブラリが必要
  • チーム: 柔軟な開発スタイル
  • 要件: 高い柔軟性とコミュニティサポート

Vueを選ぶ場合

  • プロジェクト規模: 小〜大規模
  • 学習: 初心者にも優しい
  • チーム: 段階的な導入が必要
  • 要件: バランスの取れた開発体験

Angularを選ぶ場合

  • プロジェクト規模: 中〜大規模
  • 要件: 型安全性とテスタビリティ
  • チーム: 大規模チーム(10人以上)
  • 環境: エンタープライズ、長期保守

まとめ

3日間にわたって、Structiveを主要な3つのフレームワーク(React、Vue、Angular)と比較してきました。

Structiveの本質:

  1. 構造パス: UIと状態を結ぶ単一の統一概念
  2. シンプリシティ: ボイラープレートの最小化
  3. 一貫性: 同じ構造パスでUIと状態を共有
  4. 効率性: ピンポイント更新による高パフォーマンス

各フレームワークから学べること:

  • React: コンポーネント設計とエコシステムの重要性
  • Vue: 直感性とプログレッシブな導入の価値
  • Angular: 型安全性と大規模開発の標準化

Structiveは、既存のフレームワークを置き換えることを目指すものではありません。むしろ、**「UIと状態を結ぶより直接的な方法はないか?」**という問いかけから生まれた、別のアプローチです。

小規模なプロジェクトや、学習目的、あるいはパフォーマンスが重視される特定のユースケースでは、この新しいアプローチが魅力的な選択肢となるでしょう。

一方、大規模なエンタープライズアプリケーションや、豊富なエコシステムが必要なプロジェクトでは、React、Vue、Angularの成熟度と実績が大きなアドバンテージとなります。

明日はいよいよ最後です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?