Bagaimana membuat desain ini lebih dekat dengan DDD yang tepat?

12

Saya sudah membaca tentang DDD selama berhari-hari sekarang dan butuh bantuan dengan desain sampel ini. Semua aturan DDD membuat saya sangat bingung dengan bagaimana saya seharusnya membangun apa saja ketika objek domain tidak diperbolehkan menunjukkan metode ke lapisan aplikasi; di mana lagi untuk mengatur perilaku? Repositori tidak diizinkan untuk disuntikkan ke entitas dan entitas itu sendiri harus bekerja pada negara. Maka suatu entitas perlu mengetahui sesuatu yang lain dari domain, tetapi objek entitas lain juga tidak diizinkan untuk disuntikkan? Beberapa hal ini masuk akal bagi saya tetapi beberapa tidak. Saya belum menemukan contoh yang baik tentang bagaimana membangun seluruh fitur karena setiap contoh adalah tentang Pesanan dan Produk, mengulangi contoh-contoh lainnya berulang-ulang. Saya belajar paling baik dengan membaca contoh dan telah mencoba membangun fitur menggunakan informasi yang saya dapatkan tentang DDD sejauh ini.

Saya butuh bantuan Anda untuk menunjukkan apa yang saya lakukan salah dan bagaimana memperbaikinya, paling disukai dengan kode sebagai "Saya tidak akan merekomendasikan melakukan X dan Y" sangat sulit untuk dipahami dalam konteks di mana semuanya sudah didefinisikan secara samar-samar. Jika saya tidak bisa menyuntikkan entitas ke entitas lain, akan lebih mudah untuk melihat bagaimana melakukannya dengan benar.

Dalam contoh saya ada pengguna dan moderator. Seorang moderator dapat mencekal pengguna, tetapi dengan aturan bisnis: hanya 3 per hari. Saya melakukan upaya menyiapkan diagram kelas untuk menunjukkan hubungan (kode di bawah):

masukkan deskripsi gambar di sini

interface iUser
{
    public function getUserId();
    public function getUsername();
}

class User implements iUser
{
    protected $_id;
    protected $_username;

    public function __construct(UserId $user_id, Username $username)
    {
        $this->_id          = $user_id;
        $this->_username    = $username;
    }

    public function getUserId()
    {
        return $this->_id;
    }

    public function getUsername()
    {
        return $this->_username;
    }
}

class Moderator extends User
{
    protected $_ban_count;
    protected $_last_ban_date;

    public function __construct(UserBanCount $ban_count, SimpleDate $last_ban_date)
    {
        $this->_ban_count       = $ban_count;
        $this->_last_ban_date   = $last_ban_date;
    }

    public function banUser(iUser &$user, iBannedUser &$banned_user)
    {
        if (! $this->_isAllowedToBan()) {
            throw new DomainException('You are not allowed to ban more users today.');
        }

        if (date('d.m.Y') != $this->_last_ban_date->getValue()) {
            $this->_ban_count = 0;
        }

        $this->_ban_count++;

        $date_banned        = date('d.m.Y');
        $expiration_date    = date('d.m.Y', strtotime('+1 week'));

        $banned_user->add($user->getUserId(), new SimpleDate($date_banned), new SimpleDate($expiration_date));
    }

    protected function _isAllowedToBan()
    {
        if ($this->_ban_count >= 3 AND date('d.m.Y') == $this->_last_ban_date->getValue()) {
            return false;
        }

        return true;
    }
}

interface iBannedUser
{
    public function add(UserId $user_id, SimpleDate $date_banned, SimpleDate $expiration_date);
    public function remove();
}

class BannedUser implements iBannedUser
{
    protected $_user_id;
    protected $_date_banned;
    protected $_expiration_date;

    public function __construct(UserId $user_id, SimpleDate $date_banned, SimpleDate $expiration_date)
    {
        $this->_user_id         = $user_id;
        $this->_date_banned     = $date_banned;
        $this->_expiration_date = $expiration_date;
    }

    public function add(UserId $user_id, SimpleDate $date_banned, SimpleDate $expiration_date)
    {
        $this->_user_id         = $user_id;
        $this->_date_banned     = $date_banned;
        $this->_expiration_date = $expiration_date;
    }

