การเข้าถึงที่เก็บจากโดเมน


14

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

แอพลิเคชันเลเยอร์:

public class TaskService
{
    //...

    public void Add(Guid categoryId, string description)
    {
        var category = _categoryRepository.GetById(categoryId);
        var status = _statusRepository.GetById(Constants.Status.OutstandingId);
        var task = Task.Create(category, status, description);
        _taskRepository.Save(task);
    }
}

Entity:

public class Task
{
    //...

    public static void Create(Category category, Status status, string description)
    {
        return new Task
        {
            Category = category,
            Status = status,
            Description = descrtiption
        };
    }
}

ฉันทำเช่นนี้เพราะฉันบอกอย่างสม่ำเสมอว่าเอนทิตีไม่ควรเข้าถึงที่เก็บ แต่มันจะทำให้ฉันรู้สึกดีขึ้นถ้าฉันทำสิ่งนี้:

Entity:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        return new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };
    }
}

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

นี่คือตัวอย่างที่รุนแรงยิ่งขึ้นที่นี่โดเมนตัดสินใจเร่งด่วน:

Entity:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        var task = new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            task.Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

ไม่มีวิธีที่คุณต้องการส่งผ่านในรุ่น Urgency ที่เป็นไปได้ทั้งหมดและไม่มีวิธีที่คุณต้องการคำนวณตรรกะทางธุรกิจนี้ในเลเยอร์แอปพลิเคชันดังนั้นแน่นอนว่านี่จะเป็นวิธีที่เหมาะสมที่สุด?

นี่คือเหตุผลที่ถูกต้องในการเข้าถึงที่เก็บจากโดเมน?

แก้ไข: นี่อาจเป็นกรณีในวิธีการไม่คงที่:

public class Task
{
    //...

