MVCパターン再考

## 背景

Java, Groovy, C++, C#などいろいろな言語を使ってアプリケーションを開発するなかで、MVCパターンを常に意識して開発してきた。

自分的な解釈だと、以下のような大雑把の分類である。

- **Model**
    - ビジネスロジック、データベース処理、データオブジェクトなど
- **View**
    - 画面出力
- **Controller**
    - ViewとModelの仲介役、ユーザーの入力をModelに渡すなど

しかし、あるセミナーで「ネイティブアプリでは、MVCは合わない」という話を聞き、改めて、MVCがどこまで適用できるものなのか、そもそもMVCってなんだろう？と再考したくなった。というわけで、MVCパターンを再考してみる。

## MVCと一言で言っても

改めて調べて分かったのだが、一口にMVCといっても、以下の分類であることがわかった。

- MVC1（Smalltalk MVC）
- MVC2
    - Pull-MVC
    - Push-MVC

## MVC1（Smalltalk MVCとも呼ぶ）

- 1979年 SmalltalkのGUI設計で用いられた概念
    - **デザインパターンのObserverパターンが具体例として有名**


```mermaid
graph TD
    Controller((Controller))
    Model((Model))
    View((View))
    
    View -->|1.入力を通知| Controller
    Controller -->|2.処理を命令する| Model
    Model -->|3.変更されたことを通知| View
    View -->|4.表示する情報を取得| Model
```

### MVC1の実装例(JavaScript)

以下のコード例は、MVC1の特徴である Observer パターンを使用したModel-View間の直接的な通知の仕組みを示している。

1. Modelに対して変更があったときに情報を受け取りたいViewを登録する
1. 変更されたら登録されたすべてのViewに対して通知を行う


```mermaid
sequenceDiagram
    participant V as TodoView
    participant U as Webブラウザ
    participant C as TodoController
    participant M as TodoModel

    V->>M: addObserver(this)<br/>初期化時の処理 - Viewの登録
    U->>V: クリックイベント - 「タスク追加」ボタンがクリックされる
    V->>C: addTask()<br/>タスク追加処理
    
    Note over C: コントローラーの処理
    C->>C: document.getElementById('new-task')<br/>入力値の取得
    C->>M: addTask(value)<br/>タスク追加指示
    
    Note over M: モデルの更新処理
    M->>M: tasks.push(task)<br/>タスク配列に追加
    M->>M: notifyObservers()<br/>オブザーバーへ通知
    
    Note over V: 画面更新処理
    M-->>V: update(tasks)<br/>タスク一覧更新
    V->>V: innerHTML更新<br/>DOMの更新
    V-->>U: 更新後の画面表示

```


```javascript
// MVC1の実装例

// Model
class TodoModel {
    constructor() {
        this.tasks = []
        this.observers = []  // Observerパターンの実装
    }

    // オブザーバーの登録
    addObserver(observer) {
        this.observers.push(observer)
    }

    // タスクの追加とオブザーバーへの通知
    addTask(task) {
        this.tasks.push(task)
        this.notifyObservers()  // 変更を通知
    }

    // オブザーバーへの通知
    notifyObservers() {
        this.observers.forEach(observer => observer.update(this.tasks))
    }
}

// View
class TodoView {
    constructor(model) {
        this.model = model
        this.model.addObserver(this)  // ViewをModelに登録
    }

    // Modelからの更新通知を受け取るメソッド
    update(tasks) {
        // DOMの更新
        const todoList = document.getElementById('todo-list')
        todoList.innerHTML = tasks.map(task => `
            <li>${task}</li>
        `).join('')
    }

    // HTML作成
    render() {
        return `
            <div>
                <input type="text" id="new-task">
                <button onclick="controller.addTask()">タスク追加</button>
                <ul id="todo-list"></ul>
            </div>
        `
    }
}

// Controller
class TodoController {
    constructor(model) {
        this.model = model
    }

    addTask() {
        const input = document.getElementById('new-task')
        this.model.addTask(input.value)
        input.value = ''
    }
}
```

## MVC2

- 1998年 JSP仕様のドラフトで提唱された
    - MVC1をWebサービスに適応させたものである
- サーバー側からいきなりModelの状態変更が通知されることはHTTPがステートレスなのでできない
- Modelの状態変更は、Controllerを経由してViewへの通知するように修正された
- <span style="color: #ff0000"><b>WebアプリにおいてのMVCとはMVC2のことを指す</b></span>