    public function remove()
    {
        $this->_user_id         = '';
        $this->_date_banned     = '';
        $this->_expiration_date = '';
    }
}

// Gathers objects
$user_repo = new UserRepository();
$evil_user = $user_repo->findById(123);

$moderator_repo = new ModeratorRepository();
$moderator = $moderator_repo->findById(1337);

$banned_user_factory = new BannedUserFactory();
$banned_user = $banned_user_factory->build();

// Performs ban
$moderator->banUser($evil_user, $banned_user);

// Saves objects to database
$user_repo->store($evil_user);
$moderator_repo->store($moderator);

$banned_user_repo = new BannedUserRepository();
$banned_user_repo->store($banned_user);

Haruskah hak Pengguna memiliki 'is_banned'bidang yang dapat diperiksa $user->isBanned();? Bagaimana cara menghapus larangan? Saya tidak punya ide.

Serialisasi
sumber
Dari artikel Wikipedia: "Desain berbasis domain bukanlah teknologi atau metodologi." Sehingga pembahasan semacam itu tidak sesuai untuk format ini. Selain itu, hanya Anda dan 'pakar' yang dapat memutuskan apakah model Anda tepat.
1
@Todd smith membuat titik bagus pada "objek domain tidak diperbolehkan untuk menunjukkan metode ke lapisan aplikasi" . Perhatikan contoh kode pertama kunci untuk tidak menyuntikkan repositori ke objek domain adalah, sesuatu yang lain menyimpan dan memuatnya. Mereka tidak melakukannya sendiri. Ini memungkinkan logika aplikasi mengontrol transaksi, juga, alih-alih domain / model / entitas / objek bisnis / atau apa pun yang Anda ingin menyebutnya.
FastAl

Jawaban:

11

Pertanyaan ini agak subyektif dan mengarah ke lebih banyak diskusi daripada jawaban langsung, yang, seperti yang ditunjukkan orang lain - tidak sesuai untuk format stackoverflow. Yang mengatakan, saya pikir Anda hanya perlu beberapa contoh kode tentang cara mengatasi masalah, jadi saya akan mencobanya, hanya untuk memberi Anda beberapa ide.

Hal pertama yang saya katakan adalah:

"objek domain tidak diperbolehkan menampilkan metode ke lapisan aplikasi"

Itu tidak benar - saya akan tertarik untuk mengetahui dari mana Anda membaca ini. Lapisan aplikasi adalah orkestra antara UI, Infrastruktur & Domain, dan karenanya jelas perlu memanggil metode pada entitas domain.

Saya telah menulis contoh kode tentang bagaimana saya akan mengatasi masalah Anda. Saya minta maaf karena itu dalam C #, tapi saya tidak tahu PHP - mudah-mudahan Anda masih mendapatkan intisari dari perspektif struktur.

Mungkin seharusnya saya tidak melakukannya, tetapi saya telah sedikit memodifikasi objek domain Anda. Mau tidak mau saya merasa itu sedikit cacat, dalam konsep 'BannedUser' ada dalam sistem, bahkan jika larangan telah berakhir.

Untuk memulainya, inilah layanan aplikasi - inilah yang akan disebut UI:

public class ModeratorApplicationService
{
    private IUserRepository _userRepository;
    private IModeratorRepository _moderatorRepository;

    public void BanUser(Guid moderatorId, Guid userToBeBannedId)
    {
        Moderator moderator = _moderatorRepository.GetById(moderatorId);
        User userToBeBanned = _userRepository.GetById(userToBeBannedId);

        using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
        {
            userToBeBanned.Ban(moderator);

            _userRepository.Save(userToBeBanned);
            _moderatorRepository.Save(moderator);
        }
    }
}

Cukup lurus ke depan. Anda menjemput moderator yang melakukan pelarangan, pengguna yang ingin dilarutkan oleh moderator, dan memanggil metode 'Larangan' pada pengguna, melewati moderator. Ini akan mengubah status moderator & pengguna (dijelaskan di bawah), yang kemudian perlu bertahan melalui repositori terkait.

