วิธีทำให้การออกแบบนี้ใกล้เคียงกับ DDD ที่เหมาะสมมากขึ้น?


12

ฉันได้อ่านเกี่ยวกับ DDD มาหลายวันแล้วและต้องการความช่วยเหลือในการออกแบบตัวอย่างนี้ กฎทั้งหมดของ DDD ทำให้ฉันสับสนมากกับวิธีที่ฉันควรจะสร้างอะไรเลยเมื่อวัตถุโดเมนไม่ได้รับอนุญาตให้แสดงวิธีการในชั้นแอพลิเคชัน; มีที่ไหนอีกที่จะจัดการกับพฤติกรรม? ที่เก็บไม่ได้รับอนุญาตให้ถูกฉีดเข้าไปในเอนทิตีและเอนทิตี้ของตัวเองจึงต้องทำงานกับรัฐ ถ้าอย่างนั้นเอนทิตีต้องรู้อะไรจากโดเมน แต่วัตถุเอนทิตีอื่น ๆ ไม่ได้รับอนุญาตให้ฉีด? บางสิ่งเหล่านี้มีเหตุผลสำหรับฉัน แต่บางอย่างไม่ ฉันยังไม่พบตัวอย่างที่ดีของวิธีสร้างคุณลักษณะทั้งหมดเนื่องจากทุกตัวอย่างเกี่ยวกับคำสั่งซื้อและผลิตภัณฑ์ทำซ้ำตัวอย่างอื่น ๆ ซ้ำแล้วซ้ำอีก ฉันเรียนรู้ได้ดีที่สุดจากการอ่านตัวอย่างและพยายามสร้างคุณสมบัติโดยใช้ข้อมูลที่ฉันได้รับเกี่ยวกับ DDD ในตอนนี้

ฉันต้องการให้คุณช่วยชี้ให้เห็นสิ่งที่ฉันทำผิดและวิธีการแก้ไขโดยเฉพาะอย่างยิ่งกับรหัสว่า "ฉันจะไม่ recomment ทำ X และ Y" ยากที่จะเข้าใจในบริบทที่ทุกอย่างเพิ่งกำหนดชัดเจนแล้ว ถ้าฉันไม่สามารถฉีดเอนทิตี้เข้าไปในที่อื่นได้จะง่ายกว่าที่จะเห็นวิธีการทำอย่างถูกต้อง

ในตัวอย่างของฉันมีผู้ใช้และผู้ดูแล ผู้ดำเนินรายการสามารถแบนผู้ใช้ แต่ด้วยกฎธุรกิจ: เพียง 3 ต่อวัน ฉันพยายามตั้งค่าคลาสไดอะแกรมเพื่อแสดงความสัมพันธ์ (รหัสด้านล่าง):

ป้อนคำอธิบายรูปภาพที่นี่

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);

สิทธิ์ของผู้ใช้ควรมี'is_banned'ฟิลด์ที่สามารถตรวจสอบได้$user->isBanned();หรือไม่? จะลบการแบนได้อย่างไร? ฉันไม่รู้.


จากบทความ Wikipedia: "การออกแบบที่ขับเคลื่อนด้วยโดเมนไม่ใช่เทคโนโลยีหรือวิธีการ" ดังนั้นการอภิปรายในลักษณะนี้จึงไม่เหมาะสมสำหรับรูปแบบนี้ นอกจากนี้มีเพียงคุณและผู้เชี่ยวชาญของคุณเท่านั้นที่สามารถตัดสินใจได้ว่าแบบจำลองของคุณถูกต้องหรือไม่

1
@Todd smith ทำให้จุดที่ดีเกี่ยวกับ"วัตถุโดเมนไม่ได้รับอนุญาตให้แสดงวิธีการที่ชั้นสมัคร" โปรดสังเกตว่ารหัสแรกเป็นตัวอย่างสำคัญที่จะไม่ฉีดที่เก็บข้อมูลลงในวัตถุโดเมนคือมีสิ่งอื่นบันทึกและโหลด พวกเขาไม่ทำเอง สิ่งนี้ทำให้แอพควบคุมตรรกะของธุรกรรมเช่นกันแทนที่จะเป็นวัตถุ / โดเมน / เอนทิตี้ / ธุรกิจ / หรืออะไรก็ตามที่คุณต้องการโทรหา
FastAl

คำตอบ:


11

