モデル、ロジック、OOP、開発、その他について

あなたはよく考えます-なぜ何かが何らかの方法で行われますか? マイクロサービスまたはモノリス、2リンクまたは3リンクがあるのはなぜですか? 多層アーキテクチャが必要な理由と、いくつの層がありますか? ビジネスロジック、アプリケーションロジック、プレゼンテーションロジックとは何ですか? アプリケーションを見てください-どのように設計されていますか? その中には何があり、どこにあるのですか、なぜこのようになっているのですか?
それは本に書かれているからでしょうか? これまたはそのアプローチ/パターンはあなたのどの問題を解決しますか?
一見して明らかなことでさえ、説明するのが非常に難しい場合があります。 そして時々、説明しようとして、明らかな考えが完全に間違っていたという理解があります。
いくつかの例を取り上げ、これらの問題をあらゆる側面から研究してみましょう。

おもちゃの町
仮想都市
この言葉でどれだけ固い...
サブジェクトエリア
プレゼンテーションロジック
状態保存
階層化
2掾
N幤
2として3
サービス
ツール
理論と実践
まとめ

おもちゃの町


小さなおもちゃの街を想像してみましょう。 それはいくつかの建物で構成され、いくつかの道路が通ります。 車は道路を動き、人々は歩きます。 交通は信号機で規制されています。 市内で起こることはすべて特定のルールの対象であり、この多様性はすべて制御できます。

人や車を動かしたり、信号機を切り替えたり、昼や夜の時間を変更したりできます。複数の人が同時にこの都市と対話できます。 彼らは単に見たり何かをしたりして、都市を強制的に変化させることができます。 これらはすべて完全に存在しますが、おもちゃの街を仮想世界に移すことが必要になるときが来ます。


街と直接交流した人々は、今では椅子で快適になり、暗いモニターを見つめ、片手でマウスを握り、もう片方をキーボードに置き、すべてが復活し、仮想都市が目の前の色で輝く瞬間を待っています。 しかし、これを実現するには、長い道のりがあります。

仮想都市


まず、最も重要なこと-仮想都市のモデルを作成する必要があります。 これは単純なもののように思えるかもしれませんが、実際、これは問題と困難の大部分が存在する場所です。 しかし、まだ開始する必要があるので、始めましょう。

私たちの目標は、都市モデルを仮想形式で記述することです。 このため、一般的な高レベルのオブジェクト指向言語を使用します。 このような言語の使用には、都市の仮想モデルを作成するための主要な構成要素としてのオブジェクトの使用が含まれます。

もちろん、 1つのオブジェクトでモデル全体を単純に記述することもできますが、これには不必要な複雑さと複雑さが伴います。 すべてが1か所に「ダンプ」され、どのように混同されているかが明確でない場合、何が起こっているのかを理解するのが難しくなり、さらに変更を加えることが難しくなります。 したがって、結果のプログラムを混乱させないために、簡単にするために、都市の説明を小さな部分に分けます。

そのような部分として、私たちは私たちの実際の都市を見るときに互いに簡単に分離されているものを取ります-この都市の個々のオブジェクト(文化の家、交差点の赤いBMW、彼のビジネスについて走っているペトロヴィッチ)。 仮想世界の各オブジェクトの説明は、そのプロパティ(色、モデル、名前、場所など)の説明です。 毎回同じオブジェクトの同じプロパティを繰り返し記述しないために、そのようなプロパティのグループを選択し、それらをオブジェクトのタイプと呼びます 。 良い候補者は、車、家、人などの一般的なタイプです。 主なプロパティの説明を集中することができます。 また、たとえば、さまざまなタイプのマシンは、「マシン」の基本タイプを独自のプロパティセットで補完し、新しいタイプのセット全体を作成します。 元の型に対するこのような新しい型は、 相続人と呼ばれます。 そして、プロセス自体は、 継承による既存のタイプに基づいた新しいタイプの作成です。


私たちが作成したすべての種類のオブジェクトは、私たちの都市のモデルを表しています。

その後、都市内の既存のオブジェクトごとにこれらのタイプのインスタンスを作成し、一意の値を入力します。

そして、すべてがその場所に置かれているように見えます。車のグループが緑の信号を待っている交差点に立っており、女の子のジュリアがエレベーターを待っています。さらに、巨大な超高層ビルのパイプの水が凍りました。 モデルをstateで満たし、特定の時点で実際の都市の状態を繰り返しました。

しかし、実際の都市をよく見ると、常に変化していることがわかります。 状態のすべての変更は、さまざまなオブジェクトのプロパティ値の変更、新しいオブジェクトの外観、または古いオブジェクトの消失です。 ここで、信号機が切り替わり、「現在の信号機」プロパティの値が赤から緑に変更されました 。 ここで、エレベーターは「Floor」プロパティの値を2番目から1 番目に変更し、「Doors open」プロパティの値をyesからnoに変更しました 。

これは、私たちの都市が生き返るには、プログラムがモデルの状態を変更できる必要があることを意味します。つまり、さまざまなオブジェクトのプロパティを変更したり、新しいオブジェクトを追加したり、古いオブジェクトを削除したりすることができます、つまり 行動する 。

これを行うには、私たちの街で可能なすべてのアクションをプログラムに追加します。 このような各アクションは、オブジェクトまたはオブジェクトのグループのプロパティを変更する手順として説明できます。 これらすべての手順の説明の後、それらの数が非常に大きいことが明らかになります。 利用可能なすべての手順のサポートと変更を簡素化するには、それらをグループに分ける必要があります。 よく考えることなく、アクションの類似性に基づいてこのような手順をグループ化し、一連のクラスを取得できます。各クラスは、同様のアクションのセットを担当します。

