web-dev-qa-db-ja.com

貧血ドメインモデルからリッチドメインモデルへの移行

Anemic Domain Modelに従っていくつかの簡単なプロジェクトを実行しました。今、私はリッチドメインモデルで同様のタイプのプロジェクトを実行しようとしています。しかし、私は実装について混乱しています。以前は、ドメインモデルentityとそのすべてのロジックがServicesおよびpersistencerepositoryによって作成されました、データベースの永続化に関連するすべての機能が含まれていました。

これは私のUserモデルが今扱っているものです:

  public class User : BaseEntity
    {
        private readonly UserRepository _userRepo;
        private readonly PasswordHash _passwordHash;
        private readonly USER_DETAIL _userDetail;

        protected User() { }

        public User(UserRepository userRepo, PasswordHash passwordHash, USER_DETAIL userDetail) : base()
        {
            _userRepo = userRepo;
            _passwordHash = passwordHash;
            _userDetail = userDetail;
        }

        public User(UserDto user_dto)
        {
            this.is_owner = user_dto.is_owner;
            this.id = user_dto.user_id;
            this.email = user_dto.email;
            this.normalized_email = this.email.ToUpper();
            this.is_active = user_dto.is_active;
        }

        private string _password, _registrationToken;

        [DataType(DataType.EmailAddress)]
        [MaxLength(100)]
        [Required]
        public string email { get; set; }

        [MaxLength(100)]
        [DataType(DataType.EmailAddress)]
        [Required]
        public string normalized_email { get; set; }

        [MaxLength(250)]
        [Required]
        public string password
        {
            get => _password;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("Password cannot be empty.");
                }
                _password = value;
            }
        }

        [MaxLength(1000)]
        [Required]
        public string registration_token
        {
            get => _registrationToken;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException($"Registration token is required.");
                }
                _registrationToken = value;
            }
        }

        public bool is_active { get; set; } = true;

        public bool is_email_confirmed { get; set; } = false;

        public bool is_owner { get; set; } = false;

        public void disable()
        {
            is_active = false;
        }

        public void enable()
        {
            is_active = true;
        }

        public void markEmailAsConfirmed()
        {
            is_email_confirmed = true;
        }

        public User checkAuthenticity(string username, string password)
        {
            var user = _userRepo.getByUsername(username);
            if (user == null || !user.is_active || !user.is_email_confirmed)
            {
                return null;
            }

            if (!_passwordHash.ValidatePassword(password, user.password))
            {
                return null;
            }

            return user;
        }

        public void enable(ModificationDto dto)
        {
            try
            {
                var user = _userRepo.getById(dto.id) ?? throw new ItemNotFoundException($"User with the id {dto.id} doesnot exist.");

                user.enable();
                user.modified_date = DateTime.Now;
                user.modified_by = dto.modified_by;

                _userRepo.update(user);
            }
            catch (Exception)
            {
                throw;
            }
        }
        public void markEmailRegistered(string token)
        {
            try
            {
                var encryptedToken = _cryptography.Encrypt(token);

                var user = _userRepo.getByToken(encryptedToken) ?? throw new ItemNotFoundException($"Token didnot match.");

                if (user.is_email_confirmed)
                {
                    return;
                }

                user.markEmailAsConfirmed();

                _userRepo.update(user);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public void save()
        {
            try
            {
                bool isUsernameValid = checkNameValidity();

                if (!isUsernameValid)
                {
                    throw new DuplicateItemException("User with same name already exists.");
                }

                user.created_date = DateTime.Now;
                user.created_by = user_dto.created_by;

                user.password = _passwordHash.CreateHash(this.password);

                _userRepo.insert(user);
            }
            catch (Exception)
            {
                throw;
            }
        }

        public void update(UserDto user_dto)
        {
            try
            {

                User user = _userRepo.getById(user_dto.user_id) ?? throw new ItemNotFoundException($"User with the id {user_dto.user_id} doesnot exist.");

                bool isUsernameValid = checkNameValidity(user_dto);

                if (!isUsernameValid)
                {
                    throw new DuplicateItemException("User with same name already exists.");
                }

                _userAssembler.copy(user, user_dto);
                user.modified_by = user_dto.modified_by;
                user.modified_date = DateTime.Now;
                _userRepo.update(user);

                USER_DETAIL user_detail = new USER_DETAIL(user_dto);
                user_detail.saveOrUpdate();
            }
            catch (Exception)
            {
                throw;
            }
        }

        private bool checkNameValidity()
        {
            var userWithSameEmail = _userRepo.getByUsername(this.email);
            return userWithSameEmail == null || userWithSameEmail.id == this.id;
        }
    }

public class USER_DETAIL : BaseEntity
    {
        private readonly UserDetailRepository _userDetailRepo;
        private readonly FileHelper _fileHelper;

        private string _permanentAddress, _firstName, _lastName;
        private long _userId;

        protected USER_DETAIL() { }


        public USER_DETAIL(UserDetailRepository userDetailRepo, FileHelper fileHelper)
        {
            _userDetailRepo = userDetailRepo;
            _fileHelper = fileHelper;
        }

        public USER_DETAIL(UserDetailDto user_detail_dto)
        {
            this.first_name = user_detail_dto.first_name;
            this.last_name = user_detail_dto.last_name;
            this.user_id = user_detail_dto.user_id;
            this.permanent_address = user_detail_dto.permanent_address;
            this.temporary_address = user_detail_dto.temporary_address;
            this.primary_contact = user_detail_dto.primary_contact;
            this.secondary_contact = user_detail_dto.secondary_contact;
            this.image_path = user_detail_dto.image_path;
        }

        public USER_DETAIL(UserDto user_dto)
        {
            this.user_id = user_dto.user_id;
            this.first_name = user_dto.user_detail_dto.first_name;
            this.permanent_address = user_dto.user_detail_dto.permanent_address;
            this.temporary_address = user_dto.user_detail_dto.temporary_address;
            this.primary_contact = user_dto.user_detail_dto.primary_contact;
            this.secondary_contact = user_dto.user_detail_dto.secondary_contact;
            this.modified_by = user_dto.modified_by;
            this.modified_date = DateTime.Now;
        }

        public long user_id
        {
            get => _userId;
            set
            {
                if (value <= 0)
                {
                    throw new InvalidValueException("Invalid user id.");
                }
                _userId = value;
            }
        }

        [Required]
        [MaxLength(50)]
        public string first_name
        {
            get => _firstName;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("First name must be specified.");
                }
                _firstName = value;
            }
        }

        [Required]
        [MaxLength(50)]
        public string last_name
        {
            get => _lastName;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("Last name must be specified.");
                }
                _lastName = value;
            }
        }

        [Required]
        [MaxLength(100)]
        public string permanent_address
        {
            get => _permanentAddress;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("Address must be specified.");
                }
                _permanentAddress = value;
            }
        }

        [MaxLength(100)]
        public string temporary_address { get; set; }

        [Required]
        [MaxLength(15)]
        public string primary_contact { get; set; }

        [MaxLength(15)]
        public string secondary_contact { get; set; }

        [MaxLength(100)]
        public string image_path { get; set; }

        [ForeignKey(nameof(user_id))]
        public virtual User user { get; set; }

        public string getFullName()
        {
            return $"{first_name} {last_name}";
        }

        public void saveOrUpdate(UserDetailDto user_detail_dto, IFormFile file = null)
        {
            try
            {
                var userDetail = _userDetailRepo.getByUserId(user_detail_dto.user_id);

                if (userDetail == null)
                {
                    save(user_detail_dto, file);
                }
                else
                {
                    update(user_detail_dto, userDetail, file);
                }
            }
            catch (Exception)
            {
                throw;
            }
        }

        private void update(UserDetailDto user_detail_dto, USER_DETAIL user_detail, IFormFile file)
        {
            if (file != null)
            {
                if (user_detail.image_path != null)
                {
                    deleteImage(user_detail.image_path);
                }
                user_detail_dto.image_path = _fileHelper.saveFileAndGetFileName(file);
            }

            // no idea how data is copied from user_detail_dto to user_detail entity as passing dto to this class in constructor creates a new instance of entity and all the set properties , which are not passed from view (client side) are lost

            // Or is it that I need to copy all the values from dto to entity here again?

            user_detail.modified_by = user_detail_dto.modified_by;
            user_detail.modified_date = DateTime.Now;
            _userDetailRepo.update(user_detail);
        }

        private void save(UserDetailDto user_detail_dto, IFormFile file)
        {

            var userDetail = new USER_DETAIL(user_detail_dto);

            if (file != null)
            {
                userDetail.image_path = _fileHelper.saveFileAndGetFileName(file);
            }

            userDetail.created_by = user_detail_dto.created_by;
            userDetail.created_date = DateTime.Now;
            _userDetailRepo.insert(userDetail);
        }

        private void deleteImage(string iMAGE_PATH)
        {
            _fileHelper.deleteImage(iMAGE_PATH, _fileHelper.getPathToImageFolder());
        }
    }

