はじめに
初めまして。私は新卒2年目のエンジニアで、これまでに2つのプロジェクトに携わりました。1つ目のプロジェクトはMVCアーキテクチャを採用し、2つ目はドメイン駆動設計(DDD)とレイヤードアーキテクチャに基づいて開発を行いました。
この記事では、これら2つのアーキテクチャの比較を、私自身の経験を基に説明します。まだ経験が浅い部分もありますが、意見やフィードバックをいただけると嬉しいです。
それぞれのメリット、デメリットについて
| 学習コスト | 開発スピード | 責務の分離 | |
|---|---|---|---|
| MVC | 比較的低い | 小〜中規模だと早い | チームで決めておかないと不明瞭になる | 
| レイヤードアーキテクチャ | 高い | コードが増え遅くなる | 明確 | 
私が最近感じているのは責務の分離についてです
責務の分離について
責務の分離とは、システムの各部分が「何を担当するか」を明確にし、それぞれが持つ責務(役割)を厳密に分けることです。レイヤードアーキテクチャでは、各層が特定の役割を持つため、コードの可読性や保守性が向上します。
例えば、私が担当したプロジェクトでは、次のように責務を分離していました。
- interface層: 外部との連携を担当し、リクエストやレスポンスの変換を行う
- application層: ビジネスロジックの流れ(ユースケース)を制御する
- domain層: ドメイン固有のロジックやエンティティを扱う
- infra層: データベースへのアクセスや永続化を行う
このように、責務が明確になることで、各層が独立して機能し、コードの変更が他の部分に与える影響を最小限に抑えられます。一方、MVCアーキテクチャでは、モデル、ビュー、コントローラーにドメインロジックが散らばりやすく、特に大規模プロジェクトになると保守が困難になります。
以下に例を挙げます
これは注文(order)モデルにデータ取得とドメインロジックが混在している例です
class Order < ApplicationRecord
  belongs_to :customer
  has_many :order_items
  # 注文IDで注文を取得するリポジトリ的なロジック
  def self.find_by_order_id(order_id)
    find_by(id: order_id)
  end
  # 顧客IDで注文を取得するリポジトリ的なロジック
  def self.find_all_by_customer(customer_id)
    where(customer_id: customer_id)
  end
  # 注文の合計金額を計算するビジネスロジック
  def total_price
    order_items.sum { |item| item.quantity * item.product.price }
  end
  # 支払い処理(ビジネスロジック)
  def process_payment
    total = total_price
    payment_gateway = PaymentGateway.new(customer.payment_info)
    unless payment_gateway.charge(total)
      errors.add(:base, "Payment failed")
      return false
    end
    true
  end
  # 注文処理のための複合ビジネスロジック
  def place_order
    if process_payment
      order_items.each do |item|
        item.product.update(stock: item.product.stock - item.quantity)
      end
      update(status: 'completed')
    else
      errors.add(:base, "Order could not be placed")
    end
  end
こんな感じでデータの永続化やドメインロジックが混在してしまいわかりにくくなっています
前いたプロジェクトはMVCを使っていて、ドメインロジックがmodel,view,controller全てに対して散らばっていました
そうなってくると保守性を下がり、大規模になるにつれ生産性も落ちていきます
シグネチャについて
責務の分離をする際に重要だと感じたものの一つにシグネチャがあります
シグネチャとは、関数やメソッドがどのような引数を受け取り、どのような値を返すかを定義したものです。これが明確であれば、他の開発者がコードを読む際に、関数の動作や目的をすぐに理解でき、変更による影響範囲も最小限に抑えられます。
特にGoのインターフェースでは、関数のシグネチャを定義することで、そのインターフェースを実装する全ての構造体が、同じシグネチャのメソッドを持つことを保証できます。これにより、どの実装が使われているかを意識せずに同じインターフェースを利用できる点が強力です。例えば、次のようなシグネチャを持つ OrderRepository インターフェースがあります。
type OrderRepository interface {
	// 注文を保存する
	Create(ctx context.Context, order *Order) error
	// IDで注文を取得する
	GetByID(ctx context.Context, id int) (*Order, error)
	// 顧客IDで注文リストを取得する
	GetByCustomerID(ctx context.Context, customerID int) ([]*Order, error)
	// 注文を更新する
	Update(ctx context.Context, order *Order) error
	// 注文を削除する
	Delete(ctx context.Context, id int) error
}
終わりに
エンジニアとしての経験はまだ浅いですが、これまでに感じたことをアウトプットすることで、より深く理解し、改善点を見つけたいと思っています。もしお気づきの点があれば、ぜひご指摘いただければ幸いです。
MVCで大規模になってしまった場合はどうやって対処すればいいのか知りたいなとも感じます