```mermaid
graph TD
    Browser[WebBrowser]
    Controller[Controller]
    Model[Model]
    View[View]
    Browser -->|1.クライアントからのリクエスト（URL）| Controller
    Controller -->|2.リクエストを通知| Model
    Model -->|3.結果を返す| Controller
    Controller -->|4.結果を反映させる| View
    View -->|5.レスポンス| Browser
    Model -.->|関係性を持たない| View
```

### 基本的な処理の流れ
1. クライアントからのリクエスト(URL)
2. リクエストを通知
3. 結果を返す
4. 結果を反映させる
5. レスポンス

## Push-MVC(MVC2)

- ほとんどのMVCフレームワークは、Push-MVCのアーキテクチャにしたがっている
- このようなフレームワークは、処理を要求するアクションを実行し、次に結果を出力するためにデータを表示のレイヤにプッシュする
- 代表的なフレームワーク：Struts、Django、Ruby on Rails、Spring MVCなど

### Push-MVCの実装例(Rails)

- 以下、Railsによる実装例
- Push-MVCでは、Controllerが主導的にModelのデータをViewに渡す

```mermaid
sequenceDiagram
    participant User as Webブラウザ
    participant C as TasksController
    participant M as Task Model
    participant V as tasks/index.html.erb
    
    User->>C: GET /tasks
    
    rect rgb(240, 240, 255)
        Note right of C: Controllerがデータを取得し<br/>Viewにデータをプッシュ
        C->>M: Task.all
        M-->>C: tasks collection
        C->>C: @tasks = tasks
    end
    
    C->>V: render 'index' with @tasks
    V-->>User: Rendered HTML
    
    Note right of V: Viewは渡された<br/>@tasksのみを使用
```


```ruby
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    # Controllerが能動的にModelからデータを取得し、Viewに渡す
    @tasks = Task.all
    
    # View(HTML)にデータをPushして描画
    render 'index'
  end
end
```

```ruby
# app/models/task.rb
class Task < ApplicationRecord
  # ビジネスロジックをカプセル化
  def self.completed
    where(status: 'completed')
  end
end
```

```erb
<!-- app/views/tasks/index.html.erb -->
<!-- Controllerから渡された（Pushされた）データを表示 -->
<h1>タスク一覧</h1>
<ul>
  <% @tasks.each do |task| %>
    <li><%= task.title %></li>
  <% end %>
</ul>
```

## Pull-MVC(MVC2)

- Push-MVCに対するアーキテクチャで、「コンポーネント型」とも呼ばれている
- 「コンポーネント型」は表示レイヤから処理を開始し、必要に応じて複数のコントローラからの処理の結果を「プル」する
- 「コンポーネント型」では、複数のControllerが一つのViewに関連付けられる
- 代表的なフレームワーク：Tapestry、Velocityなどがプル型アーキテクチャの例である

### Pull-MVC(MVC2)の実装例(Rails)

- 以下、Railsによる実装例
- Pull-MVCでは、Viewが必要に応じてModelからデータを取得する

```mermaid
sequenceDiagram
    
    participant B as Webブラウザ
    
    participant C as DashboardController
    participant V as dashboard/show.html.erb
    participant M as Task Model
    
    B->>C: GET /dashboard<br/>ダッシュボード表示リクエスト
    C->>V: render 'dashboard'<br/>テンプレート呼び出し
    
    Note over V: 最近のタスク取得処理
    V->>M: Task.recent<br/>created_atの降順で5件取得
    M-->>V: 最近のタスク一覧
    
    Note over V: 緊急タスク取得処理
    V->>M: Task.urgent<br/>優先度の高いタスク取得
    M-->>V: 緊急タスク一覧
    
    V->>V: タスク一覧のレンダリング
    V-->>B: HTML生成結果
```

```ruby
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  # Controllerの役割を最小限に
  def show
    render 'dashboard'
  end
end
```

```ruby
# app/models/task.rb
class Task < ApplicationRecord
  # 同じModelでも、Pull-MVCではViewから直接アクセスされる
  def self.recent
    order(created_at: :desc).limit(5)
  end
  
  def self.urgent
    where(priority: 'high')
  end
end
```