今ではすべてが分割され、非常に見栄えが良いように見えますが、1つの「しかし」があります。 オブジェクトのプロパティの説明は、これらのプロパティを変更し、モデルを貧血モデルに変える手順とは完全に分離されています。 この分離により、オブジェクトのプロパティがどのように変化するかは完全に理解できません。 また、あるプロパティの変更を他のプロパティの変更に関連付ける必要がある場合、または他のプロパティの値に依存する必要がある場合、オブジェクトの内部構造に関するこの知識は、このプロパティを変更するすべてのプロシージャで複製する必要があります。 これらの問題を回避するために、アクションごとにプロシージャをグループ化するのではなく、プロパティを変更するタイプにこれらのプロシージャを分解します。

これにより、他のユーザーが使用するプロパティとメソッドのみを公開して、オブジェクトの内部構造に関する知識を隠すことができます。 仕事の内部原則を隠すこのプロセスは、 カプセル化と呼ばれます 。 たとえば、エレベーターを複数の階に移動したいとします。 これを行うには、ドアの状態を確認する必要があります-開閉、エンジンの始動と停止など。 このロジックはすべて、フロアへの移動アクションの背後に単純に隠されます。 その結果、オブジェクトのタイプは、これらのプロパティを変更するプロパティとプロシージャのセットであることがわかりました。

一部のプロシージャは同じ意味を持つ場合がありますが、異なるオブジェクトに関連付けられます。 たとえば、「音声信号を発する」手順は、赤のBMWと青のラダにあります。 内部ではまったく異なる方法で実行できますが、同じ意味を持ちます。

すでに一般的なタイプの「マシン」があるため、「音声信号を発する」手順をそこに置くことができます。 そのような動作のロジックがすべての人で同じであれば、同じ場所で判断できます。 これにより、派生型が簡素化され、コードの重複がなくなります。 しかし、突然、派生型でそのような動作のロジックが異なる場合、動作を再定義することで簡単に変更できます。 この可能性はポリモーフィズムと呼ばれます。

抽象化、継承、カプセル化、およびポリモーフィズム-これらはOOPの喜びであり、これに続いてより柔軟なデザインを作成できます。 それらに加えて、目的が同じである特定の原則のセットがあります-より柔軟な設計を作成するのを助けるために。

この言葉でどれだけ固い...


SOLIDは、オブジェクト指向設計の有名な原則の略です。 この略語には、各文字に1つずつ、5つの非表示があります。

単一責任の原則 -各エンティティは、変更の理由を1つだけ持つ必要があります。

すべての家の各部屋でインターネットと電気の両方を使用するために、インターネットと電線用のコネクタを備えた単一のコンセントが作成されました。

public class Socket { private PowerWire _powerWire; private EthernetWire _ethernetWire; public Socket(PowerWire powerWire, EthernetWire ethernetWire) { _powerWire = powerWire; _ethernetWire = ethernetWire; } ... } 

このようなコンセントの操作には、インターネット回線と電線を供給する必要がありました。 そして、すべてのソケットが常にインターネットと電気の両方を使用するわけではないが、コンパクトで便利であるという事実には何の問題もないように思えます。 しかし、変化の時が来ると、すべてがそれほど便利ではなくなります。

まず、インターネットが不要なアパートや建物全体がありました。 絶対に。 しかし、ソケットがソケットで機能するためには、そこにもインターネットワイヤを引き伸ばす必要があり、利益をもたらさずに作業コストが増加するだけでした。

その後、ソケットに接地用の追加のワイヤを装​​備する必要があるため、インターネット専用のソケットも含めて、すべてのソケットを変更する必要がありました。 大量の作業が行われましたが、インターネットのみに使用されるソケットが影響を受けない場合は、それよりも少ない可能性があります。

 public class Socket { private PowerWire _powerWire; private EthernetWire _ethernetWire; private GroundWire _groundWire; public Socket(PowerWire powerWire, EthernetWire ethernetWire, GroundWire groundWire) { _powerWire = powerWire; _ethernetWire = ethernetWire; _groundWire = groundWire; } ... } 

しかし、最後のストローは要件でした。すべてのインターネットアウトレットのインターネットワイヤを新しい標準に変更することです。 一般に、すべてのソケットは同時にインターネットソケットでもあるため、すべてを再度変更する必要がありました。 インターネットに使用されるコンセントの数は、すべてのコンセントの数よりも数倍少ないため、作業量ははるかに少なくなります。

 public class Socket { private PowerWire _powerWire; private SuperEnthernetWire _superEnthernetWire; private GroundWire _groundWire; public Socket(PowerWire powerWire, SuperEthernetWire superEthernetWire, GroundWire groundWare) { _powerWire = powerWire; _superEnthernetWire = superEnthernetWire; _groundWare = groundWare; } ... } 

すべての場合において、いくつかの完全に無関係な職務が1つのオブジェクトに一度に結合されたという事実により、まったく不要な量の作業が行われました。 そして、これらの義務にはそれぞれ、変更の理由があります。

このような問題を回避するために、コンセントは、互いに独立した2つの部分(電気コンセントとインターネットコンセント)に分割する必要がありました。

 public PowerSocket { private PowerWire _powerWire; private GroundWare _groundWare; public PowerSocket(PowerWire powerWire, GroundWare groundWare) { _powerWire = powerWire; _groundWare = groundWare; } ... } public class EthernetSocket { private SuperEthernetWire _superEthernetWire; public EthernetSocket (SuperEthernetWire _superEthernetWire) { _superEthernetWire = superEthernetWire; } ... } 

そして、実際に電気とインターネットの両方のコンセントが必要な場合は、 集約を使用できます。

 public PowerEthernetSocket { private PowerSocket _powerSocket; private EthernetSocket _ethernetSocket; public PowerEthernetSocket (PowerSocket powerSocket, EthernetSocket ethernetSocket) { _powerSocket = powerWire; _ethernetSocket = ethernetSocket; } ... } 

