フェニックスによるブレージング:プロジェクト構造


この記事ではPhoenix, a web framework で書かれているElixir programming language . あなたはこれらのツールに精通していない場合は、それも素晴らしいです!我々は、あなたが考えることを楽しむと確信しているウェブ開発に適用されるソフトウェア設計の一般的な概念について話すつもりです.

導入


ほとんどの新しいフェニックス開発者、特にRuby on Railsのような他のWebフレームワークから来ているものは、機能的な方法でビジネスロジック(MVCのM)を適切に構築する方法について、彼らの頭を包むのに少しの時間がかかるので、私は、あなたのプロジェクトを健全な方法で組織させ続ける階層的な慣例を提案するためにここにいますDomain Driven Design それはとてもエレガントなフェニックス自身によって奨励されます.

It's important to note that the conventions laid out here are focused on optimizing larger codebases, so if you have a small project, following the patterns set by the Phoenix generators is completely fine and will make you more productive. It's always best to start with simple abstractions and refactor as the project evolves.


この概念的な話の全てをバランスさせるために、いくつかのコードで手を汚しましょう.私たちは楽しい建物を少しでしょうRPG に基づいてa side-project of mine called MOBA , ここでは、より精巧な方法で適用されるパターンを参照してくださいチェックアウトすることができます.
から始まるmix phx.new rpg , 次の構造体を取得します.
├── _build
├── assets
├── config
├── deps
├── lib
│   └── rpg
│   └── rpg.ex
│   └── rpg_web
│   └── rpg_web.ex
├── priv
└── test
GET Goから、フェニックスは主要な層分離で我々のプロジェクトを生成しますrpg 私たちのビジネスロジックが生きているフォルダですrpg_web 実際には、コントローラ、ビュー、テンプレート、ルーティングなどを介してWebへのアプリケーションを公開するために必要なすべての複雑なフォルダ.

最初の反復:単一トップレベルドメイン


明示的なレイヤリングのこの中心的な考え方を念頭に置いて、単純にリストを作成し、その主なリソースを作成するゲームの最初の反復をコード化しましょう.
Rpg.create_hero(attrs)
Rpg.list_heroes()
Rpg.list_enabled_heroes_by_level(level)
このようにして我々のパブリックAPIはWeb レイヤー.さあ、ダイビングをしましょうrpg どのように我々は開発者の生産性に焦点を当てた簡単な大会で私たちのビジネスロジックを構築することができますを参照してください.データベーステーブルごとに、スキーマ、クエリ、サービスの3つのサポートモジュールを提案します.我々にそれを適用することHero リソースには、
├── lib
│   └── rpg
│       └── hero.ex (schema)
│       └── hero_query.ex (query)
│       └── heroes.ex (service)
│   └── rpg.ex
│   └── rpg_web
│   └── rpg_web.ex
スキーマのコーディングを始めましょう.
# lib/rpg/hero.ex
defmodule Rpg.Hero do
  schema "heroes" do
    field :level, :integer
    field :is_enabled, :boolean
    field :gold, :integer
  end

  def changeset(hero, attrs) do
    hero
    |> cast(attrs, [:level, :is_enabled, :gold])
  end
end
スキーマは単純で厳密なルールセットを持っています:実際のスキーマ定義と変更セット関数を持っている必要があります.これがビジネスロジックを始めるのを誘惑しているので、これはモデルのように感じます、しかし、衝動に抵抗してください、我々は第2でビジネスロジックに着きます.
問い合わせモジュールへ移動する
# lib/rpg/hero_query.ex
defmodule Rpg.HeroQuery do
  import Ecto.Query

  def filter_by_level(query, level) do
    from hero in query, where: hero.level == ˆlevel
  end

  def filter_by_enabled(query) do
    from hero in query, where: hero.is_enabled == true
  end

  def order_by_level(query) do
    from hero in query, order_by: [desc: hero.level]
  end
end
ここでは、私たちは、構成可能なecto問合せ構造を返して、受け取る機能だけを目的としなければなりません.これにより、私たちのサービスモジュールでこれらをつなぎ合わせることで、非常に明確にクエリを記述できます.
# lib/rpg/heroes.ex
defmodule Rpg.Heroes do
  alias Rpg.{Repo, Hero, HeroQuery}

  def create(attrs) do
    Hero.changeset(%Hero{}, attrs)
    |> Repo.insert()
  end

  def list do
    Hero
    |> HeroQuery.order_by_level()
    |> Repo.all()
  end

  def list_enabled_with_level(level) do
    Hero
    |> HeroQuery.filter_by_enabled()
    |> HeroQuery.filter_by_level(level)
    |> Repo.all()
  end