User_Detailエンティティは、Userクラスに挿入されたエンティティでもあります。これは決して良いとは思いませんし、うまくいくかどうかはわかりません。 複数のコンストラクターUserエンティティに存在します。依存性注入はこれをどのように解決しますか?コンパイル時の問題も解決できません。ドメインモデルに依存している、または依存性注入の原則を使用しているコードのサンプル/例は見つかりませんでした。誰かが私を助けてくれますか?

1

要約する

ファットドメインモデルは、すべてを同じクラスに配置することを意味しません。あなたはこれに少し乗り気になりました。

(太っている)ドメインモデルメソッドは現在のオブジェクト上の操作に焦点を当てるべきであり、現在のオブジェクトの状態を効果的に無視するいくつかのメソッドを追加しましたが、これは意味がありません。


コンテンツを確認する

_public User(UserRepository userRepo, PasswordHash passwordHash, USER_DETAIL userDetail) : base()
{
    _userRepo = userRepo;
    _passwordHash = passwordHash;
    _userDetail = userDetail;
}

public User(UserDto user_dto)
{
    this.is_owner = user_dto.is_owner;
    this.id = user_dto.user_id;
    this.email = user_dto.email;
    this.normalized_email = this.email.ToUpper();
    this.is_active = user_dto.is_active;
}
_