オープンクローズの原則 -オブジェクトは変更のために閉じられ、拡張のために開かれなければなりません。

市内中心部の重要な情報を人々に知らせるために、最も忙しい交差点に大きなスクリーンが設置されました。 さまざまなソースからのメッセージのテキストを表示しました。

 public class Message { public string Text { get; set; } } public class BigDisplay { public void DisplayMessage(Message message) { PrintText(message.Text); } public void PrintText(string text) { ... } } 

しばらくして、新しいタイプのメッセージが表示されました-日付を含むメッセージ。 そして、画面上のそのようなメッセージについては、日付とテキストの両方を表示する必要がありました。 洗練はさまざまな方法で実行できます。
1.「メッセージ」のタイプから派生した新しいタイプを作成し、属性「日付」を追加して、メッセージを表示する手順を変更します。

 public class Message { public string Text { get; set; } } public class MessageWithDate : Message { public DateTime Date { get; set; } } ... public void DisplayMessage(Message message) { if (message is MessageWithDate) PrintText(message.Date + Message.Text) else PrintText(message.Text); } 

しかし、この方法は、何らかの形でメッセージを表示するすべてのタイプの動作を変更する必要があるという点で悪いです。 そして、将来、新しい特別なタイプのメッセージがまだある場合は、すべてを再度変更する必要があります。

2.「date」プロパティを「message」タイプに追加し、テキストの受信方法を変更して、次のようにします。

 public class Message { private string _text; public string Text { get { if(Date.HasValue) return Date.Value + _text; else return _text; } set { _text = value; } } public DateTime? Date { get; set; } } 

しかし、最初に、このメソッドはメインのタイプを変更してそれに動作を追加する必要があるという点で悪いです。 第二に、新しいタイプのメッセージが表示された場合、すべてのメッセージにはない別の属性を作成し、コードに不要なチェックを追加する必要があります。 第三に、日付付きのメッセージの場合、日付なしでメッセージテキストを取得する方法はありません。 そして、そのようなニーズの場合-メッセージのテキストを選択する必要があります。

3.新しいタイプをまったく作成せず、メッセージが画面に表示される方法を変更しないように、メッセージテキストに日付をすぐに書き込みます。 ただし、メッセージから日付を取得するだけの場所では、最初にこのメッセージに日付が含まれているかどうかを確認してから、メッセージテキストから分離する必要があるため、この方法は不適切です。

開放性と近接性の原則に従うと、これらの問題をすべて回避し、4番目の方法に進むことができます。

 public class Message { public string Text { get; set; } public virtual string GetDisplayText() { return Text; } } public class MessageWithDate : Message { public DateTime Date { get; set; } public override string GetDisplayText() { return Date + Text; } } ... public void DisplayMessage(Message message) { PrintText(message.GetDisplayText()); } 

このアプローチでは、「メッセージ」の基本タイプを変更する必要はありません- 閉じたままになります。 同時に、その機能を拡張することもできます。 さらに、他のアプローチに固有の問題はすべてなくなります。

リスコフ置換の原則 -基本型を使用する関数は、基本型のサブタイプを知らなくても使用できる必要があります。

「自転車」タイプをプログラムに追加したら、もう1つのタイプ、「モペット」を追加します。 モペットは自転車のようなものですが、それだけです。 そのため、バイクはモペットのベースタイプとして最適です。 すぐに言われ、プログラムには別のタイプの「モペット」がありました-「自転車」のタイプに由来します。

 public class Bicycle { public int Location; public void Move (int distance) { ... } } public class MotorBicycle : Bicycle { public int Fuel; public override void Move(int distance) { if (fuel == 0) return; ... fuel = fuel – usedFuel; } } 

しかし、自転車と比較すると、モペットには1つの不快な機能があります。ガソリンがなくなると、モペットは移動できなくなります。 そして、この不快な機能をコードで考慮する必要がありました。 彼らはそれを考慮に入れましたが、それを重要視しませんでした-このため、あらゆる種類の特定の機能を考慮するために、派生型があります。

モペットは自転車よりも高速であるため、可能であれば、自転車が以前に使用されていたプログラムで使用され始めました。 しかし、ある晴れた日、プログラムはtight落しました。 エラーの検索は長くて苦痛でした。 問題は定期的かつランダムに繰り返されました。 眠れぬ夜の説明を省略し、すぐにすべての病気の犯人-サイクリストを互いに離れた2つのポイント間で移動させる方法に目を向けます。

 public void LongJourney (int to, Biker biker, Bicycle bicycle) { while(bicycle.location < to) { int distance = to - bicycle.location; if (distance > 1000) distance = 1000; bicycle.move(distance); biker.sleep(); } } 

ガソリンが不足しているモペットが自転車ではなくメソッドに転送されると、そのようなアクションが明示的に呼び出された場合でも、モペットがセンチメートルを進めなかったため、永遠にハングします。 この問題を修正するために、もちろんそうすることができます:

 public void LongJourney (int to, Biker biker, Bicycle bicycle) { while(bicycle.location < to) { int distance = to - bicycle.location; if (distance > 1000) distance = 1000; if (bicycle is MotorBicycle && bicycle.Fuel == 0) FillFuel((MotorBicycle)bicycle); bicycle.move(distance); biker.sleep(); } } public void FillFuel(MotorBicycle motorBicycle) { ... } 

しかし、そのような変更を導入するには、多数の手順を変更する必要があります。これは、第一に、長い間、第二に、忘れることができ、そのような忘却が別のとらえどころのない問題につながるという事実に満ちています。 さらに、このような条件の追加は、抽象化のリークになります。 そして、行動に影響を与える他の要因が出現した場合、これらの困難はすべて倍増します。

実際、最初の問題は、外部の類似点がすべてあるにも関わらず、モペットは一種の自転車ではないという事実にあります。 したがって、それらを共通の分母に持ち込もうとしても、何も良い結果にはなりません。 モペットについては、自転車から独立した別のタイプを作成し、必要なすべての手順でこれを考慮する必要がありました。