```erb
<!-- app/views/dashboard/show.html.erb -->
<!-- Viewが必要なデータを必要なタイミングでPullする -->
<h1>ダッシュボード</h1>

<div class="recent-tasks">
  <h2>最近のタスク</h2>
  <ul>
    <% Task.recent.each do |task| %>
      <li><%= task.title %></li>
    <% end %>
  </ul>
</div>

<div class="urgent-tasks">
  <h2>緊急タスク</h2>
  <ul>
    <% Task.urgent.each do |task| %>
      <li><%= task.title %></li>
    <% end %>
  </ul>
</div>
```

## Push-MVCとPull-MVCの主な違い

Push-MVCとPull-MVCの主な違いは以下の3点である。

1. 制御の流れ
   - Push-MVC: Controller → Model → View
   - Pull-MVC: View → Model（Controllerの役割が小さい）
2. データの受け渡し
   - Push-MVC: Controller経由で明示的にViewにデータを渡す（`@tasks`）
   - Pull-MVC: ViewがModelから直接必要なデータを取得する
3. 使い分けの指針
   - Push-MVC: 単一の画面で完結する場合に適している
   - Pull-MVC: 1つの画面で複数のデータを表示する場合に適している


## MVCのメリット

1. ViewとModelでの並行開発が可能となる
   - ViewとModelが分離されており、それぞれの変更の影響を受けないため
   - フロントエンドエンジニアとバックエンドエンジニアの分業が容易になる
2. Modelを機能別に分けることで、テストがしやすく再利用性の高いコンポーネントを作成できる
   ```ruby
   # 機能別に分かれたModelの例
   class Task < ApplicationRecord
     # ビジネスロジックをModelに集約
     belongs_to :user
     has_many :comments
     
     def self.due_today
       where(due_date: Date.today)
     end
     
     def overdue?
       due_date < Date.today
     end
     
     def notify_assignee
       # 担当者への通知ロジック
     end
   end
   ```
3. ViewとModelの相互参照を避けることができるのでコードの見通しが良くなり、影響範囲も限定しやすい

## MVCのメリットを踏まえた上で実務で感じた課題

**View、Model、Controllerの並行開発がなかなか難しい**

以下、ECサイトの配送日指定機能を実装例として、Railsのコードで具体的に説明する

### Model

```ruby
# app/models/delivery.rb
class Delivery < ApplicationRecord
  # Modelでは配送に関するビジネスロジックを定義
  def calculate_estimated_date
    # 在庫状況、配送先、など様々な要因で配送予定日を計算
    Date.today + 3.days
  end

  def available_dates
    # 配送可能日の計算ロジック
    (Date.today..(Date.today + 7.days)).to_a
  end
end
```

### View

```erb
<!-- app/views/deliveries/edit.html.erb -->
<div class="delivery-options">
  <!-- View側でラジオボタンの状態によって表示を切り替える必要がある -->
  <div>
    <input type="radio" name="delivery_type" value="estimated" checked>
    配送日を指定しない
  </div>
  <div>
    <input type="radio" name="delivery_type" value="custom">
    配送日を指定する
  </div>

  <!-- 配送予定日の表示部分 -->
  <div id="estimated-date" class="delivery-date">
    配送予定日: <%= @delivery.calculate_estimated_date %>
  </div>

  <!-- カレンダーの表示部分 -->
  <div id="custom-date" class="delivery-date" style="display: none;">
    <select id="delivery-calendar">
      <% @delivery.available_dates.each do |date| %>
        <option value="<%= date %>"><%= date %></option>
      <% end %>
    </select>
  </div>
</div>

<!-- View側で必要な制御のJavaScript -->
<script>
document.querySelectorAll('input[name="delivery_type"]').forEach(radio => {
  radio.addEventListener('change', function() {
    // View側で表示制御が必要
    const estimatedDateDiv = document.getElementById('estimated-date');
    const customDateDiv = document.getElementById('custom-date');
    
    if (this.value === 'estimated') {
      estimatedDateDiv.style.display = 'block';
      customDateDiv.style.display = 'none';
    } else {
      estimatedDateDiv.style.display = 'none';
      customDateDiv.style.display = 'block';
    }
  });
});
</script>
```

### Controller