    public void Update(Category category, string description)
    {
        Category = category,
        Status = _statusRepository.GetById(Constants.Status.OutstandingId),
        Description = descrtiption

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

คำตอบ:


8

คุณกำลังผสม

เอนทิตีไม่ควรเข้าถึงที่เก็บ

(ซึ่งเป็นข้อเสนอแนะที่ดี)

และ

เลเยอร์โดเมนไม่ควรเข้าถึงที่เก็บ

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

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

แก้ไข: ที่คุณUpdateตัวอย่างเช่นกำหนดว่า_urgencyRepositoryและstatusRepository เป็นสมาชิกของชั้นเรียนTaskที่กำหนดไว้เป็นชนิดของอินเตอร์เฟซที่บางท่านตอนนี้จำเป็นต้องฉีดพวกเขาเข้าไปในใด ๆTaskนิติบุคคลก่อนที่คุณจะสามารถใช้Updateในขณะนี้ (เช่นในตัวสร้างงาน) หรือคุณกำหนดให้เป็นสมาชิกแบบคงที่ แต่ระวังว่าอาจทำให้เกิดปัญหาหลายเธรดได้อย่างง่ายดายหรือเพียงแค่ปัญหาเมื่อคุณต้องการที่เก็บที่แตกต่างกันสำหรับหน่วยงานที่แตกต่างกันในเวลาเดียวกัน

การออกแบบนี้ทำให้ยากขึ้นเล็กน้อยในการสร้างTaskเอนทิตีแยกต่างหากดังนั้นจึงยากที่จะเขียนการทดสอบหน่วยสำหรับTaskเอนทิตียากที่จะเขียนการทดสอบอัตโนมัติขึ้นอยู่กับเอนทิตี้ของงานและคุณสร้างหน่วยความจำโอเวอร์เฮดมากขึ้น ถือสองอ้างอิงถึง reposities แน่นอนว่าอาจจะทนได้ในกรณีของคุณ ในทางกลับกันการสร้างคลาสยูทิลิตี้แยกต่างหากTaskUpdaterซึ่งเก็บการอ้างอิงไปยังที่เก็บข้อมูลที่ถูกต้องอาจบ่อยครั้งหรืออย่างน้อยก็เป็นทางออกที่ดีกว่า

ส่วนที่สำคัญคือTaskUpdaterจะยังคงเป็นส่วนหนึ่งของเลเยอร์โดเมน! เพียงเพราะคุณใส่รหัสการปรับปรุงหรือการสร้างในชั้นเรียนที่แยกต่างหากไม่ได้หมายความว่าคุณต้องเปลี่ยนไปใช้ชั้นอื่น


ฉันได้แก้ไขเพื่อแสดงสิ่งนี้นำไปใช้กับวิธีการไม่คงที่มากเท่าที่คงที่ ฉันไม่เคยคิดถึงวิธีการของโรงงานที่ไม่ได้เป็นส่วนหนึ่งของกิจการ
Paul T Davies

@PaulTDavies: ดูการแก้ไขของฉัน
Doc Brown

ฉันเห็นด้วยกับสิ่งที่คุณกำลังพูดอยู่ที่นี่ แต่ฉันเพิ่มบทสรุปที่กระชับจุดที่Status = _statusRepository.GetById(Constants.Status.OutstandingId)เป็นกฎทางธุรกิจหนึ่งที่คุณสามารถอ่านได้ว่า "ธุรกิจกำหนดสถานะเริ่มต้นของงานทั้งหมดจะโดดเด่น" และนี่คือเหตุผลที่ บรรทัดโค้ดนั้นไม่ได้อยู่ในที่เก็บซึ่งมีข้อกังวลเพียงอย่างเดียวคือการจัดการข้อมูลผ่านการดำเนินการ CRUD
Jimmy Hoffa

@JimmyHoffa: หืมไม่มีใครที่นี่แนะนำให้ใส่สายแบบนั้นลงในคลาสของที่เก็บหนึ่งชั้นทั้ง OP และฉัน - แล้วประเด็นของคุณคืออะไร?
Doc Brown

ฉันค่อนข้างชอบความคิดของ TaskUpdater ที่เป็นบริการโดมิโน ดูเหมือนว่าจะเป็นเรื่องเหลวไหลเพียงเล็กน้อยเพื่อรักษาหลักการ DDD แต่มันก็หมายความว่าฉันสามารถหลีกเลี่ยงการฉีดที่เก็บทุกครั้งที่ฉันใช้งาน
Paul T Davies

6

ฉันไม่ทราบว่าตัวอย่างสถานะของคุณเป็นรหัสจริงหรือที่นี่เพียงเพื่อการสาธิต แต่มันก็แปลกสำหรับฉันที่คุณควรใช้สถานะเป็นนิติบุคคล (ไม่ต้องพูดถึงรากรวม) เมื่อรหัสนั้นเป็นค่าคงที่ ในรหัส Constants.Status.OutstandingId- นั่นไม่ได้เป็นการทำลายจุดประสงค์ของสถานะ "ไดนามิก" ที่คุณสามารถเพิ่มได้มากเท่าที่คุณต้องการในฐานข้อมูลใช่ไหม

ฉันจะเพิ่มว่าในกรณีของคุณการสร้างTask(รวมถึงงานในการรับสถานะที่ถูกต้องจาก StatusRepository ถ้าจำเป็น) อาจสมควรได้รับTaskFactoryมากกว่าอยู่ในTaskตัวเองเพราะมันเป็นการชุมนุมที่ไม่สำคัญของวัตถุ

แต่:

ฉันบอกอย่างสม่ำเสมอว่าเอนทิตีไม่ควรเข้าถึงที่เก็บ

คำแถลงนี้ไม่แน่ชัดและชัดแจ้งมากที่สุดทำให้เข้าใจผิดและเป็นอันตรายที่สุด

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

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

มีอีกสองสามตัวอย่าง สิ่งคือบางครั้งมีพฤติกรรมที่คุณไม่สามารถวางในบริการโดเมนได้เนื่องจากดูเหมือนว่าจะเข้ากันได้ดีกับเอนทิตีที่มีอยู่ และยังเอนทิตีนี้ต้องเข้าถึง Repository เพื่อให้ความชุ่มชื้นแก่รูทหรือคอลเลกชั่นของรูทที่ไม่สามารถส่งผ่านได้

ดังนั้นการเข้าถึงพื้นที่เก็บข้อมูลจากกิจการไม่ได้เลวร้ายในตัวเองก็สามารถใช้รูปแบบที่แตกต่างกันว่าเป็นผลมาจากการความหลากหลายของการตัดสินใจในการออกแบบตั้งแต่ภัยพิบัติที่จะได้รับการยอมรับ


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

ถ้าคุณอ่านให้ฉันดีเราเห็นด้วยอย่างยิ่งกับเรื่องนั้น ...
guillaume31

2

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

public class Interval
{
  public Interval(DateTime start, DateTime? end)
  {
    Start=start;
    End=end;
  }

  //To be called by internal framework
  protected Interval()
  {
  }

  public void End(DateTime? when=null)
  {
    if(when==null)
      when=DateTime.Now;
    End=when;
  }

  public DateTime Start{get;protected set;}

  public DateTime? End{get; protected set;}
}

public class TaskStatus
{
  protected TaskStatus()
  {
  }
  public Long Id {get;protected set;}

  public string Name {get; protected set;}

  public string Description {get; protected set;}

  public Interval Duration {get; protected set;}

  public virtual TNewStatus TransitionTo<TNewStatus>()
    where TNewStatus:TaskStatus
  {
    throw new NotImplementedException();
  }
}

public class OutStandingTaskStatus:TaskStatus
{
  protected OutStandingTaskStatus()
  {
  }

  public OutStandingTaskStatus(bool initialize)
  {
    Name="Oustanding";
    Description="For tasks that need to be addressed";
    Duration=new Interval(DateTime.Now,null);
  }

  public override TNewStatus TransitionTo<TNewStatus>()
  {
    if(typeof(TNewStatus)==typeof(CompletedTaskStatus))
    {
      var transitionDate=DateTime.Now();
      Duration.End(transitionDate);
      return new CompletedTaskStatus(true);
    }
    return base.TransitionTo<TNewStatus>();
  }
}

การใช้งานของ CompletedTaskStatus จะเหมือนกันมาก

มีหลายสิ่งที่ควรทราบที่นี่:

  1. ฉันทำให้ตัวสร้างเริ่มต้นได้รับการป้องกัน นี่คือสิ่งที่เฟรมเวิร์กสามารถเรียกมันได้เมื่อดึงวัตถุออกจากการคงอยู่ (ทั้งรหัส EntityFramework-first และ NHibernate ใช้พร็อกซีที่ได้มาจากวัตถุโดเมนของคุณเพื่อใช้เวทย์มนตร์)

  2. ตัวตั้งค่าคุณสมบัติหลายตัวได้รับการป้องกันด้วยเหตุผลเดียวกัน ถ้าฉันต้องการเปลี่ยนวันที่สิ้นสุดของช่วงเวลาฉันต้องเรียกใช้ฟังก์ชัน Interval.End () (นี่เป็นส่วนหนึ่งของการออกแบบโดเมนขับเคลื่อนให้การดำเนินการที่มีความหมายมากกว่าวัตถุโดเมน Anemic

  3. ฉันไม่แสดงที่นี่ แต่ภารกิจจะซ่อนรายละเอียดของวิธีการเก็บสถานะปัจจุบัน ฉันมักจะมีรายการที่มีการป้องกันของ HistoricalStates ที่ฉันอนุญาตให้สาธารณะสอบถามถ้าพวกเขาสนใจ มิฉะนั้นฉันจะเปิดเผยสถานะปัจจุบันเป็นทะเยอทะยานที่สอบถาม HistoricalStates.Single (state.Duration.End == null)

  4. ฟังก์ชัน TransitionTo มีความสำคัญเนื่องจากสามารถมีตรรกะเกี่ยวกับสถานะที่ถูกต้องสำหรับการเปลี่ยนแปลง ถ้าคุณมี enum ตรรกะนั้นต้องอยู่ที่อื่น

หวังว่านี่จะช่วยให้คุณเข้าใจวิธีการ DDD ดีขึ้นเล็กน้อย


1
นี่จะเป็นแนวทางที่ถูกต้องหากรัฐต่าง ๆ มีพฤติกรรมที่แตกต่างกันตามตัวอย่างรูปแบบรัฐของคุณ อย่างไรก็ตามฉันจะพบว่ามันยากที่จะปรับคลาสสำหรับแต่ละรัฐหากพวกเขามีค่าต่างกันไม่ใช่พฤติกรรมที่แตกต่างกัน
Paul T Davies

1

ฉันพยายามแก้ปัญหาเดียวกันบางครั้งฉันตัดสินใจว่าฉันต้องการโทร Task.UpdateTask () แบบนั้นถึงแม้ว่าฉันจะค่อนข้างเป็นโดเมนเฉพาะ แต่ในกรณีของคุณฉันอาจเรียกมันว่า Task.ChangeCategory (... ) เพื่อระบุการกระทำไม่ใช่แค่ CRUD

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UnitTestProject2
{
    public class ClientCode
    {
        public void Main()
        {
            TaskFactory factory = new TaskFactory();
            Task task = factory.Create();
            task.UpdateTask(new Category(), "some value");
        }

    }
    public class Category
    {
    }

    public class Task
    {
        public Action<Category, String> UpdateTask { get; set; }

        public static void UpdateTaskAction(Task task, Category category, string description)
        {
            // do the logic here, static can access private if needed
        }
    }

    public class TaskFactory
    {      
        public Task Create()
        {
            Task task = new Task();
            task.UpdateTask = (category, description) =>
                {
                    Task.UpdateTaskAction(task, category, description);
                };

            return task;
        }

    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.