インターフェイス分離の原則 -クライアントは、使用しない方法に依存するべきではありません。

より現実的にするために、1つの興味深い動作が追加されました。 人が道路上の間違った場所に現れた場合、彼が停止していた車両は停止し、鳴り響くはずです。 これを行うために、メソッドが実装されました。

 public void CheckIntersect(Car car, People[] people) { ... if (Intersect(car, people)) { car.Stop(); car.Beep(); } } public bool Intersect(Car car, People[] people) { ... } 

動作がチェックされ、自転車がシステムに現れて高速道路に走った男にぶつかるまで、すべてが順調でした。 衝突チェックと自動停止は車に対してのみ行われたため、これは驚くことではありません。

最初のクレイジーなアイデアは、自転車を「マシン」タイプの派生物にしたいという願望である可能性があります。 結局、よく見ると、手順は自転車のようにマシンのアクションのみを使用し、悪いことは何も起こりません。 しかし、これはこの方法でのみです。 そのような奇妙な派生型が、マシン固有のものを使用する他のプロシージャに渡されると、そのようなプロシージャは必然的に壊れます。

2番目のクレイジーなアイデアは、自転車と人との衝突をチェックするための別の手順を作成することです。 しかし、その後、すべてのロジックが複製されることがわかります。 さらに、新しい車両ごとにホテルプロシージャを作成する必要があります。 これはまったく柔軟性がありません。

しかし、衝突チェック手順で車両が持つ2つのアクションのみを使用する場合、特定のタイプをメソッドに渡すのはなぜですか?

このメソッドで使用できるタイプに準拠する必要があるコントラクトを定義し、すべてのビークルに実装できます。

 public interface IVehicle { ... void Stop(); void Beep(); } public class Car : IVehicle { ... } public class Bycycle : IVehicle { ... } public void CheckIntersect(IVehicle vehicle, People[] peoples) { ... if (Intersect(vehicle, peoples)) { vehicle.Stop(); vehicle.Beep(); } } public bool Intersect(IVehicle vehicle, People[] people) { ... } 

次に、この方法では、既存のタイプの車両と、契約条件に従って将来登場するタイプの車両を転送できます。

依存関係の逆転の原則 -抽象化は詳細に依存すべきではありません。 詳細は抽象化に依存する必要があります。

私たちの街にはニュース通知システムがあります。 システムは重要なニュースを受信し、都市のスピーカーシステムを介してそれらを放送します。

 public class NotifySystem { private SpeakerSystem _speakerSystem = new SpeakerSystem(); public void NotifyOnWarning(string message) { _speakerSystem.Play(message); } } 

すべて正常に動作しますが、要件は変更される傾向があります。 現在、スピーカーシステムではなく、携帯電話を介してSMSとしてメッセージを送信する必要がある場合があります。 もちろん、SMS経由で通知メッセージを送信するための別のオブジェクトを作成することもできますが、そのためにはほとんどのロジックを複製する必要があります。 これを回避するために、他の方法を使用します。 通知システムは、原則として、メッセージの表示方法を気にしません。 最も重要なことはそれらを送信することです。 したがって、これを行うことができます。

 public class NotifySystem { private IMessageDelivery _messageDelivery; public public NotifySystem(IMessageDelivery messageDelivery) { _messageDelivery = messageDelivery; } public void NotifyOnWarning(string message) { _messageDelivery.SendMessage(message); } } 

通知システム内で使用するメッセージ配信システムのインターフェイスを単に発表します。 そして、このインターフェースの実装は、通知システムを使用したい人の肩にかかっています。 実際、私たちは消費者にとって便利なように拡張できるような行動パターンを作成しています。

-そして、これらのすべての原則に煩わされず、どのようにコードを書くことも可能でしょうか?
-もちろん!
「それでも同じように機能しますか?」
-当然!
-では、なぜこれらすべての困難をOOP、OODの形で使用する必要があるのですか? これらすべての困難について考えなければ、プログラムをもっと早く書くことになります! そして、書く速度が速いほど良い!
-プログラムが十分に単純であり、エラーのさらなる開発、修正、または修正が予想されない場合、原則としてこれはすべて不要です。 しかし! プログラムが複雑な場合は、それを完成させてサポートする予定です。これらの「不必要な困難」をすべて使用すると、最も重要なこと、つまり改善とサポートに費やされるリソースの数、つまりコストに直接影響します 。

これらのすべてのルール、パターン、パラダイムは、コードをモジュール化、柔軟、容易に拡張可能、および変更に耐えられるようにするという1つの基本的な考え方によって統一されています。 原則として、このような小さく疎結合されたモジュールの動作を変更する方が、すべてが混在している大きなモジュールの動作を変更するよりも安価です。

サブジェクトエリア


「悪いアルゴリズムのコストが1,000ドルである場合、悪いアーキテクチャのコストは100万ドルになることを忘れないでください。」

それでも、上記で説明したことはすべて、基本的に技術的な問題の解決策です。 OOP、OODの概要、それらの利点、およびそれらの使用方法については、すでに何十年も計画されています。 たとえば、SOLIDなどの2、3のキーワードを操作して、多くの説明情報を取得するだけで十分です。

問題が非常に一般的であり、その解決のために何らかのアルゴリズムを必要とする場合、ほとんどの場合、すでに存在している可能性があります。 検索エンジンでのいくつかのクエリ、たとえば、「 輪になったワゴンを数え、解決策 」と、目の前にある何百もの結果。