```ruby
# app/controllers/deliveries_controller.rb
class DeliveriesController < ApplicationController
  def edit
    @delivery = Delivery.find(params[:id])
    
    # ViewとModelの両方に依存する処理が必要
    if params[:delivery_type] == 'custom'
      # カレンダー表示用のデータ取得
      @available_dates = @delivery.available_dates
    else
      # 配送予定日表示用のデータ取得
      @estimated_date = @delivery.calculate_estimated_date
    end
  end
end
```

### 上記コードから見える課題

1. **ViewとControllerの密結合**
   - 配送日の選択状態によってViewの表示を変える必要がある
   - その制御がJavaScriptで行われるため、ViewとControllerの分離が難しい
   - UIの複雑さに応じて、ViewとControllerの結合が強くなり、それぞれが単独で交換可能ではなくなる
2. **ビジネスロジックとUIの依存**
   - 配送予定日の計算（Model）と表示方法（View）が密接に関連している
   - UIの変更がModelの変更を必要とする可能性がある
3. **状態管理の複雑さ**
   - 配送日指定の有無という状態をView、Controller、Model間で共有する必要がある
   - これにより、各層の独立性が低下する

## MVCという用語の功罪

### 功
- ｢ModelとViewを分離させる｣という常識を浸透させたこと
- 知ってしまうと当たり前のように考えてしまうが、MVCを知らない人が最初からModelとViewを分離しようと考えることは難しい
- Viewの部分(html)をデザイナーに依頼、Model部分をエンジニアが開発という分業が可能になった

### 罪

罪というか、自分の無知が大きいのですが...。

- MVC1とMVC2というのがごっちゃになってコミュニケーションされる傾向があり、モヤっとさせる場面がある
- Javaなどで扱うデザインパターンは、`MVC = MVC1`ですが、Webアプリ入門などで取り上げられるのは `MVC = MVC2`という感じで、同じMVCでも文脈によって違う意味で使われている

## MVCの捉え方によって違ってくる！

### 捉え方1 サーバーサイドのみでMVCを考える

```mermaid
graph LR
    BrowserView[（Webブラウザ）View]
    RoutingController[（Routing）Controller]
    BusinessModel[（Business Logic）Model]
    HTMLView[（HTML）View]
    
    BrowserView -->|Network| RoutingController
    RoutingController --> BusinessModel
    BusinessModel --> HTMLView
    HTMLView -.->|Network| BrowserView
```

- Model: データ重複、ロジック重複を避けるために共通化する。共通化する単位としてビジネスロジックなど
- Controller: ユーザー入力を受け付けて、それに合ったModelを起動する
    - Push型:  担当するViewにModelの情報をPushする
    - Pull型: （-）ViewがModelからデータをPullする。ControllerはViewと密着する
- View: ControllerからのデータをもとにHTMLを出力する


### 捉え方2 クライアントサイト、サーバーサイド、それぞれでMVCを考える

```mermaid
graph LR
    subgraph Client[クライアントサイド]
        BrowserView[（Webブラウザ）View]
        Controller1[Controller]
        AppModel[（Application Logic）Model]
    end
    
    subgraph Server[サーバーサイド]
        RoutingController[（Routing）Controller]
        BusinessModel[（Business Logic）Model]
        DataFormatView[（DataFormat）View]
    end
    
    BrowserView --> Controller1
    Controller1 --> AppModel
    AppModel -->|Network| RoutingController
    RoutingController --> BusinessModel
    BusinessModel -.-> RoutingController
    RoutingController --> DataFormatView
    DataFormatView -.->|Network| AppModel
    AppModel -.-> BrowserView
```

#### クライアントサイドMVC

- Model: サーバーサイドのAPIを利用しながら、そのアプリケーションが扱う領域のデータと手続きを表現する
- View: ユーザーにModelの状態を反映したHTMLを出力する
- Controller: ユーザーの入力を受け取って判断し、Modelを起動する

#### サーバーサイドMVC

- Model: ビジネスロジック、データベース処理など
- View: json形式等、APIに適したDataFormatでクライアントに返す
- Controller: クライアントのリクエストを受け付けてそれに合ったModelを起動する

#### 「配送日指定機能」のリファクタリング例