ここで奇妙なのは、2つのコンストラクターを分離することで、どちらか入力済みのユーザーがいるまたはバッキングリポジトリを持つ空のユーザーであるということです。これらは、オブジェクトが存在するための大きく異なる2つの状態と、アプリケーションでオブジェクトが果たすための大きく異なる役割です。

依存関係を注入したいことは理解していますが、それは良いことですが、そのルートをたどっている場合は、2番目のコンストラクターを避けて、代わりにクラスの静的メソッドに移動します。

_[MaxLength(250)]
[Required]
public string password
{
    get => _password;
    set
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new NonEmptyValueException("Password cannot be empty.");
        }
        _password = value;
    }
}
_

すべてのプロパティとそれらの検証(理由内)は確かにモデルに属しています。

検証が単純な値チェック以上の場合は、検証を別のクラスに配置するかどうかを評価し、Userにドルを渡すようにしてください。注入されたEmailAddressValidator依存関係。

_public void disable()
{
    is_active = false;
}
_

操作現在のオブジェクトもモデルに属しています。 これは脂肪vs貧血の核となる本質ですです。ファットモデルでは、オブジェクトタイプ(クラス)自体のオブジェクトに対して実行できる操作を記述します。

これは私を導きます:

_public User checkAuthenticity(string username, string password)
{
    var user = _userRepo.getByUsername(username);
    if (user == null || !user.is_active || !user.is_email_confirmed)
    {
        return null;
    }

    if (!_passwordHash.ValidatePassword(password, user.password))
    {
        return null;
    }

    return user;
}
_

このメソッドは、インスタンス化されたオブジェクトに一切依存しません(注入された依存関係を使用する以外)。これを別の場所で静的メソッドにしても、機能し続けます(同じ依存関係が注入された場合)。
(インスタンス化された)クラスメソッドは、コンシューマがすでに(Userの)インスタンスを持ち、それに対して操作を実行したい場合にのみ意味があります。しかしこの場合、コンシューマは資格情報しか持っておらず、そこからUserオブジェクトをgetにしたいと考えています。これをUserのクラスメソッドとして配置しても意味がありません。

