はじめに
そのため、範囲 、 方法論 、およびアーキテクチャをすでに決定しています。 理論から実践、コードの作成に移りましょう。 まず、ビジネスロジック( サービスとインタラクター)を記述するデザインパターンから始めたいと思います。 しかし、それらに着手する前に、 ValueObjectとEntityの構造パターンを調べます。 ルビー言語で開発します。 さらなる記事では、 Variable Architectureを使用して開発に必要なすべてのパターンを分析します。 この一連の記事への応用であるすべての開発は、別のフレームワークで収集されます。
そして、すでに適切な名前-LunaParkを選択しています。
現在の開発はGithubに投稿されています。
すべてのテンプレートを検証した後、1つの本格的なマイクロサービスを組み立てます。
歴史的に
Ruby on Railsで記述された複雑なエンタープライズアプリケーションをリファクタリングする必要がありました。 ルビー開発者の既製のチームがありました。 ドメイン駆動開発の方法論はこれらのタスクに最適でしたが、使用されている言語でのターンキーソリューションはありませんでした。 言語の選択は主に私たちの専門分野によって決定されたという事実にもかかわらず、非常に成功したことが判明しました。 私の意見では、Webアプリケーションで一般的に使用されるすべての言語の中で、ルビーが最も表現力があります。 したがって、実際のオブジェクトのモデリングに適したものは他にありません。 これは私の意見だけではありません。
それがJavaの世界です。 次に、Rubyのような新参者がいます。 Rubyには非常に表現力豊かな構文があり、この基本レベルでは、DDDにとって非常に優れた言語である必要があります(これらの種類のアプリケーションでの実際の使用についてはまだ聞いていませんが)。 Railsは、1990年代前半のWeb以前のUIと同じくらい簡単にWeb UIを作成できるようになったため、多くの興奮をもたらしました。 現時点では、この機能は、ドメインの豊富さをあまり持たない膨大な数のWebアプリケーションの構築に主に適用されています。 しかし、私の期待は、問題のUI実装の部分が削減されるにつれて、人々がこれをドメインにもっと注意を向ける機会と見なすことです。 Rubyの使用がその方向に進むようになれば、DDDに優れたプラットフォームを提供できると思います。 (インフラストラクチャのいくつかの要素を埋める必要があるでしょう。)
エリック・エヴァンス2006
残念ながら、過去13年間、大きな変化はありませんでした。 インターネット上では、Railsをこれに適応させる試みを見つけることができますが、どれもひどいように見えます。 Railsフレームワークは重く、遅く、SOLIDではありません。 ActiveRecordに基づいたRepositoryパターンの実装を誰かがどのように描写しようとしているのかを涙なしで見ることは非常に困難です。 マイクロフレームワークを採用し、ニーズに合わせて改良することにしました。 Grapeを試してみましたが、自動ドキュメント化のアイデアは成功したように見えましたが、そうでない場合は放棄され、すぐにそれを使用するというアイデアを放棄しました。 そして、すぐに別のソリューション、 シナトラを使用し始めました。 REST コントローラーおよびエンドポイントには引き続き使用します 。
Webアプリケーションを開発した場合、その技術について既にアイデアを持っています。 長所と短所があり、完全なリストはこの記事の範囲外です。 しかし、エンタープライズアプリケーションの開発者としての私たちにとって最も重要な欠点は、REST(名前からも明らかです)がプロセスではなくその状態を反映することです。 そして、その利点は理解しやすいことです。この技術は、バックエンド開発者とフロントエンド開発者の両方にとって明らかです。
しかし、その後、RESTに焦点を当てるのではなく、http + jsonソリューションを実装しますか? サービスAPIを開発し、その説明を第三者に提供したとしても、多くの質問が寄せられます。 使い慣れたRESTを提供する場合よりもはるかに多くのことができます。
RESTの使用は妥協案と考えます。 開発者がリクエスト形式をめぐる聖戦に時間を浪費しないように、簡潔さとjsonapi標準のためにJSONを使用します。
将来、 Endpointを分析するとき、残りを取り除くためには、1つのクラスのみを書き換えるだけで十分であることがわかります。 したがって、RESTに疑問がある場合は、RESTをまったく気にする必要はありません。
いくつかのマイクロサービスを作成する過程で、基礎-抽象クラスのセットを取得しました。 このような各クラスは30分で記述できます。このコードの目的を知っていれば、コードは簡単に理解できます。
ここで主な困難が生じました。 DDDプラクティスとクリーンなアーキテクチャに対処しなかった新しい従業員は、コードとその目的を理解できませんでした。 エヴァンスを読む前に私自身がこのコードを初めて見た場合、私はそれをレガシー、オーバーエンジニアリングと見なします。
この障害を克服するために、使用されるアプローチの哲学を説明するドキュメント(ガイドライン)を書くことが決定されました。 このドキュメントの概要は成功したようで、Habréに掲載することにしました。 プロジェクトごとに繰り返される抽象クラスは、別のgemに配置することにしました。
哲学
武道についての古典的な映画を思い出せば、極を巧みに扱っているタフな男がいるでしょう。 6つ目は、本質的に非常に原始的なツールであるスティックであり、人間の手に落ちた最初のツールの1つです。 しかし、マスターの手で、彼は恐るべき武器になります。
足を撃たないピストルを作成するのに時間を費やすか、または射撃のテクニックを学ぶのに時間を費やすことができます。 4つの基本原則を特定しました。
- 複雑なものをシンプルにする必要があります。
- 技術よりも知識が重要です。 ドキュメントはコードよりも人にとって理解しやすいものであり、ドキュメントを別のものに置き換えるべきではありません。
- 実用主義は独断主義よりも重要です。 標準は、境界ボックスを設定するのではなく、方法をガイドする必要があります。
- アーキテクチャの構造性、ソリューションの選択における柔軟性。
同様の哲学は、たとえば、ArchLinux OS- The Arch Wayでたどることができます。 私のラップトップでは、Linuxは長い間定着していませんでしたが、遅かれ早かれクラッシュしてしまい、常に再インストールする必要がありました。 これは多くの問題を引き起こし、時には仕事の締め切りの混乱などの深刻な問題も引き起こしました。 しかし、Archをインストールして2〜3日を費やした後、OSがどのように機能するかを理解しました。 その後、彼女は失敗することなく、より安定した仕事を始めました。 私のメモは、数時間で新しいPCにインストールするのに役立ちました。 豊富なドキュメントは、新しい問題の解決に役立ちました。
フレームワークには、絶対に高レベルのキャラクターがあります。 それを記述するクラスは、アプリケーションの構造を担当します。 サードパーティのソリューションを使用して、データベースとやり取りし、httpプロトコルやその他の低レベルのものを実装します。 プログラマーにコードをのぞき見して、1つまたは別のクラスがどのように機能するかを理解してもらいたいです。また、ドキュメントにより、それらの管理方法を理解できます。 エンジンの設計を理解しても、車を運転することはできません。
枠組み
通常の意味でLunaParkをフレームワークと呼ぶことは困難です。 フレーム-フレーム、仕事-仕事。 私たちは自分自身を範囲に限定しないことを強く求めます。 宣言する唯一のフレームは、このロジックまたはそのロジックを記述するクラスに伝えるフレームです。 それはむしろ、それらのための広範な指示を備えたツールのセットです。
各クラスは抽象的であり、3つのレベルがあります。
module LunaPark # module Forms # class Single # / end end end
単一の要素を作成するフォームを実装する場合、このクラスから継承します。
module Forms class Create < LunaPark::Forms::Single
複数の要素がある場合、別の実装を使用します。
module Forms class Create < LunaPark::Forms::Multiple
現時点では、すべての開発が完全に整っているわけではなく、gemはアルファ状態です。 記事の発行に従って、段階的に引用します。 つまり ValueObject
とEntity
に関する記事が表示される場合、これらの2つのテンプレートは既に実装されています。 サイクルの終わりまでに、それらはすべてプロジェクトでの使用に適したものになります。 フレームワーク自体はsinatra \ rodaへのリンクなしではほとんど役に立たないので、プロジェクトをすばやく開始するために「すべてを台無しにする」方法を示す別のリポジトリが作成されます。
フレームワークは、主にドキュメントへのアプリケーションです。 これらの記事をフレームワークのドキュメントとして受け取らないでください。
それでは、ビジネスに取り掛かりましょう。
値オブジェクト
-彼女の身長は?
-151
-あなたは自由の女神と会い始めましたか?
このような何かがインディアナで起こったかもしれない。 人間の成長は単なる数ではなく、測定の単位でもあります。 オブジェクトの属性は、プリミティブ(整数、文字列、ブールなど)によってのみ記述できるとは限らず、それらの組み合わせが必要になる場合があります。
- お金は単なる数字ではなく、数字(金額)+通貨です。
- 日付は、日、月、および年で構成されます。
- 体重を測定するには、1つの数値では不十分であり、測定単位も必要です。
- パスポート番号は、シリーズと実際には番号で構成されています。
一方、これは常に組み合わせではなく、おそらくプリミティブの一種の拡張です。
多くの場合、電話番号は番号と見なされます。 その一方で、彼が加算または除算の方法を持っているべきではないでしょう。 おそらく、国コードを発行するメソッドと都市コードを定義するメソッドがあります。 おそらく、数字の文字列79001231212
としてだけでなく、読み取り可能な文字列79001231212
として提示する特定の装飾方法があるでしょう。
教義に基づいて、それは議論の余地のない-はい。 常識的にこのジレンマに近づいた場合、この番号に電話することを決定すると、オブジェクト自体を電話に転送します。
phone.call Values::PhoneNumber.new(79001231212)
そして、文字列として表示することにした場合、これは明らかに人に対して行われます。 では、なぜこの行をすぐに人が読めるようにしないのでしょうか?
Values::PhoneNumber.new(79001231212).to_s
Three Axesオンラインカジノのサイトを作成し、カードゲームを販売しているとします。 「トランプ」クラスが必要になります。
module Values class PlayingCard < Lunapark::Values::Compound attr_reader :suit, :rank end end
したがって、クラスには2つの読み取り専用属性があります。
- スーツ-カードスーツ
- ランク-カードの尊厳
これらの属性は、マップの作成時にのみ設定され、マップの使用時には変更できません。 もちろん、トランプを取り、 8を消してQを書きますが、これは受け入れられません。 まともな社会では、ほとんどの場合、あなたは撃たれます。 オブジェクトの作成後に属性を変更できないことにより、値オブジェクトの最初のプロパティである不変性が決まります。
値オブジェクトの2番目の重要なプロパティは、それらを比較する方法です。
module Values RSpec.describe PlayingCard do let(:card) { described_class.new suit: :clubs, rank: 10 } let(:other) { described_class.new suit: :clubs, rank: 10 } it 'should be eql' do expect(card).to eq other end end end
そのようなテストはアドレスで比較されるため、失敗します。 テストに合格するには、 Value-Obectsを値で比較する必要があります。このため、比較メソッドを追加します。
def ==(other) suit == other.suit && rank == other.rank end
これでテストに合格します。 比較を行うメソッドを追加することもできますが、10とKを比較するにはどうすればよいですか? おそらく既に推測されているように、それらをValue Objectsの形式で提示します。 さて、次のようなトップ10クラブを開始する必要があります。
ten = Values::Rank.new('10') clubs = Values::Suits.new(:clubs) ten_clubs = Values::PlayingCards.new(rank: ten, clubs: clubs)
ルビには3行で十分です。 この制限を回避するために、 Object-Valueの 3番目のプロパティである売り上げを導入します。 .wrap
クラスの特別なメソッドを.wrap
ましょう。このメソッドは、さまざまなタイプの値を取得し、適切な値に変換できます。
class PlayingCard < Lunapark::Values::Compound def self.wrap(obj) case obj.is_a? self.class # PlayingCard obj # case obj.is_a? Hash # , new(obj) # case obj.is_a String # , new rank: obj[0..-2], suit:[-1] # , - . else # raise ArgumentError # . end end def initialize(suit:, rank:) # @suit = Suit.wrap(suit) # @rank = Rank.wrap(rank) end end
このアプローチには大きな利点があります。
ten = Values::Rank.new('10') clubs = Values::Suits.new(:clubs) from_values = Values::PlayingCard.wrap rank: ten, suit: clubs from_hash = Values::PlayingCard.wrap rank: '10', suit: :clubs from_obj = Values::PlayingCard.wrap from_values from_str = Values::PlayingCard.wrap '10C' # utf , , .
これらのカードはすべて同じです。 wrap
メソッドが適切なプラクティスに展開される場合、別のクラスに配置します。 独断的なアプローチの観点からは、別のクラスも必須となります。
うーん、デッキのスペースはどうですか? このカードが切り札かどうかを調べる方法は? これはトランプではありません。 これはトランプの価値です 。 これはまさに、段ボールの隅にある碑文10です。
Object-Valueとプリミティブに関連する必要がありますが、これは何らかの理由でrubyに実装されていませんでした。 ここから、最後のプロパティが発生します-Object -Valueはどのドメインにもバインドされていません。
推奨事項
各プロセスのすべての瞬間に使用されるさまざまな方法とツールの中で、他の方法と比べてより速く、より優れた方法とツールが常に1つあります。
フレデリック・テイラー 1914
算術演算は新しいオブジェクトを返す必要があります
# GOOD class Money < LunaPark::Values::Compound def +(other) other = self.class.wrap(other) raise ArgumentError unless same_currency? other self.class.new( amount: amount + other.amount, currency: currency ) end end
値オブジェクトの属性は、プリミティブまたは他の値オブジェクトのみです。
# GOOD class Weight < LunaPark::Values::Compound def intialize(value:, unit:) @value = value @unit = Unit.wrap(unit) end end # BAD class PlaingCard < LunaPark::Value def initialize(rank:, suit:, deck:) ... @deck = Entity::Deck.wrap(deck) # end end
クラスメソッド内で簡単な操作を維持する
# GOOD class Weight < LunaPark::Values::Compound def >(other) value > other.convert_to(unit).value end end
操作「変換」が大きい場合、おそらく別のクラスに移動するのが理にかなっています
# UGLY class Weight < LunaPark::Values::Compound def convert_to(unit) unit = Unit.wrap(unit) case { self.unit.to_sym => unit.to_sym } when { :kg => :ft } Weight.new(value: 2.2046 * value, unit.to_sym) when # ... end end end # GOOD #./lib/values/weight/converter.rb class Weight class Converter < LunaPark::Services::Simple def initialize(weight, to:) ... end end end #./lib/values/weight.rb class Weight < LunaPark::Values::Compound def convert_to(unit) Converter.call! self, to: unit end end
このようなロジックの別のサービスへの転送は、 サービスが分離されている場合にのみ可能です。外部ソースからのデータは使用しません。 このサービスは、値オブジェクト自体のコンテキストによって制限される必要があります。
値オブジェクトはドメインロジックについて何も知ることができません
オンラインストアを作成しており、商品の評価があるとします。 取得するには、 リポジトリを介してデータベースにリクエストを行う必要があります 。
# DEADLY BAD class Rate < LunaPark::Values::Single def top?(10) Repository::Rates.top(first: 10).include? self end end
エンティティ
Entityクラスは、実際のオブジェクトを担当します。 契約書、椅子、不動産業者、パイ、アイロン、猫、冷蔵庫など、何でも構いません。 ビジネスプロセスをモデル化するために必要なオブジェクトはすべてEntityです。
エンティティの概念は、EvansとMartinで異なります。 エヴァンスの観点から見ると、エンティティは、その個性を強調する何かによって特徴付けられるオブジェクトです。
オブジェクトが、属性のセットではなく、一意の個々の存在によって定義されている場合、このプロパティは、モデルでオブジェクトを定義するときにメインのプロパティとして読み取られる必要があります。 クラスの定義はシンプルで、オブジェクトのライフサイクルの継続性と一意性を中心に構築する必要があります。 形状や存在の履歴に関係なく、各オブジェクトを区別する方法を見つけます。 属性に応じてオブジェクトを比較することに関連する技術要件に特に注意してください。 そのようなオブジェクトごとに必然的に一意の結果を与える操作を定義します-特定のシンボルをこのための一意性を保証する必要があるかもしれません。 そのような識別手段には外部起源があるかもしれないが、それ自身の便宜のためにシステムによって生成された任意の識別子であってもよい。 ただし、このようなツールは、モデル内のオブジェクトを区別するためのルールに準拠する必要があります。 モデルは、同一のオブジェクトが何であるかを正確に定義する必要があります。
マーティンの観点から見ると、 エンティティはオブジェクトではなくレイヤーです。 このレイヤーは、オブジェクトとそれを変更するためのビジネスロジックの両方を組み合わせます。
エンティティの私の見解では、エンティティにはアプリケーション独立ビジネスルールが含まれています。 それらは単なるデータオブジェクトではありません。 データオブジェクトへの参照を保持できます。 しかし、その目的は、多くの異なるアプリケーションで使用できるビジネスルールメソッドを実装することです。
ゲートウェイはエンティティを返します。 実装(行の下)は、データベースからデータをフェッチし、それを使用してデータ構造を構築し、エンティティに渡します。 これは、包含または継承のいずれかで実行できます。
例:
パブリッククラスMyEntity {プライベートMyDataStructureデータ;}
または
パブリッククラスMyEntityはMyDataStructureを拡張します{...}
そして、覚えておいて、私たちはすべて本質的に海賊です。 そして私がここで話しているルールは本当にガイドラインに似ています...
エッセンスとは、構造のみを意味します。 最も単純な形式では、 Entityクラスは次のようになります。
module Entities class MeatBag < LunaPark::Entities::Simple attr_accessor :id, :name, :hegiht, :weight, :birthday end end
ビジネスモデルの構造を記述する可変オブジェクトには、プリミティブ型と値を含めることができます。
LunaPark::Entites::Simple
クラスは信じられないほどシンプルで、そのコードを見ることができます。1つだけ-簡単な初期化を提供します。
module LunaPark module Entities class Simple def initialize(params) set_attributes params end private def set_attributes(hash) hash.each { |k, v| send(:"#{k}=", v) } end end end end
あなたは書くことができます:
john_doe = Entity::MeatBag.new( id: 42, name: 'John Doe', height: '180cm', weight: '80kg', birthday: '01-01-1970' )
おそらく既にご想像のとおり、体重、身長、生年月日をValue Objectsでラップします。
module Entities class MeatBag < LunaPark::Entites::Simple attr_accessor :id, :name attr_reader :heiht, :wight, :birthday def height=(height) @height = Values::Height.wrap(height) end def weight=(height) @height = Values::Weight.wrap(weight) end def birthday=(day) @birthday = Date.parse(day) end end end
このようなコンストラクタで時間を無駄にしないために、 LunaPark::Entites::Nested
より複雑な実装を LunaPark::Entites::Nested
。
module Entities class MeatBag < LunaPark::Entities::Nested attr :id attr :name attr :heiht, Values::Height, :wrap attr :weight, Values::Weight, :wrap attr :birthday, Values::Date, :parse end end
名前が示すように、この実装ではツリー構造を作成できます。
かさばる家電製品に対する私の情熱を満足させましょう。 前の記事で、洗濯機のねじれと建築の類似性を引き出しました。 次に、冷蔵庫などの重要なビジネスオブジェクトについて説明します。
class Refregerator < LunaPark::Entites::Nested attr :id, attr :brand attr :title namespace :fridge do namespace :door do attr :upper, Shelf, :wrap attr :lower, Shelf, :wrap end attr :upper, Shelf, :wrap attr :lower, Shelf, :wrap end namespace :main do namespace :door do attr :first, Shelf, :wrap attr :second, Shelf, :wrap attr :third, Shelf, :wrap end namespace :boxes do attr :left, Box, :wrap attr :right, Box, :wrap end attr :first, Shelf, :wrap attr :second, Shelf, :wrap attr :third, Shelf, :wrap attr :fourth, Shelf, :wrap end attr :last_open_at, comparable: false end
このアプローチにより、冷蔵庫のドアなどの不要なエンティティを作成する必要がなくなります。 冷蔵庫がなければ、それは冷蔵庫の一部でなければなりません。 このアプローチは、保険の購入申請など、比較的大きな文書をコンパイルするのに便利です。
LunaPark::Entites::Nested
クラスには、さらに2つの重要なプロパティがあります。
比較可能性:
module Entites class User < LunaPark::Entites::Nested attr :email attr :registred_at end end u1 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now) u2 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now) u1 == u2 # => false
指定された2人のユーザーは同等ではありません。 それらは異なる時間に作成されたため、 registred_at
属性の値は異なります。 ただし、比較対象のリストから属性を削除した場合:
module Entites class User < LunaPark::Entites::Nested attr :email attr :registred_at, comparable: false end end
次に、2つの比較可能なオブジェクトを取得します。
この実装には売上高のプロパティもあります-クラスメソッドを使用できます
Entites::User.wrap(email: 'john.doe@mail.com', registred_at: Time.now)
Hash、OpenStructまたは任意のgemをEntityとして使用できます。これにより、エンティティの構造を実現できます。
エンティティはビジネスオブジェクトのモデルであるため、単純にしておきます。 一部のプロパティがビジネスで使用されていない場合は、説明しないでください。
エンティティの変更
お気づきのとおり、 Entityクラスには独自の変更のメソッドはありません。 すべての変更は外部から行われます。 値オブジェクトも不変です。 その中に存在するすべての機能は、概して、本質を装飾したり、新しいオブジェクトを作成したりします。 本質自体は変わりません。 Ruby on Rails開発者にとって、このアプローチは珍しいでしょう。 側から見ると、一般にOOP言語を他の何かに使用しているように見えるかもしれません。 しかし、少し深く見てみると、そうではありません。 ウィンドウは自動的に開くことができますか? 車で仕事をして、ホテルを予約して、かわいい猫が新しい加入者を獲得しますか? これらはすべて外部の影響です。 現実の世界で何かが起こっています。私たちはこれを自分自身に反映しています。 リクエストごとに、モデルに変更を加えます。 そのため、ビジネスタスクに十分な最新の状態に維持します。 モデルの状態と、この状態の変化を引き起こすプロセスを分離する必要があります。 これを行う方法については、次の記事で検討します。