คำถามนี้ค่อนข้างเป็นอัตนัยและนำไปสู่การอภิปรายมากกว่าคำตอบโดยตรงซึ่งเป็นคนอื่นได้ชี้ให้เห็น - ไม่เหมาะสำหรับรูปแบบ stackoverflow ที่กล่าวว่าฉันคิดว่าคุณแค่ต้องการตัวอย่างรหัสเกี่ยวกับวิธีการแก้ไขปัญหาดังนั้นฉันจะให้มันยิงเพียงเพื่อให้ความคิด

สิ่งแรกที่ฉันจะพูดคือ:

"วัตถุโดเมนไม่ได้รับอนุญาตให้แสดงวิธีการในชั้นโปรแกรมประยุกต์"

นั่นไม่ใช่เรื่องจริง - ฉันสนใจที่จะทราบว่าคุณอ่านบทความนี้จากที่ไหน แอปพลิเคชันเลเยอร์เป็นตัวควบคุมระหว่าง UI, โครงสร้างพื้นฐานและโดเมนและดังนั้นจึงจำเป็นต้องเรียกใช้เมธอดบนเอนทิตีโดเมนอย่างชัดเจน

ฉันได้เขียนตัวอย่างรหัสว่าฉันจะจัดการปัญหาของคุณอย่างไร ฉันขอโทษที่อยู่ใน C # แต่ฉันไม่รู้ PHP - หวังว่าคุณจะยังคงได้รับส่วนสำคัญจากมุมมองโครงสร้าง

บางทีฉันไม่ควรทำ แต่ฉันได้ปรับเปลี่ยนวัตถุโดเมนของคุณเล็กน้อย ฉันอดไม่ได้ที่จะรู้สึกว่ามันมีข้อบกพร่องเล็กน้อยในแนวคิดของ 'BannedUser' นั้นมีอยู่ในระบบแม้ว่าการแบนจะหมดอายุแล้ว

เริ่มต้นด้วยนี่คือบริการแอปพลิเคชัน - นี่คือสิ่งที่ 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);
        }
    }
}

ตรงไปตรงมาสวย คุณดึงผู้ดำเนินการที่ห้ามผู้ใช้ที่ผู้ดูแลต้องการที่จะห้ามและเรียกวิธีการ 'แบน' กับผู้ใช้ผ่านผู้ดูแล การดำเนินการนี้จะแก้ไขสถานะของทั้งโมเดอเรเตอร์และผู้ใช้ (อธิบายไว้ด้านล่าง) ซึ่งจำเป็นต้องคงอยู่ผ่านที่เก็บข้อมูลที่สอดคล้องกัน

คลาสผู้ใช้:

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;
    }
}

ค่าคงที่สำหรับผู้ใช้คือพวกเขาไม่สามารถดำเนินการบางอย่างเมื่อถูกแบนดังนั้นเราต้องสามารถระบุได้ว่าผู้ใช้ถูกแบนหรือไม่ เพื่อให้บรรลุผลดังกล่าวผู้ใช้จะต้องเก็บรักษารายการการให้บริการเรย์แบนที่ออกโดยผู้ดูแล กระบวนการ IsBanned () วิธีการตรวจสอบสำหรับเรย์แบนการให้บริการใด ๆ ที่ยังไม่หมดอายุ เมื่อมีการเรียกใช้เมธอด Ban () จะได้รับโมเดอเรเตอร์เป็นพารามิเตอร์ สิ่งนี้จะขอให้ผู้ควบคุมทำการสั่งห้าม:

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;
    }
}

ค่าคงที่สำหรับผู้ดำเนินรายการคือสามารถออกได้ 3 แบนต่อวันเท่านั้น ดังนั้นเมื่อมีการเรียกเมธอด IssueBan มันจะตรวจสอบว่าผู้ดำเนินรายการไม่มี 3 bans ที่ออกแล้วพร้อมกับวันที่วันนี้ในรายการของ bans ที่ออกให้ จากนั้นจะเพิ่มการห้ามที่ออกใหม่ไปยังรายการและส่งคืน

ส่วนตัวและฉันแน่ใจว่าบางคนจะไม่เห็นด้วยกับวิธีการ แต่หวังว่ามันจะช่วยให้คุณมีความคิดหรือวิธีที่จะเข้าด้วยกัน


1

ย้ายตรรกะทั้งหมดของคุณซึ่งเปลี่ยนสถานะเป็นเลเยอร์บริการ (เช่น ModeratorService) ซึ่งรู้เกี่ยวกับทั้งเอนทิตีและที่เก็บ

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.