Kelas Pengguna:

public class User : IUser
{
    private readonly Guid _userId;
    private readonly string _userName;
    private readonly List<ServingBan> _servingBans = new List<ServingBan>();

    public Guid UserId
    {
        get { return _userId; }
    }

    public string Username
    {
        get { return _userName; }
    }

    public void Ban(Moderator bannedByModerator)
    {
        IssuedBan issuedBan = bannedByModerator.IssueBan(this);

        _servingBans.Add(new ServingBan(bannedByModerator.UserId, issuedBan.BanDate, issuedBan.BanExpiry));
    }

    public bool IsBanned()
    {
        return (_servingBans.FindAll(CurrentBans).Count > 0);
    }

    public User(Guid userId, string userName)
    {
        _userId = userId;
        _userName = userName;
    }

    private bool CurrentBans(ServingBan ban)
    {
        return (ban.BanExpiry > DateTime.Now);
    }

}

public class ServingBan
{
    private readonly DateTime _banDate;
    private readonly DateTime _banExpiry;
    private readonly Guid _bannedByModeratorId;

    public DateTime BanDate
    {
        get { return _banDate;}
    }

    public DateTime BanExpiry
    {
        get { return _banExpiry; }
    }

    public ServingBan(Guid bannedByModeratorId, DateTime banDate, DateTime banExpiry)
    {
        _bannedByModeratorId = bannedByModeratorId;
        _banDate = banDate;
        _banExpiry = banExpiry;
    }
}

Yang invarian bagi pengguna adalah mereka tidak dapat melakukan tindakan tertentu saat dicekal, jadi kami harus dapat mengidentifikasi apakah pengguna saat ini dicekal. Untuk mencapai hal ini, pengguna menyimpan daftar larangan penayangan yang telah dikeluarkan oleh moderator. Metode IsBanned () memeriksa semua larangan penayangan yang belum kedaluwarsa. Ketika metode Ban () dipanggil, ia menerima moderator sebagai parameter. Ini kemudian meminta moderator untuk mengeluarkan larangan:

public class Moderator : User
{
    private readonly List<IssuedBan> _issuedbans = new List<IssuedBan>();

    public bool CanBan()
    {
        return (_issuedbans.FindAll(BansWithTodaysDate).Count < 3);
    }

    public IssuedBan IssueBan(User user)
    {
        if (!CanBan())
            throw new InvalidOperationException("Ban limit for today has been exceeded");

        IssuedBan issuedBan = new IssuedBan(user.UserId, DateTime.Now, DateTime.Now.AddDays(7));

        _issuedbans.Add(issuedBan); 

        return issuedBan;
    }

    private bool BansWithTodaysDate(IssuedBan ban)
    {
        return (ban.BanDate.Date == DateTime.Today.Date);
    }
}

public class IssuedBan
{
    private readonly Guid _bannedUserId;
    private readonly DateTime _banDate;
    private readonly DateTime _banExpiry;

    public DateTime BanDate { get { return _banDate;}}

    public DateTime BanExpiry { get { return _banExpiry;}}

    public IssuedBan(Guid bannedUserId, DateTime banDate, DateTime banExpiry)
    {
        _bannedUserId = bannedUserId;
        _banDate = banDate;
        _banExpiry = banExpiry;
    }
}

Yang lain dari moderator adalah ia hanya dapat mengeluarkan 3 larangan per hari. Jadi, ketika metode IssueBan dipanggil, ia memeriksa bahwa moderator tidak memiliki 3 larangan yang dikeluarkan dengan tanggal hari ini dalam daftar larangan yang dikeluarkan. Kemudian menambahkan larangan yang baru dikeluarkan ke daftar itu dan mengembalikannya.

Subyektif, dan saya yakin seseorang akan tidak setuju dengan pendekatan itu, tetapi mudah-mudahan itu memberi Anda ide atau bagaimana itu bisa cocok bersama.

David Masters
sumber
1

Pindahkan semua logika Anda yang mengubah status ke lapisan layanan (mis: ModeratorService) yang tahu tentang Entitas dan Repositori.

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
Todd Smith
sumber