このメソッドはUserに属していません。少なくとも現在の形式ではありません。メソッドに渡すのではなく、オブジェクトのユーザー資格情報に依存していた場合、それはより理にかなっています。コードの意図がやや曖昧であるため、これが正しいアプローチであるかどうかを明確に述べることはできません。資格情報を確認しようとしているのか、またはデータベースからユーザーをフェッチしようとしているのですか?

少し話題から外れているので、ここでロジックを大幅に変更します。匂いテストに合格しません。既存のユーザーオブジェクトから新しくフェッチしたユーザーオブジェクトを渡すのはなぜですか?ログインを確認するためにブール値を渡すだけでなく、オブジェクトまたはnullを返すのはなぜですか?

このメソッドは、Userオブジェクトの外のどこかに属しているか(User GetUserByCredentials(string username, string password)の行に沿って)、またはクラスメソッドをbool IsAuthenticated()のようなものに変更する必要があります。オブジェクトの状態を入力してデータベースと一致させます(これもプロパティの可能性がありますが、複数回呼び出された場合に備えて結果をキャッシュしてください)。

UserクラスをUserRepositoryへの唯一のゲートウェイとして使用しようとしているようですが、これはファットドメインモデルの目的ではありません。たとえそれが太っていても、ドメインモデルはモノリスになるべきではありません。

_public void update(UserDto user_dto)
{
    try
    {

        User user = _userRepo.getById(user_dto.user_id) ?? throw new ItemNotFoundException($"User with the id {user_dto.user_id} doesnot exist.");

        bool isUsernameValid = checkNameValidity(user_dto);

        if (!isUsernameValid)
        {
            throw new DuplicateItemException("User with same name already exists.");
        }

        _userAssembler.copy(user, user_dto);
        user.modified_by = user_dto.modified_by;
        user.modified_date = DateTime.Now;
        _userRepo.update(user);

        USER_DETAIL user_detail = new USER_DETAIL(user_dto);
        user_detail.saveOrUpdate();
    }
    catch (Exception)
    {
        throw;
    }
}
_

このメソッドには同じ問題があり、実際には現在のUserオブジェクトに意味のある方法で依存せず、依存関係にのみ使用します。

奇妙なことはyour create methodsave()すでに正しい方法で行われているです。作成/更新ロジックは同じアプローチを使用すると思いますが、ここでは大きく異なります。 1つは入力オブジェクトを使用し、もう1つはこのメソッドが存在する実際のオブジェクトを使用します。


_USER_DETAIL_クラスのフィードバックも同様です。

1
Flater

ここにはロジックがないようです。そのすべての検証と永続性。

基本的にはかなり単純な変換ですが。

ADM

class Service
{
    void SendPackage(User user)
}

class User
{
    string Id;
    string Name;
}

脂肪モデル

class User
{
    string Id;
    string Name;
    void SendPackage() {...}
}

私はレポを別々に保ち、注入しません。

class Repository
{
    void Save(User user);
}

私はそれがリポジトリを注入することがあなたのコードを助けるために何もしないことがかなり証明されたと思います

0
Ewan

他のコメントには良い点がたくさんありますが、もう一つ付け加えたいと思います。

「リッチ」ドメインモデル(つまり、オブジェクト指向)は技術的ではありません(またはすべきではありません)。これは一部の人々にとって最も混乱しているものです。手続きの世界(貧血モデル)では、必要なすべてのデータを定義し、後で別の場所で機能を分類します。あなたはいつでもすべてにアクセスできるので、あなたはそれを知る必要はありません。

「リッチ」ドメインモデルでは、動作から始める必要があります。つまり、このオブジェクトに必要なbusiness-relevant機能は何ですか。したがって、この時点では考えていないことについて考えなければなりません。これらのオブジェクトは「汎用」にすることはできません。オブジェクトへのnoアクセスが必要なため、オブジェクトによって定義された方法でのみ使用できますデータはすべて。

saveOrUpdate()または同様のパブリックメソッドは、技術的なものであるため、存在できません。また、すべての内部データにアクセスできる外部Repositoryは存在できません。これは、オブジェクトを最初に配置する目的を無効にするためです。これらの技術的なものは、実装の他の詳細と同様に、ビジネス関連のインターフェースの背後に隠されています。

HTH。

0