end
サービスモジュールは私たちのビジネスロジックの大半を担当しています.すべてのREPO操作は、リソースを排他的に操作する他のコードと同様にここに置かれるべきです.Hero .
最後に、これら三つのモジュールの全てを接着するには、トップレベルドメインがあります.
# lib/rpg.ex

defmodule Rpg do
  alias Rpg.Heroes

  def create_hero(attrs \\ %{}) do 
    Heroes.create(attrs)
  end

  def list_all_heroes do 
    Heroes.list()
  end

  def list_enabled_heroes_by_level(level \\ 1) do
    Heroes.list_enabled_by_level(level)
  end
end
トップレベルドメイン内の関数は、我々がここでやっているように、子サービスに直接配送するか、または複数の子サービス間で作業を調整する必要があります.
もし、主人公が買うことができるアイテムの別のデータベーステーブルを追加する場合、以下のように同じトップレベルドメインに関数を追加します.
# lib/rpg.ex
defmodule Rpg do
  alias Rpg.{Heroes. Items}

  def create_hero(attrs \\ %{}) do # ...
  def list_all_heroes do #...
  def list_enabled_heroes_by_level(level \\ 1) do #...

  def buy_item(item, hero) do
    Items.buy(item, hero)
  end

  def sell_item(item, hero) do
    Items.sell(item, hero)
  end
end
たとえ我々がaを通過しているとしてもhero Structの両方の項目の関数に対する引数として、概念的に我々はちょうど英雄だけを操作していないので、すべての項目に関連する関数を処理する排他的なサービスを持っているコードは、整理され、最も重要なことは、ブロックを回避するHeroes サービスとしてのサービスGod module .
上で移動して、我々がこれのすべてを使用する方法を見るためにウェブ層にジャンプしましょうRpg 関数.
# lib/rpg_web/router.ex
resources "/heroes", RpgWeb.HeroController, only: [:index, :create]

# lib/rpg_web/controllers/hero_controller.ex
defmodule RpgWeb.HeroController do
  use RpgWeb, :controller

  def index(conn, %{"level" => level}) do
    heroes = Rpg.list_enabled_heroes_by_level(level)
    render(conn, "index.html", heroes: heroes)
  end

  def index(conn, _params) do
    heroes = Rpg.list_all_heroes()
    render(conn, "index.html", heroes: heroes)
  end

  def create(conn, %{"hero" => hero_params}) do
    case Rpg.create_hero(hero_params) do
      {:ok, hero} -> 
        conn |> redirect(to: hero_path(conn, :index))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "index.html", changeset: changeset)
    end
  end
end
私たちの階層化を通じて、どのように我々はそれを取得したり、私たちのヒーローを作成して知っていないように、コントローラの詳細については、実装の詳細は漏れていた、私たちは、データベースを持っていない知っている.この全ては公開APIを通して公開されました.このAPIは、IEXのように、Web層以外の手段を介して分離しアクセスすることができます.
OK、最初の繰り返しの場合、単一のトップレベルドメインを使用しますRpg ) よかった.しかし、ユーザー登録、管理パネルまたは多分支払いシステムのように本当にゲームプレーをしなければならないより多くの機能を始めるとき、我々にはどうですか?下のすべてを投げるRpg ドメインは、我々の層の努力のすべてを取り消すので、我々は1つのレベルをより深く行かなければなりません.

第二反復:多重トップレベル領域


ほとんどの現実世界のアプリケーションでは、複数のトップレベルドメインが異なるコンテキストを表す必要があります.この場合、ユーザー登録とチャット機能を追加しましょうAccounts ドメインと我々の既存の機能のすべてを移動するGameplay ドメイン

├── lib
│   └── rpg
│       └── gameplay.ex
│       └── accounts.ex
│       └── gameplay
│           └── hero.ex
│           └── hero_query.ex
│           └── heroes.ex
│           └── item.ex
│           └── items.ex
│       └── accounts
│           └── user.ex
│           └── users.ex
│           └── message.ex
│           └── messages.ex
│   └── rpg.ex
│   └── rpg_web
│   └── rpg_web.ex
これをコードに変換すると、次のようになります.
# lib/rpg/accounts.ex
defmodule Rpg.Accounts do
  alias Rpg.Accounts.{Users, Messages}

  def create_user(attrs) do
    Users.create(attrs)
  end
  def set_current_hero(user, hero) do 
    Users.set_current_hero(user, hero)
  end
  def create_message(attrs, user) do 
    Messages.create(attrs, user)
  end
