私は従業員を管理するためのプログラムを作成しようとしています。ただし、Employee
クラスの設計方法がわかりません。私の目標は、Employee
オブジェクトを使用してデータベース上で従業員データを作成および操作できるようにすることです。
私が考えた基本的な実装は、この単純なものでした:
class Employee
{
// Employee data (let's say, dozens of properties).
Employee() {}
Create() {}
Update() {}
Delete() {}
}
この実装を使用すると、いくつかの問題が発生しました。
ID
はデータベースによって提供されるため、新しい従業員を説明するためにオブジェクトを使用すると、まだ保存するID
はありませんが、既存の従業員を表すオブジェクトはID
を持ちます。だから私は時々オブジェクトを説明し、時々説明しないプロパティがあります(これは違反していることを示す可能性があります [〜#〜] srp [〜#〜] ?新しいクラスを表すために同じクラスを使用しているためおよび既存の従業員...)。Create
メソッドはデータベースに従業員を作成することになっていますが、Update
とDelete
は既存の従業員に作用することになっています(ここでも、 [〜 #〜] srp [〜#〜] ...).Employee
オブジェクトの何十ものパラメータ?Update
はどのように機能しますか?プロパティを取得してデータベースを更新しますか?または、「古い」オブジェクトと「新しい」オブジェクトの2つのオブジェクトを取得し、それらの違いでデータベースを更新しますか? (私は答えがクラスの可変性についての答えと関係があると思います)。id
パラメータを使用してデータベースから従業員データをフェッチし、プロパティに入力しますか?ご覧のとおり、私は頭が少し混乱しています。私はとても混乱しています。そのようなクラスがどのように見えるべきかを理解するのを手伝っていただけませんか?
このような頻繁に使用されるクラスが一般的にどのように設計されているかを理解するためだけに、私は意見を望まないことに注意してください。
これはあなたの質問の下での私の最初のコメントのより整形式の書き起こしです。 OPが対処する質問への回答は、この回答の下部にあります。また、同じ場所にある重要事項も確認してください。
現在説明しているSipoは、 アクティブレコード と呼ばれるデザインパターンです。すべての場合と同様に、これもプログラマーの間でその場所を見つけましたが、1つの単純な理由であるスケーラビリティーのために repository および data mapper パターンのために破棄されました。
つまり、アクティブレコードはオブジェクトであり、次のことを行います。
現在の設計に関するいくつかの問題に対処し、設計の主要な問題は最後の6番目のポイントで解決されます(最後ですが、私はそう思います)。コンストラクターを設計しているクラスがあり、コンストラクターが何をすべきかさえわからない場合、クラスはおそらく何か間違ったことをしています。それはあなたの場合に起こりました。
ただし、エンティティ表現とCRUDロジックを2つ(またはそれ以上)のクラスに分割することで、設計の修正は実際には非常に簡単です。
これはあなたのデザインが今どのように見えるかです:
Employee
-従業員の構造(その属性)およびエンティティを変更する方法(変更可能な方法を採用する場合)に関する情報が含まれ、Employee
エンティティのCRUDロジックが含まれ、返すことができますEmployee
オブジェクトのリスト、従業員を更新するときにEmployee
オブジェクトを受け入れ、getSingleById(id : string) : Employee
のようなメソッドを介して単一のEmployee
を返すことができますうわー、クラスは巨大なようです。
これは提案された解決策です:
Employee
-従業員の構造(その属性)とエンティティを変更する方法に関する情報が含まれます(変更可能な方法を採用する場合)EmployeeRepository
-Employee
エンティティのCRUDロジックを含み、Employee
オブジェクトのリストを返すことができ、従業員を更新する場合はEmployee
オブジェクトを受け入れます。 getSingleById(id : string) : Employee
のようなメソッドを介して単一のEmployee
を返すことができます懸念の分離 について聞いたことがありますか?いいえ、今します。これは、単一の責任の原則のそれほど厳密ではないバージョンであり、クラスは実際には1つの責任しか持つべきではない、またはボブおじさんが言うように:
モジュールには、変更する理由が1つだけあるはずです。
十分に丸みを帯びたインターフェイスを備えた2つのクラスに初期クラスを明確に分割できた場合、おそらく初期クラスの処理が多すぎることは明らかでした。
リポジトリパターンの優れた点は、データベース(ファイル、noSQL、SQL、オブジェクト指向など)の間に中間層を提供する抽象化として機能するだけでなく、具体的である必要もないことです。クラス。多くのOO言語では、インターフェイスを実際のinterface
(またはC++の場合は純粋な仮想メソッドを持つクラス)として定義し、複数の実装を持つことができます。
これにより、リポジトリが実際の実装であるかどうかの決定が完全に撤廃され、実際にはinterface
キーワードを使用した構造体に依存するだけで、インターフェースに依存しています。そして、リポジトリはまさにそれであり、それはデータ層の抽象化、すなわちドメインへのデータのマッピングとその逆のマッピングのための空想的な用語です。
それを(少なくとも)2つのクラスに分離するもう1つの素晴らしい点は、Employee
クラスが独自のデータを明確に管理し、それを非常にうまく実行できることです。他の難しいことを処理する必要がないためです。
質問6:では、新しく作成されたEmployee
クラスでコンストラクターは何をすべきですか?簡単です。引数を取り、それらが有効であるかどうか(年齢がおそらく負であったり、名前が空であってはならないなど)を確認したり、データが無効な場合にエラーを発生させたり、渡された検証で引数をプライベート変数に割り当てたりする必要がありますエンティティの。どうすればいいのかわからないので、データベースと通信できなくなりました。
質問4:まったく答えることはできません。一般的にではありません。答えは必要なものに大きく依存するためです。
質問5:肥大化したクラスを2つに分離したので、Employee
クラスで複数の更新メソッドを直接使用できます。 changeUsername
、markAsDeceased
、これはEmployee
クラスのデータを操作しますRAM内のみおよび次に、registerDirty
などのメソッドを 作業単位 パターンからリポジトリクラスに導入できます。これにより、このオブジェクトがプロパティを変更したことをリポジトリに通知し、 commit
メソッドを呼び出した後で更新されます。
明らかに、更新の場合、オブジェクトはIDを持っている必要があるため、すでに保存されています。これが検出され、基準が満たされない場合にエラーが発生するのは、リポジトリの責任です。
質問3:作業単位パターンを使用する場合、create
メソッドはregisterNew
になります。 。そうでない場合は、代わりにsave
と呼びます。リポジトリの目的は、ドメインとデータレイヤーの間の抽象化を提供することです。このため、このメソッド(registerNew
またはsave
)がEmployee
オブジェクトであり、リポジトリインターフェースを実装するクラス次第です。これらの属性はエンティティから取得することを決定します。オブジェクト全体を渡す方がよいため、多くのオプションパラメータを指定する必要はありません。
質問2:両方のメソッドがリポジトリインターフェイスの一部になり、単一責任の原則に違反しなくなりました。リポジトリの役割は、Employee
オブジェクトにCRUD操作を提供することです。これは、リポジトリが行うことです(読み取りと削除の他に、CRUDは作成と更新の両方に変換されます)。もちろん、EmployeeUpdateRepository
などを使用してリポジトリをさらに分割することもできますが、それが必要になることはまれであり、通常、単一の実装にすべてのCRUD操作を含めることができます。
質問1:単純なEmployee
クラスになりました(他の属性の中で)IDを持ちます。 IDが入力されているか空であるか(またはnull
)は、オブジェクトがすでに保存されているかどうかによって異なります。それでも、IDはエンティティが所有する属性であり、Employee
エンティティの責任はその属性を処理することであり、したがってIDを処理します。
エンティティがIDを持っているかどうかは、永続化ロジックを実行するまで、通常は問題になりません。質問5の回答で述べたように、すでに保存されているエンティティを保存したり、IDなしでエンティティを更新したりしていないことを検出するのは、リポジトリの責任です。
懸念の分離は非常に重要ですが、実際に機能リポジトリレイヤーを設計することは非常に退屈な作業であり、私の経験では、アクティブレコードアプローチよりも正しく理解するのが少し難しいことに注意してください。しかし、最終的にははるかに柔軟でスケーラブルなデザインになり、それは良いことかもしれません。
最初に、概念的な従業員のプロパティを含む従業員構造体を作成します。
次に、一致するテーブル構造を持つデータベースを作成します。たとえば、mssql
次に、必要なさまざまなCRUD操作を使用して、そのデータベースEmployeeRepoMsSqlの従業員リポジトリを作成します。
次に、CRUD操作を公開するIEmployeeRepoインターフェイスを作成します。
次に、Employee構造体を、IEmployeeRepoの構築パラメーターを持つクラスに展開します。必要なさまざまなSave/Deleteなどのメソッドを追加し、注入されたEmployeeRepoを使用してそれらを実装します。
Idと一致する場合は、GUID=を使用することをお勧めします。これは、コンストラクターのコードを介して生成できます。
既存のオブジェクトを操作するために、コードはUpdateメソッドを呼び出す前に、リポジトリを介してデータベースからオブジェクトを取得できます。
別の方法として、CRUDメソッドをオブジェクトに追加せずに、眉をひそめた(ただし私の考えでは上位の)Anemic Domain Objectモデルを選択して、更新/保存/削除するオブジェクトをリポジトリに単に渡すことができます。
不変性は、パターンとコーディングスタイルに依存する設計上の選択です。すべての機能を使用する場合は、同様に不変であるようにしてください。しかし、不確かな場合は、おそらく変更可能なオブジェクトを実装する方が簡単です。
Create()の代わりに、Save()を使用します。作成は不変性の概念で機能しますが、まだ「保存」されていないオブジェクトを構築できると便利です。たとえば、従業員オブジェクトを入力して、前にいくつかのルールを再度確認できるUIがあります。データベースに保存します。
*****サンプルコード
public class Employee
{
public string Id { get; set; }
public string Name { get; set; }
private IEmployeeRepo repo;
//with the OOP approach you want the save method to be on the Employee Object
//so you inject the IEmployeeRepo in the Employee constructor
public Employee(IEmployeeRepo repo)
{
this.repo = repo;
this.Id = Guid.NewGuid().ToString();
}
public bool Save()
{
return repo.Save(this);
}
}
public interface IEmployeeRepo
{
bool Save(Employee employee);
Employee Get(string employeeId);
}
public class EmployeeRepoSql : IEmployeeRepo
{
public Employee Get(string employeeId)
{
var sql = "Select * from Employee where Id=@Id";
//more db code goes here
Employee employee = new Employee(this);
//populate object from datareader
employee.Id = datareader["Id"].ToString();
}
public bool Save(Employee employee)
{
var sql = "Insert into Employee (....";
//db logic
}
}
public class MyADMProgram
{
public void Main(string id)
{
//with ADM don't inject the repo into employee, just use it in your program
IEmployeeRepo repo = new EmployeeRepoSql();
var emp = repo.Get(id);
//do business logic
emp.Name = TextBoxNewName.Text;
//save to DB
repo.Save(emp);
}
}
デザインのレビュー
Employee
は、実際には、データベースで永続的に管理されるオブジェクトの一種のプロキシです。
したがって、IDはデータベースオブジェクトへの参照であるかのように考えることをお勧めします。このロジックを念頭に置いて、非データベースオブジェクトの場合と同じように設計を続行できます。IDを使用すると、従来の構成ロジックを実装できます。
Employee
はまだ作成されていないか、削除されている可能性があります。また、オブジェクトのステータスを管理する必要があります。例えば:
これを念頭に置いて、次のオプションを選択できます。
_class Employee
{
...
Employee () {} // Initialize an empty Employee
Load(IDType ID) {} // Load employee with known ID from the database
bool Create() {} // Create an new employee an set its ID
bool Update() {} // Update the employee (can ID be changed?)
bool Delete() {} // Delete the employee (and reset ID because there's no corresponding ID.
bool isClean () {} // true if ID empty or if all properties match database
}
_
オブジェクトのステータスを信頼できる方法で管理できるようにするには、プロパティをプライベートにしてカプセル化を強化し、ステータスを更新するゲッターとセッターを介してのみアクセスを許可する必要があります。
あなたの質問
IDプロパティはSRPに違反していないと思います。その単一の責任は、データベースオブジェクトを参照することです。
従業員は全体としてSRPに準拠していません。これは、データベースとのリンクだけでなく、一時的な変更の保持、およびそのオブジェクトで発生するすべてのトランザクションも担当するためです。
別の設計では、フィールドにアクセスする必要がある場合にのみロードされる別のオブジェクトに変更可能なフィールドを保持することもできます。
コマンドパターン を使用して、従業員にデータベーストランザクションを実装できます。この種の設計は、データベース固有のイディオムとAPIを分離することにより、ビジネスオブジェクト(従業員)と基盤となるデータベースシステム間の分離を容易にします。
ビジネスオブジェクトが進化し、これをすべて維持するのが非常に困難になる可能性があるため、Create()
に数十のパラメーターを追加しません。そして、コードは読めなくなります。ここには2つの選択肢があります。データベースで従業員を作成するために絶対に必要な最小限のパラメーターセット(4つ以下)を渡し、更新によって残りの変更を実行するか、ORちなみに、あなたのデザインでは、あなたがすでに選択していることを理解しています:my_employee.Create()
。
クラスは不変でなければなりませんか?上記の説明を参照してください。私は不変のIDを選択しますが、不変の従業員は選択しません。従業員は実際の生活の中で進化します(新しい職位、新しい住所、新しい夫婦の状況、新しい名前までも...)。少なくともビジネスロジックレイヤーでは、この現実を念頭に置いて作業するほうが簡単で自然だと思います。
更新のコマンドと(GUI?)の個別のオブジェクトを使用して、必要な変更を保持することを検討している場合は、古い/新しいアプローチを選択できます。その他の場合はすべて、変更可能なオブジェクトを更新することを選択します。注意:更新によってデータベースコードがトリガーされる可能性があるため、更新後もオブジェクトが実際にDBと同期していることを確認する必要があります。
コンストラクターでDBから従業員をフェッチすることは良い考えではないと思います。フェッチが失敗する可能性があり、多くの言語では、失敗した構築に対処するのが難しいからです。コンストラクターは、オブジェクト(特にID)とそのステータスを初期化する必要があります。