しかし、ロジックをモジュールとサービスに正しく分割するには、技術的な知識だけでは不十分であり、検索エンジンはここでは役に立ちません。 主題分野をよく理解する必要がありますが、これは既に問題になっています。プログラミングとはまったく関係のないことを学ぶことに多大な努力を払う必要があるからです。 そのため、開発者は技術的な問題を解決することを好み、アナリストに頼ってビジネス上の問題を解決します。 しかし、アナリストはプログラムの内部構造を認識していません。内部構造はビジネスの構造を反映していませんが、この構造に関する開発者のアイデアを反映しています。 そして、この見解が現実から遠ざかるほど、この構造は、ビジネスで発生した変化に合わせて変更することがより難しくなります。

これも問題の二重の解釈によるものです。 最初に、アナリストはアプリケーションで追加または変更する必要があるものを理解しようとし、ビジネスユーザーと通信します。 その後、彼はこの知識を開発者に自分の言葉で伝えようとします。 その結果、真実が歪められ、これらすべての歪みが最適なプログラム構造ではなくなる可能性があります。

ユーザーは、すべてが見えるように、日光が家の部屋の日光に入らないようにという願いを表明しました。 開発者は、考え直すことなく、壁の丸い穴をくりぬいて確認しました。光がまだ足りないことがわかりました。 その後、開発者は近くの2つの穴をくりぬき、再度確認しました。 十分な光があることを確認した後、開発者はタスクが完了したと判断しました。 この原則によれば、すべての家に穴が空いていました。 要件は満たされました。
しかし、夏が終わり、冬が来て、それで寒さが変わりました。 怒ったユーザーは走り始め、部屋がひどく寒くなったことを誓い始めました。 理由を明確にすると、日光の下で穴が開いたために、通りから多くの冷たい空気が部屋に入り、凍結することが明らかになりました。 状況を明確にした後、ユーザーは普通のウィンドウを望んでいたことがわかりましたが、いくつかの要件について言及するのを忘れ、アナリストは自分のやり方で何かを与え、何が起こったのかがわかりました。 すべてが凍結されたため、外出先で問題を解決する必要があったため、良い窓を開発したり、冬に窓の穴を作り直したりする時間はありませんでした。 そのため、「松葉杖」と仲良くして、穴をポリエチレンで塞ぐことにしました。 この方法は、激しい凍結を取り除くのに役立ち、午後には日光を残すことができました。 しかし、初期要件の不完全な理解がもたらす新しい問題の数を誰が知っているのか...

開発のすべての参加者の間でこの問題を解決するために、何が起こっているのか、そして何のためにそれが必要であるのかについての共通の平等なアイデアが開発されるべきです。 これには全体的なアプローチがあります。 ドメイン駆動設計の目的は、開発のすべての参加者間で共通の理解と共通の用語(共通言語)を開発することです。

モデルに対する不十分な理解は、開発者が技術的な問題を解決するために、開発者がより身近で理解しやすいことをしようとするという事実につながります。 純粋に技術的な方法でビジネス上の問題の解決を含みます。 そして技術的な問題がない場合-彼らの発明と彼ら自身の英雄的な解決策。 これはすべて、プログラム内の奇妙で不明瞭な構造の出現につながるだけです。 一方、開発者がIpoteka Driven Developmentに従う場合、これは意識的な選択かもしれません。

しかし、OODの原則やその他の抽象的なトピックに関する短い旅行を終えて、プログラムに戻る時が来ました。 モデルにビジネスロジックを追加すると、凍結した都市のスナップショットだけでなく、本格的なモデルが得られました。 実際のモデルの状態と同じ方法で簡単に状態を変更できるモデル。

プレゼンテーションロジック


私たちのすべての成功にもかかわらず、ユーザーは絶滅したモニターの前でイライラして待ち続けています。 私たちは都市のモデルを作成し、そこに生命を吹き込み、その変化の論理を記述して、非常に重要な仕事をしました。 今、私たちはユーザーに私たちがやったことを「見る」機会を与える必要があります。これを行うには、ユーザーと対話するようにアプリケーションを教える必要があります。まず、ユーザーは自分の都市で何が起こっているのか、車がどのように動くのか、夜窓がどのように点灯するのか、人々がどこへ飛び出すのかを見たいと思っています。そして次に、彼は街に影響を与えたいと思っています-信号機を切り替え、昼夜を変え、新しい家を作りますなど。

しかし、全体を全体としてカバーすることは、単純な場合にのみ可能です。モデルが複雑な場合、画面上に完全に表示し、さらに理解するために、それは難しいだけでなく、ほとんど不可能になります。複雑さに対処する最善の方法は、これから行うように、複雑さをより単純な部分に分割することです。モデルの状態を部品で表示します。最も重要なことは、これらの部品を正しく識別することです。そのような各部分は、できるだけ自給自足でなければなりません。いくつかの有用な情報を理解したり、変更に関して必要な決定を下したりするのに十分です。したがって、ユーザーのタスクに応じて状態をパーツに分割し、それらをユーザーに表示しようとします。

たとえば、交差点の信号機を調整します。これを行うために、私たちはどの家が立っているか、人々が働いているか住んでいるか、青信号を待っているか、街の反対側で何が起こっているかを知る必要はありません。決定を下すために必要なのは、十字架とその上にあるものに関する情報だけです。車の数と車線の数、横断歩道で何人の人が待っているか、信号機の信号は何ですか。このタスクのコンテキストでの交差点は、他のオブジェクト(車、信号機、人)が接続されているルートオブジェクトです。たとえば、都市の交通流全体を管理するなどの別のタスクの場合、交差点は別のルートオブジェクト(都市全体の輸送インフラストラクチャ)の一部になります。

ユーザーの画面に交差点の状態を表示すると、実際には、プログラムにアクセスしたときの彼の「写真」が表示され、ユーザーはそれを調べて決定する機会が与えられます。ユーザーは、この「写真」をあらゆる角度から研究し、自分に役立つ何かを学ぶことができます。この知識に基づいて、彼はモデルの状態の変更について決定を下すことができます。これを行うために、彼は、例えば、交通信号を切り替えることができます。もちろん、写真はフリーズせずにアニメーション化することもできます。定期的に変更し、発生した変更を表示します。ただし、最新のステータスが表示される保証はありません。

