`Employee`クラスはどのように設計されるべきですか?


11

私は従業員を管理するためのプログラムを作成しようとしています。ただし、Employeeクラスの設計方法を理解することはできません。私の目標は、Employeeオブジェクトを使用してデータベース上の従業員データを作成および操作できるようにすることです。

私が考えた基本的な実装は、この単純な実装でした:

class Employee
{
    // Employee data (let's say, dozens of properties).

    Employee() {}
    Create() {}
    Update() {}
    Delete() {}
}

この実装を使用すると、いくつかの問題が発生しました。

  1. ID従業員のは、私は、新しい従業員を記述するためのオブジェクトを使用するのであれば、何があるだろう、データベースによって与えられていないID既存の従業員を表すオブジェクトはながら、まだ格納するだろう持っていますID。そのため、オブジェクトを説明するプロパティと、オブジェクトを説明しないプロパティがあります(SRPに違反していることを示すことができますか?新しいクラスと既存の従業員を表すのに同じクラスを使用しているため...)。
  2. Create一方、この方法は、データベース上の従業員を作成することになっているUpdateとは、Delete既存の従業員(ここでも、上で動作するようになっているSRP ...)。
  3. 「作成」メソッドにはどのようなパラメーターが必要ですか?すべての従業員データまたはEmployeeオブジェクトのための数十のパラメーター?
  4. クラスは不変であるべきですか?
  5. どのようにUpdate動作しますか?プロパティを取得してデータベースを更新しますか?または、「古い」オブジェクトと「新しい」オブジェクトの2つのオブジェクトを取り、それらの違いでデータベースを更新しますか?(答えは、クラスの可変性に関する答えと関係があると思います)。
  6. コンストラクターの責任は何ですか?必要なパラメーターは何ですか?idパラメーターを使用してデータベースから従業員データを取得し、プロパティを設定しますか?

それで、あなたが見ることができるように、私は私の頭の中に少し混乱を持っています、そして、私は非常に混乱しています。そのようなクラスがどのように見えるべきかを理解してください。

このような頻繁に使用されるクラスが一般的にどのように設計されているかを理解するためだけに、意見を望まないことに注意してください。


3
SRPの現在の違反は、エンティティとCRUDロジックの両方を表すクラスがあることです。それを分離すると、そのCRUD操作とエンティティ構造は異なるクラスになり、12はSRPを壊しません。3.取る必要がありEmployee、抽象化を提供することを目的とする質問4.5.は一般unanswerableあり、ニーズに依存し、次の2つのクラスに構造およびCRUD操作を分離した場合、それはかなり明確だ、のコンストラクタは、Employeeデータをフェッチすることはできませんdbから、それで6に
アンディ

@DavidPacker-ありがとう。それを答えていただけますか?
シポ

5
繰り返しますが、あなたの俳優がデータベースに手を伸ばすことはありません。これを行うと、コードがデータベースに緊密に結合され、テストが非常に難しくなります(手動テストでも難しくなります)。リポジトリパターンを調べます。少し考えてみてくださいUpdate、あなたは従業員ですか、それとも従業員レコードを更新しますか?あなたがやるEmployee.Delete()か、ありませんかBoss.Fire(employee)
ラバーダック

1
すでに言及されていることとは別に、従業員を作成するには従業員が必要であることはあなたにとって理にかなっていますか?アクティブなレコードでは、Employeeを更新してから、そのオブジェクトでSaveを呼び出す方が意味があります。それでも、ビジネスロジックと独自のデータ永続性を担当するクラスができました。
コチェッセ氏16

回答:


10

これは、あなたの質問の下での私の最初のコメントのより整形式の転写です。OPが対応する質問への回答は、この回答の下部に記載されています。また、同じ場所にある重要なメモを確認してください。


現在説明しているSipoは、Active recordと呼ばれるデザインパターンです。すべてと同様に、これもプログラマーの間でその位置を見つけましたが、1つの単純な理由、スケーラビリティのためにリポジトリデータマッパーパターンを支持して破棄されました。

つまり、アクティブなレコードはオブジェクトであり、次のことを行います。

  • ドメイン内のオブジェクトを表します(ビジネスルールを含み、ユーザー名を変更できるかどうかなど、オブジェクトに対する特定の操作を処理する方法を知っています)、
  • エンティティを取得、更新、保存、削除する方法を知っています。

現在の設計に関するいくつかの問題に対処し、設計の主な問題は最後の6番目の点で対処します(最後ではありますが、私は推測します)。コンストラクタを設計するクラスがあり、コンストラクタが何をすべきかさえ知らない場合、クラスはおそらく何か間違ったことをしているでしょう。それはあなたの場合に起こりました。