```mermaid
sequenceDiagram
    box クライアントサイド
    participant B as Webブラウザ
    participant V as DeliveryView
    participant C as DeliveryController
    participant M as DeliveryModel
    end
    
    box サーバーサイド
    participant A as DeliveriesController
    participant D as Delivery Model
    end

    B->>V: DOMContentLoaded
    V->>C: initializeDelivery()<br/>初期表示用メソッド
    
    Note over M: Modelがデータを<br/>取得・保持する
    C->>M: fetchDeliveryData()<br/>配送データ取得
    
    Note over M,D: APIを介した通信
    M->>A: GET /api/deliveries/estimated_date
    A->>D: calculate_estimated_date()<br/>配送予定日の計算
    D-->>A: 計算結果
    A-->>M: JSON レスポンス
    
    Note over M,V: ModelがViewに変更を通知
    M-->>V: render()<br/>画面の更新
    V-->>B: DOM更新
    
    Note over B,V: ラジオボタン選択時
    B->>V: change イベント
    V->>C: updateDeliveryType(type)<br/>配送タイプ更新
    C->>M: deliveryType = type
    
    Note over M,D: 選択に応じたAPI呼び出し
    M->>A: GET /api/deliveries/available_dates
    A->>D: available_dates()<br/>配送可能日の取得
    D-->>A: 利用可能日一覧
    A-->>M: JSON レスポンス
    
    M-->>V: render()<br/>画面の更新
    V-->>B: DOM更新
```

##### 配送日指定画面のHTML(index.html)

```html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>配送日指定</title>
</head>
<body>
    <!-- Viewのコンテナ要素 -->
    <div id="delivery-container"></div>

    <!-- JavaScriptファイルの読み込み -->
    <script src="delivery.js"></script>
</body>
</html>
```

##### クライアントサイドMVC(delivery.js)

```javascript
// delivery.js

// Model: クライアントサイドのデータとロジックを管理
class DeliveryModel {
    constructor() {
        this.deliveryType = 'estimated';
        this.estimatedDate = null;
        this.availableDates = [];
    }

    // APIと通信してデータを取得
    async fetchDeliveryData() {
        if (this.deliveryType === 'estimated') {
            const response = await fetch('/api/deliveries/estimated_date');
            const data = await response.json();
            this.estimatedDate = data.estimated_date;
        } else {
            const response = await fetch('/api/deliveries/available_dates');
            const data = await response.json();
            this.availableDates = data.available_dates;
        }
    }
}

// View: 画面の表示を担当
class DeliveryView {
    constructor(model, controller) {
        this.model = model;
        this.controller = controller;
    }

    // イベントの設定
    setupEvents() {
        document.querySelectorAll('input[name="delivery_type"]').forEach(radio => {
            radio.addEventListener('change', (e) => {
                this.controller.updateDeliveryType(e.target.value);
            });
        });
    }

    // 画面の更新
    render() {
        const container = document.querySelector('#delivery-container');
        container.innerHTML = `
            <div class="delivery-options">
                <div>
                    <input type="radio" name="delivery_type" value="estimated" 
                        ${this.model.deliveryType === 'estimated' ? 'checked' : ''}>
                    配送日を指定しない
                </div>
                <div>
                    <input type="radio" name="delivery_type" value="custom"
                        ${this.model.deliveryType === 'custom' ? 'checked' : ''}>
                    配送日を指定する
                </div>
                
                ${this.model.deliveryType === 'estimated' ? 
                    `<div>配送予定日: ${this.model.estimatedDate}</div>` :
                    `<div>
                        <select>
                            ${this.model.availableDates.map(date => 
                                `<option value="${date}">${date}</option>`
                            ).join('')}
                        </select>
                    </div>`
                }
            </div>
        `;
        this.setupEvents();
    }
}

// Controller: ModelとViewの橋渡し
class DeliveryController {
    constructor(model, view) {
        this.model = model;
        this.view = view;
    }

    // 初期表示用のメソッド
    async initializeDelivery() {
        // 初期データの取得
        await this.model.fetchDeliveryData();
        // 初期表示
        this.view.render();
    }

    async updateDeliveryType(type) {
        this.model.deliveryType = type;
        await this.model.fetchDeliveryData();
        this.view.render();
    }
}

// 初期化と実行
document.addEventListener('DOMContentLoaded', () => {
    // MVCの各インスタンスを作成
    const model = new DeliveryModel();
    const view = new DeliveryView(model);
    const controller = new DeliveryController(model, view);
    
    // ViewにControllerを設定
    view.controller = controller;
    
    // 初期表示
    controller.initializeDelivery();
});
```