実際のモデルを仮想モデルに転送する過程で、それはパラメータとその値の非人間的なセットに変わりました。そして、あなたの心が望むとすぐに、このデータセットはねじれ、変換されます。したがって、クロスロードは、モデルの他の部分と同様に、さまざまな方法で表示できます。そして、それらすべてが使いやすいわけではありません。この利便性は、ユーザーの意思決定のスピードと品質に表れます。

たとえば、交差点の状態をいくつかのリストの形式で画面に表示できます。一方では、現在走行中のすべての車をリストし、もう一方では-交差点にいる人々をリストします。リストに加えて、信号機と交差点の名前と座標を含むいくつかのテキストフィールドを表示するスイッチのセットが画面に表示されます。


または、交差点を人にとってより便利な方法で表示できます。実際のモデルに見えるように交差点を描き、車や人を置き、信号機を描きます。角のどこかに、交差点の位置を示すアイコンとともに、都市のミニマップを表示します。明らかに、2番目のケースでは、状態の認識がはるかに便利で高速になります。どちらの場合でも、画面には同じデータが表示されます。


プログラムの残りの部分でも同じです-データを画面に表示するだけでなく、できるだけ便利で理解しやすいものにすることが非常に重要です。

そして、Microsoft HoloLensのようなものを使用する場合、一般的に、仮想都市を現実のように表示したり、さらに便利にしたりできます。

状態保存


, , .
— – .
— – .
. . — , – . , …


もちろん、私たちの世界は完璧とはほど遠いものであり、コンピューターは継続的に動作することはできません。さらに、コンピューターのRAMは、モデルのすべての状態をメモリに保持するのに十分ではない場合があります。したがって、コンピューターのシャットダウンに耐えられるように状態を維持することは、ほとんどのプログラムの重要な要素です。また、モデルのすべてのオブジェクトが同時に変更されることはないため、通常は常に保存状態を維持し、小さな部分で保存状態から復元して、必要な変更を表示または変更できます。その後、新しい状態を安全に保存できます。

状態を保存するには、さまざまな方法があります。何らかの形式でファイルを保存したり、ディスクまたはUSBフラッシュドライブに書き込んだり、特別なストレージシステムを使用したりできます。現時点で最も一般的な方法は、このためにデータベースを使用することです。現在最も人気のあるのはリレーショナルデータベースです。

ホイールを再発明せず、RDBMSを使用してモデルの状態を維持する問題を解決しません。データの保存と取得の利便性に加えて、さまざまな制限を使用して実装されるデータ整合性制御などの追加の便利な機能を提供します。また、データベースは保存時のさまざまな障害から保護し、変更された状態を部分的にしか記録できないため、破損状態になります。

データベースでは、すべての情報はテーブルに保存されます。原則として、オブジェクトの各タイプには独自のテーブルがあります。また、この表のオブジェクトの各プロパティには列があります。そのため、あるタイプのインスタンスを保存するには、適切な列セットを使用して、そのインスタンスに適したテーブルを作成する必要があります。

テーブルの必要な構造をすべて説明したので、安全性を心配しないように、最終的にデータベースに状態を保存できます。ただし、このためには、これらのテーブルを使用して状態を保存およびロードすることをアプリケーションに教える必要があります。これを行うために、完成したツールをORMの形式で取得し、データベース内のタイプとテーブル間のマッピングを説明します。タイプがそれほど多くない場合、またはサードパーティのツールを使用したくない場合、または使用できない場合は、他の 方法があります。

保存する別の興味深い方法は、モデル自体の状態を保存するのではなく、この状態を変更したアクションを記述することです。一連の動きとして、データベース内のやり直しログまたはチェスの試合の記録に似ています。このアプローチは、イベントソーシングと呼ばれます。

-聞いて。データを保存するための構造を作成し、それらに制限、トリガー、テーブル間の関係などを追加することがわかりました。アプリケーションサーバー上のモデルに既に存在するビジネス上の制約を複製しますか?
-そのようになります。
-複製が悪いことを知るたびに、それが複製されたことが判明しました。うーん...


階層化


プログラムを慎重に作成し、異なるアクションを混在させないようにしたため、最終的に、いくつかの異なるロジックレイヤーがありました。

ビジネスロジック

これは、モデルの状態を変更するプログラムロジックの一部です。変更を行うために満たさなければならない条件と、変更自体について説明します。彼女はモデルを知っており、利用可能です。

プレゼンテーションロジック

これは、モデルの状態と使用可能なアクションをユーザーに表示するプログラムのロジックです。彼女は、表示する必要があるモデルと呼び出すことができるビジネスロジックを知っています。

データアクセスロジック

これは、モデルの状態を保存およびロードする方法を知っているプログラムのロジックです。彼女はモデルと、データベースからのデータをどのように取り込むことができるかを知っています。

アプリケーションロジック

これは、接着剤のようにすべてを結び付けるロジックです。彼女はビジネスロジック、データアクセスロジック、およびプレゼンテーションロジックについて知っています。彼女はそれらを結びつけ、助け、お互いの相互作用を確立します。

この分離に準拠すると、アプリケーションの変更が簡単になります。レイヤーの相互理解が少なく、ロジックが相互に隠されているほど、変更が容易になります。実際、プレゼンテーションロジックの違いは何ですか、モデルの状態はどこにどのように保存されますか?また、その逆に、データストレージのロジックとの違いは何ですか、このデータはインターフェイスでどこでどのように表示されますか?結局のところ、それらを表示するにはいくつかの方法があり、それらは互いに大きく異なる可能性があります。同時に、モデルは、アプリケーションがその状態を保存する方法や、この状態が画面に表示される方法を気にしません。他のレイヤーはプログラムの中心部分であるため、モデルまたはビジネスロジックの変更のみが他のレイヤーに影響を与えます。