しかし、エンティティー表現とCRUDロジックを2つ(またはそれ以上)のクラスに分割することにより、設計の修正は実際には非常に簡単です。

これは、現在の設計の外観です。

  • Employee-従業員の構造(その属性)およびエンティティを変更する方法に関する情報(変更可能な方法を選択した場合)、EmployeeエンティティのCRUDロジックが含まれ、Employeeオブジェクトのリストを返すことができEmployee、必要に応じてオブジェクトを受け入れる従業員を更新し、次のEmployeeようなメソッドを介してシングルを返すことができますgetSingleById(id : string) : Employee

うわー、クラスは巨大なようです。

これが提案されたソリューションになります。

  • Employee -従業員の構造(その属性)およびエンティティを変更する方法に関するメソッドが含まれています(変更可能な方法を選択した場合)
  • EmployeeRepository- EmployeeエンティティのCRUDロジックが含まれ、EmployeeオブジェクトのリストをEmployee返すことができ、従業員を更新するときにオブジェクトを受け入れ、次のEmployeeようなメソッドで単一を返すことができますgetSingleById(id : string) : Employee

懸念分離について聞いたことがありますか?いいえ、あなたは今します。これは、単一責任原則の厳密性の低いバージョンであり、クラスには実際に1つの責任のみを持たせるか、ボブおじさんが次のように述べています。

モジュールには、変更する唯一の理由が必要です。

最初のクラスを2つのクラスに明確に分割でき、それらのインターフェイスがまだ丸みを帯びている場合は、おそらく最初のクラスが多すぎて多すぎることは明らかです。

リポジトリパターンの優れた点は、データベース(何でも、ファイル、noSQL、SQL、オブジェクト指向のもの)の中間層を提供する抽象化として機能するだけでなく、具体的である必要さえありません。クラス。多くのオブジェクト指向言語では、インターフェイスを実際のinterface(またはC ++の場合は純粋な仮想メソッドを持つクラス)として定義し、複数の実装を持つことができます。

これにより、リポジトリが実際の実装であるかどうかの決定が完全に解除されますinterface。これは、キーワードを持つ構造に実際に依存することにより、単にインターフェイスに依存しているだけです。そして、リポジトリはまさにそれです。これは、データ層の抽象化、つまりデータをドメインにマッピングしたり、その逆を行ったりするための、凝った用語です。

(少なくとも)2つのクラスに分けることのもう1つの素晴らしい点は、Employeeクラスが独自のデータを明確に管理し、非常にうまく処理できるようになったことです。

質問6:では、コンストラクターは新しく作成されたEmployeeクラスで何をすべきでしょうか?それは単純だ。引数を取る必要があり、それらが有効かどうか(年齢がおそらく負であったり、名前が空であってはならない)をチェックし、データが無効で、検証が引数をプライベート変数に割り当てる場合にエラーを発生させる必要がありますエンティティの。データベースと通信することはできません。これを行う方法がわからないからです。


質問4:答えは、あなたが本当に必要としているものに大きく依存しているため、一般的には答えられません。


質問5:肥大化したクラスを2つに分離したEmployeeので、のようchangeUsernameにクラスに複数の更新メソッドを直接持つことができますmarkAsDeceased。これは、RAMのみEmployeeクラスのデータを操作し、リポジトリクラスへの作業単位パターン。これにより、このオブジェクトがプロパティを変更し、メソッドを呼び出した後に更新する必要があることをリポジトリに通知します。registerDirtycommit

明らかに、更新の場合、オブジェクトには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なしでエンティティを更新しようとしていないことを検出するのは、リポジトリの責任です。


重要な注意点

関心の分離は素晴らしいものの、実際に機能的なリポジトリ層を設計するのは非常に退屈な作業であり、私の経験では、アクティブレコードアプローチよりも適切に取得するのが少し難しいことに注意してください。しかし、最終的にははるかに柔軟でスケーラブルなデザインになります。これは良いことかもしれません。


うーん、私の答えと同じですが、「エッジの効いた」色合いではあり
ユアン

2
@Ewan私はあなたの答えに反対票を投じませんでしたが、なぜそうなるかもしれませんか。OPの質問のいくつかに直接答えることはなく、あなたの提案のいくつかは根拠のないようです。
アンディ

1
素晴らしく包括的な答え。心配の分離で頭の釘を打ちます。そして、完璧な複雑な設計と妥協案の間の重要な選択を指摘する警告が好きです。
クリストフ

確かに、あなたの答えは優れています
ユアン

新しい従業員オブジェクトを最初に作成するとき、IDに値はありません。idフィールドはnull値のままにすることができますが、従業員オブジェクトが無効な状態になりますか????
スーザンサ7

2