##### サーバーサイドMVC(Rails)

```ruby
# app/controllers/api/deliveries_controller.rb
class Api::DeliveriesController < ApplicationController
    def estimated_date
        delivery = Delivery.find(params[:id])
        render json: {
            estimated_date: delivery.calculate_estimated_date
        }
    end

    def available_dates
        delivery = Delivery.find(params[:id])
        render json: {
            available_dates: delivery.available_dates
        }
    end
end

# app/models/delivery.rb
class Delivery < ApplicationRecord
    def calculate_estimated_date
        # 配送予定日の計算ロジック
        Date.today + 3.days
    end

    def available_dates
        # 配送可能日の計算ロジック
        (Date.today..(Date.today + 7.days)).to_a
    end
end
```

### 捉え方1と捉え方2の比較

#### 捉え方1（従来のアプローチ）の課題

1. ViewとControllerの結合が強い
2. ビジネスロジックとUIロジックの混在
3. 状態管理が複雑
4. テストの難しさ

#### 捉え方2（分離アプローチ）のメリット

1. **責任の明確な分離**
   - クライアントサイド: UIに関する責任
   - サーバーサイド: ビジネスロジックに関する責任
2. **開発の並行性向上**
   - フロントエンドチームとバックエンドチームが独立して開発可能
   - APIインターフェースさえ決めれば、実装の詳細は各チームに委ねられる
3. **テスタビリティの向上**
   - クライアントサイド: UIのテストに集中
   - サーバーサイド: ビジネスロジックのテストに集中
4. **スケーラビリティの向上**
   - APIを介した疎結合により、各層の独立したスケーリングが可能
   - 将来的なマイクロサービス化への対応が容易
5. **保守性の向上**
   - UIの変更がバックエンドに影響を与えにくい
   - ビジネスロジックの変更がUIに影響を与えにくい

## まとめ

- <span style="color: #ff0000"><b>MVCにはMVC1とMVC2がある</b></span>
- MVC1では、Modelの変化を引き金として、Viewにその変化を通知する。
- MVC2では、サーバー側からModelの状態変更を通知することはHTTPがステートレスなのでできない。したがって、Modelの状態変更は、Controllerを経由してViewへの通知するように修正された
- <span style="color: #ff0000"> <b>MVCといったら、WebアプリではMVC2を指す</b></span>
- **サーバーサイドとクライアントと分割してMVCを考えた方がしっくりくる**
    - 調査以前は、Viewはブラウザ、ModelとControllerはサーバーとして、サーバーサイドとクライアントサイドを統合して考えていたが、分割して考えた方がMVCをすっきりと受け入れられる気がした
- <span style="color: #ff0000"><b>Viewはブラウザと捉えずにHTML, json, xmlなど最終的にクライアントに返すフォーマットという広い捉え方にしよう</b></span>
    - そういう捉え方をすることで、API+ネイティブアプリという構成であっても、MVCで捉えることもできる
    - 「APIサーバーはMVCパターンだけど、ネイディブ側はDocument-Viewパターンだよ」とか
## 参考サイト

- http://ynomura.dip.jp/archives/2012/09/mvc1.html
- http://qiita.com/tshinsay/items/5b1724baf32b8b5113c2
- http://cartman0.hatenablog.com/entry/2015/12/17/045635
- http://javazuki.com/articles/mvc-tutorial.html
- http://forza.cocolog-nifty.com/blog/2014/07/mvc2mvc-ffd9.html

## [宣伝]Udemyの自作教材

- 2021年7月に[wywy合同会社](https://wywy.jp/)という会社を起業しました
- Qiita記事をキッカケに知名度を少しでも上げたいので、以下自作のUdemyを宣伝させていただければです

**■ Udemy**
- GoogleアカウントとWebブラウザがあれば開発準備OK！これからプログラミングを始める方も取り組みやすいように「40のスキル」を選定。受講後、Googleアプリ連携による業務効率化ができるようになります
- `2022年6月時点で、約1500人の受講生を獲得し、UdemyBusinessに選定されてます。`

https://www.udemy.com/course/wywy-google-apps-script-forty-skills/?referralCode=7F71969F9DD9849220B4