2掾


— ! – . – !
— , , . – .
— … – . — , .
— , – . – , .., , .
— ? – . – , , .
.


互いの変更を確認するには、ユーザーはモデルの同じ状態で作業する必要があります。モデルの状態はデータベースに保存されるため、これはユーザーが共通のデータベースを使用する必要があることを意味します。原則として、すべてのユーザーのプログラムがアクセスできる別のコンピューターに配置されます。

これで、同時に複数のユーザーがモデルの同じ状態を見て変更できます。しかし、別の問題が登場しました- 競争アクセスの問題です。さまざまな方法で解決できます。最も一般的な方法は、リソースに変更を加える前に、リソースをロックしてアクセス専用にすることです。これを使用します。これを行うには、プログラム全体を注意深く調べ、ロックを追加する必要がある場所を理解する必要があります。

タスクが少数のリソースを短時間ロックする必要がある場合に適しています。大量のリソースをブロックし、長時間これを行う必要がある場合、それは悪いことです。そのような場合、別のアプローチを試すことができます。

プログラムを2台のコンピューターに物理的に分割した後、クライアントサーバー(2層)アーキテクチャを取得しました。

この物理的な分離は、さらに技術的な問題を追加します。まず、ネットワーク接続が切断される可能性があり、これは追加の障害ポイントになります。これは、ネットワークの切断またはサーバーのアクセス不能エラーを正しく処理するために、アプリケーションを開発する際に考慮する必要があります。

第二に、ネットワークを介したデータ伝送にはより多くの時間が必要であり、これも考慮する必要があります。一方では、データ取得を高速化するために、データをクライアントにキャッシュできます。一方、変更は継続的に送信されるのではなく、タスクの一部として蓄積され、単一のリクエストで送信されます。

N幤


-そして、私がタブレットから街を制御するのはまだ普通です。なぜなら、私が仕事から家に帰る間、彼が何かを数える前に眠りに落ちるような複雑な計算があるからです。 -ユーザーの一人に尋ねた。
-そして、すべてのアクションにアクセスできるわけではない新しいユーザーが必要です。 -別のユーザーが言った。
開発者の目には馴染みのある外観が登場しました...


新しい問題を解決するには、プログラムのビジネスロジックをユーザーコンピューターから、この専用のコンピューター(通常はアプリケーションサーバーと呼ばれる)に転送する必要があります。

レイヤーへの論理のきちんとした分割は、私たちの手にかかっています。プレゼンテーションロジックとビジネスロジックは既に完全に分離されていたため、残されたのはそれらを物理的に分離し、それらの相互作用のための追加のアプリケーションロジックを作成することだけでした。私たちのシッククライアント大幅にビジネスロジックを含んで、「シンナー」となるシンクライアント。

短所として、クライアント/サーバーアプローチの場合のように、ネットワークの問題の可能性と、ビジネスロジックとプレゼンテーションロジックの物理的な分離によるアプリケーションの速度の低下が追加されます。さらに、変更には変更がより困難になるという事実が含まれます。データ転送に必要なコードを含む、より多くのコードに影響します アプリケーションサーバーとクライアントの間。


2として3


実際、上記の問題を解決するために、3層アーキテクチャを使用する必要はありません。2層アーキテクチャでこれらの問題を解決するには、ビジネスロジックをデータベースに移動して、クライアントを薄くします。このアプローチには長所と短所があります。

特筆すべきは、ビジネスロジックとモデルの状態が物理的に分離されておらず、速度とフォールトトレランスの両方にプラスの効果があることです。マイナスのうち、データベースで最も重要なことは、ビジネスロジックをサポートするのではなく、データを保存および取得できることです。したがって、可能性の言語が、使用しますデータベースでは、最新の高レベル言語よりも機能が劣っています。また、多種多様なフレームワークを使用する通常の機能はありません。これらはすべて、柔軟なコードを書く能力とコストに影響します。

もう1つの欠点は、スケーリングする能力です。負荷の高い操作に対応するアプリケーションサーバーのスケーリングは、データベースよりも簡単です。これらの側面を考慮すると、ビジネスロジックに個別のリンクをアプリケーションサーバーとして使用する方が、データベースに保持するよりも安価であることが多いことがわかります。


サービス


— - . – , – , – . – - , ? – .
— – – , ? – .


最初の段階で十分な時間を費やし、対象分野を十分に理解していたため、サービスのグループを選び出すことができました。たとえば、交通制御サービス、エネルギー管理サービス、または建物管理サービスです。これらのサービスはすべて仮想都市の一部ですが、ほとんど接続されておらず、ほとんど接続されていません。したがって、プログラムコードは同じ方法で記述され、サービスごとに分類されています。これを実現するために、各サービスに必要なパブリックメソッドセットを特定し、他のすべてを隠しています。したがって、サービスを「ブラックボックス」に変えました。これは、メソッドアクセス修飾子、インターフェイス、抽象クラスなどのさまざまな言語ツールを使用して簡単に実現できました。


接続性が低いため、ほとんどの場合、これらのサービスはすべて互いに独立して変更できます。しかし、なぜなら物理的には、これらのサービスはすべて1つのプログラムであり、そのような変更をクライアントが個別にインストールすることはできません。サービスが頻繁に変更され、そのような変更をできるだけ早くクライアントにインストールする必要がある場合、これはあまり便利ではありません。他のサービスに強制的に依存しているため、避けたいと思います。

この問題に対処するために、プログラムを物理的にいくつかの部分に分割しました。このような物理的な分離は、さまざまなファイル(ライブラリ)へのサービスの割り当てです。これにより、プログラムを複数のファイル(メインの実行可能ファイルと一連のライブラリ)に分割できました。