最初に、概念的な従業員のプロパティを含む従業員構造体を作成します。

次に、たとえば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);

    }
}

1
貧血領域モデルは、CRUDロジックとはほとんど関係がありません。これは、ドメイン層に属しますが、機能を持たないモデルであり、すべての機能はサービスを通じて提供され、このドメインモデルはパラメーターとして渡されます。
アンディ

まさにこの場合、レポジトリはサービスであり、機能はCRUD操作です。
ユアン

@DavidPackerは、Anemic Domain Modelは良いことだと言っていますか?
candied_orange

1
@CandiedOrangeコメントで私の意見を表明していませんが、いいえ、1つの層がビジネスロジックのみを担当する層にアプリケーションを移動することにした場合、私はFowler氏と一緒に、貧弱なドメインモデルです実際にはアンチパターンです。メソッドをクラスに直接追加できるのに、なぜメソッドをUserUpdate備えたサービスが必要なのですか。そのためのサービスを作成するのはナンセンスです。changeUsername(User user, string newUsername)changeUsernameUser
アンディ

1
この場合、モデルにCRUDロジックを配置するためだけにリポジトリを挿入することは最適ではないと思います。
ユアン

1

デザインのレビュー

あなたは、Employee実際にデータベースに永続的に管理対象オブジェクトのプロキシの一種です。

したがって、IDをデータベースオブジェクトへの参照であるかのように考えることをお勧めします。このロジックを念頭に置いて、非データベースオブジェクトの場合と同様に設計を続行できます。IDにより、従来の構成ロジックを実装できます。

  • IDが設定されている場合、対応するデータベースオブジェクトがあります。
  • IDが設定されていない場合、対応するデータベースオブジェクトEmployeeはありません。まだ作成されていないか、削除された可能性があります。
  • 従業員とメモリにまだロードされていないデータベースレコードを削除するための関係を開始するには、何らかのメカニズムが必要です。

また、オブジェクトのステータスを管理する必要があります。例えば:

  • 作成またはデータ取得によって従業員がまだDBオブジェクトにリンクされていない場合、更新または削除を実行できないはずです。
  • オブジェクトの従業員データはデータベースと同期していますか、それとも変更が行われていますか?

これを念頭に置いて、以下を選択できます。

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
}

オブジェクトのステータスを信頼性の高い方法で管理できるようにするには、プロパティをプライベートにし、ステータスを更新するゲッターとセッターを介してのみアクセスを許可することにより、より良いカプセル化を確保する必要があります。

あなたの質問

  1. IDプロパティはSRPに違反しないと思います。その唯一の責任は、データベースオブジェクトを参照することです。

  2. 従業員全体はSRPに準拠していません。データベースとのリンクだけでなく、一時的な変更を保持し、そのオブジェクトに発生するすべてのトランザクションも担当しているためです。

    別の設計では、フィールドにアクセスする必要がある場合にのみロードされる別のオブジェクトに変更可能なフィールドを保持します。

    コマンドパターンを使用して、従業員にデータベーストランザクションを実装できます。この種の設計では、データベース固有のイディオムとAPIを分離することにより、ビジネスオブジェクト(従業員)と基礎となるデータベースシステム間の分離を容易にします。

  3. Create()ビジネスオブジェクトが進化し、これをすべて維持するのが非常に難しくなる可能性があるため、私はに多数のパラメーターを追加しません。そして、コードは読めなくなるでしょう。ここには2つの選択肢があります。データベースで従業員を作成し、更新によって残りの変更を実行するために絶対に必要な最小限のパラメーターセット(4つ以下)を渡すか、オブジェクトを渡します。ところで、あなたのデザインでは、あなたがすでに選んだことを理解しています my_employee.Create()

  4. クラスは不変である必要がありますか?上記の説明を参照してください。元の設計番号 不変の従業員ではなく、不変のIDを選択します。従業員は実生活で進化します(新しい職位、新しい住所、新しい結婚の状況、新しい名前さえ...)。少なくともビジネスロジック層では、この現実を念頭に置いて作業する方が簡単で自然になると思います。

  5. 更新用のコマンドと、必要な変更を保持するための(GUI?)用の個別のオブジェクトの使用を検討する場合、古いアプローチと新しいアプローチを選択できます。それ以外の場合はすべて、可変オブジェクトの更新を選択します。重要:更新によってデータベースコードがトリガーされる可能性があるため、更新後もオブジェクトが実際にDBと同期していることを確認する必要があります。

  6. コンストラクターでDBから従業員を取得することはお勧めできません。取得が失敗する可能性があり、多くの言語では構築の失敗に対処するのが難しいからです。コンストラクターは、オブジェクト(特にID)とそのステータスを初期化する必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.