end

# lib/rpg/gameplay.ex 
defmodule Rpg.Gameplay do
  alias Rpg.Gameplay.{Heroes. Items}
  alias Rpg.Accounts

  def create_hero(attrs) do 
    attrs
    |> Heroes.create()
    |> Items.equip_initial()    
  end

  def list_heroes, do: Heroes.list()
  def list_enabled_heroes_by_level(level), do: #...
  def buy_item(hero, item), do: Items.buy(hero, item)
  def sell_item(hero, item), do: Items.sell(hero, item)
end

# lib/rpg.ex
defmodule Rpg do
  alias Rpg.{Gameplay, Accounts}

  def create_current_hero(attrs, user)
    hero = Gameplay.create_hero(attrs)
    user = Accounts.set_current_hero(user, hero)
    {hero, user}
  end
end
ここで展開するには、いくつかのトピックがあります.

ユーザーとメッセージ


我々は、全く新しい文脈を持っているAccounts ユーザー登録とチャットのようなゲームプレイに関連していない要件に対処するために使用できます.
ユーザーが登録すると、彼はメッセージを送信することができる、英雄を作成したり、彼の現在の1つとして、既存のヒーローを設定します.

ゲームプレイ内の複数サービスのオーケストレーション


我々は、最初のアイテムが少しプレーヤーを助けるために装備されている我々のヒーローの作成プロセスに余分な機能を追加しました.通知方法equip_initial 公開されないbuy_item and sell_item それはヒーローの作成プロセスのどこか外で使用すべきではないので.また、プロセスの各部分を独自のサービスに分離しました.Heroes.create 私たちの新しいヒーローを返すだけで心配Items.equip_initial 特定のアイテムと既存のヒーローを装備する方法だけを知っているし、これはすべての美しく説明的な鎖に配置されている-あなたはすぐに何をそれを見て起こっている知っている.

RPG内部の多重ドメインのオーケストレーション


同じテクニックは、それから我々のアプリレベルドメインのために使われます.Rpg . ユーザーが主人公を作成した後に、新しくつくられた英雄はユーザーの現在の英雄として割り当てられるべきです、しかし、管理しているユーザーはもう一つのドメインの一部であるので、我々はこの場合、親ドメインに尋ねますRpg , 我々のために子供たちの間で仕事を調整すること.ユーザーを更新すると、ヒーロー、アカウントの取引を返すとゲームプレイ取引.レイヤー.

では、公共APIは何ですか?アプリケーションレベルのコンテキストまたはトップレベルドメイン?


あなたがアプリケーションのレベルを確認したい場合は、あなた次第ですRpg コンテキストのみパブリックパブリックAPI.これの欠点は、あなたがどちらかにどちらかを延期する多くの機能を持っているということですGameplay or Accounts と非常に繰り返しを得ることができる大規模なアプリケーションで、私は個人的にトップレベルドメインへのパブリックアクセスを提供することを好むGameplay and Accounts ), アプリケーションレベルコンテキストへのアクセスRpg ) 他のトップレベルドメイン(以前に実証されたような)や他のアプリケーションのために必要な関数だけでなく、インストゥルメント、定数やヘルパーのような用途にのみ.

結論


それはショートカットを取ることによってカプセル化を中断しないようにすべての協力者からの規律を必要とするように、このようなプロジェクトを階層化することは確かに少し威嚇することができますが、高い凝集性と層の間の低結合を持っている利点は、あなたのアプリケーションをより保守性、テスト可能で堅牢なものにします.強力な、将来の証拠条約が行われているので、新しい機能も追加するのは簡単です.
場合は、これらのパターンを実際のアプリケーションで適用を参照してくださいしたい場合は、再度チェックアウトを招待するMOBA, a project I recently open-sourced これは協力者を探しているので、ゲームを構築するのが楽しいように聞こえるなら、参加してください.
プロジェクトの構築に関する優れた議論over at this topic on ElixirForum , 私は非常に記事を楽しんで読んでお勧めします.