この物理的な分離により、サービスを個別に更新することが可能になります。これは、サービスライブラリの単純な置き換えです。サービスは物理的に分離されているという事実にもかかわらず、非常に便利なアプリケーションの一般的なロジックを引き続き使用し、同じプロセスに存在します。これにより、単一のアプリケーションのすべての機能を使用できます。さらに、この明示的な分離により、一部の開発者は特定のサービスでのみ作業できます。



プログラム全体の「クラッシュ」を回避するために、サービスの動作中に発生するさまざまなエラーを処理するメカニズムをプログラムに追加します。これにより、一部のサービスが失敗してもプログラムは動作し続けます。

ただし、そのような安定性が十分でない場合は、サービスを異なるプロセスに分割できます。これを行うには、アプリケーションロジックを変更して、プロセス間通信を何らかの便利 な方法でサポートする必要があります。

このアプローチの利点は、個々のサービスごとに独自のテクノロジーセットを使用できることにも起因します。もちろん、これは開発者にとって素晴らしい餌です。ただし、この場合、開発者の互換性がなくなり、欠点になる可能性があります。

同様の場合のように、このアプローチの欠点には、相互作用の速度の低下に関連する問題や、プロセス間でのデータ転送の中断の可能性が含まれます。また、ビジネスロジックを分離すると、たとえば分散トランザクションを使用するなど、複数のサービスを同時に含むタスクでデータの整合性を維持する必要があるという形で、追加の問題が生じることもあります。


-聞いて、なぜ彼らはこれらのマイクロサービスでそんなに摩耗しているのですか?
「開発者は、ビジネスロジックを物理的に分離すると、疎結合で簡単に交換可能な安定したモジュールを作成できると考えています。
「何かが以前にそれらを悩ませていたかのように...


物理的な分離はいくつかの技術的な問題を解決しますが、代わりに他の問題をもたらします。」もちろん、最初の技術的な問題を解決する必要がありますが、それらを解決する利点が新しい問題の出現の欠点を超えるかどうかを常に検討する価値があります。

プログラム部分の物理的な分離が発生するたびに、この分離を隠すインフラストラクチャ層を実装する必要があります。はい、技術的な観点から言うと、このようなインフラストラクチャの作成はもちろん、純粋に技術的な興味深い作業であるため、プログラマは喜んでそれを引き受けます。しかし、これで問題が実際に解決されない場合、利益をもたらすことなく、プログラムのコストと複雑さを増加させるだけです。

ツール


-そして、なぜ、データベースに通常のクエリを作成する代わりに、両方のテーブルをアプリケーションサーバーにプルし、それらを英雄的に結合し、結果をフィルタリングして必要なデータを取得しますか?
-手にハンマーしか持っていない場合、すべてのタスクは釘のように見えます。


言語、ライブラリ、フレームワーク、ユーティリティ-これは、開発者が作業の過程で使用するものすべてです。これはすべて、技術的な問題を解決するコストを最小限に抑えるために必要であり、プロジェクトに固有のビジネス上の問題を解決するための時間をより多く残します。したがって、労働生産性に直接影響するため、ツールの正しい選択が重要です。大ざっぱに言えば、あなたはのこぎりで半日の間何かを切ることができます、または、あなたは電気ジグソーをとることができます-強打して、あなたは終わりました!

ツールはさまざまであり、さまざまな目的に合わせて設計されているため、それらの正しいアプリケーションも同様に重要です。もちろん、クローバーで釘を打つことは可能です(そして、これは拳よりも便利です)が、それでも、ハンマーほど簡単で便利ではありません。

しかし、開発者の間では、「ここで発明されていない」という非常に悪いシンドロームがあります。その意味は、特定の範囲のタスクを解決するための既製のツールを使用する代わりに、開発者が独自の発明を開始するということです。これは、独自のプラットフォームを作成したいという要望に最も強く現れます。

このようなツールの作成は純粋に技術的なタスクであり、ビジネスの主題分野を理解する必要がないため、開発者はこれを行うのが大好きです。しかし、開発者がビジネス上の問題を解決することを目的とする場合、逆に、サードパーティのツールを使用することで、彼の人生は楽に簡素化されます。

理論と実践


理論的には、理論と実践はまったく同じです。実際には、ありません。

理論的には、もちろん、すべてが美しいですが、実際には、すべてが完全に間違っています。技術的知識、ドメイン知識、時間、または人の不足-これらはすべて、コードに望ましくない問題が現れ、プログラムの品質に影響を与えます。

実際には、開発者はしばしば品質と速度の妥協に直面します。顧客を満足させ、納期を守ろうとすると、急いですべてを行うことになります。これは、最終的には逆の効果をもたらします。急いで問題を解決すると、悪い解決策を作成せざるを得なくなり、最終的には大きな雪だるまのように蓄積して、プロジェクト全体をあなたの下に埋めようとします。そのため、サポートと修正のコストはさらに高くなり、特に同じ変更のコストが高くなった理由を顧客に説明する際に、問題を悪化させるだけです。

このような悪い決定はどのプロジェクトでもほとんど避けられないため、リファクタリングの時間を見つけることが非常に重要です。

まとめ


結局、すべてはお金に帰着し​​ます。開発とサポートのコストが安いほど、プログラムを作成することの収益性は高くなります。したがって、すべてのアプローチ/ツール/テンプレート/パラダイムなど 開発とサポートプロセスのコストを削減するという1つのことに焦点を当てています。

しかし、開発も創造的なプロセスです。信じられないほど多数のパスが同じ問題を解決することになり、それぞれに長所と短所があります。

そして、開発者だけがこのパスがどれほど良いかを判断します。したがって、ソフトウェア開発では、人が最も重要なリソースです。

Source: https://habr.com/ru/post/J263025/


All Articles