Truy cập kho từ miền


14

Giả sử chúng tôi có một hệ thống ghi nhật ký tác vụ, khi một tác vụ được ghi lại, người dùng chỉ định một danh mục và tác vụ mặc định thành trạng thái 'Xuất sắc'. Giả sử trong trường hợp này rằng Danh mục và Trạng thái phải được triển khai như các thực thể. Thông thường tôi sẽ làm điều này:

Lớp ứng dụng:

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

Thực thể:

public class Task
{
    //...

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

Tôi làm điều này như thế bởi vì tôi luôn nói rằng các thực thể không nên truy cập vào kho lưu trữ, nhưng nó sẽ có ý nghĩa hơn đối với tôi nếu tôi làm điều này:

Thực thể:

public class Task
{
    //...

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

Dù sao, kho lưu trữ trạng thái là phần phụ thuộc được chèn vào, do đó không có sự phụ thuộc thực sự và điều này đối với tôi nhiều hơn, đó là tên miền đang tạo ra một nhiệm vụ mặc định là một nhiệm vụ mặc định. Phiên bản trước có cảm giác như đó là ứng dụng layeer đưa ra quyết định đó. Bất kỳ lý do tại sao các hợp đồng kho lưu trữ thường trong miền nếu điều này không phải là khả thi?

Đây là một ví dụ cực đoan hơn, ở đây tên miền quyết định cấp bách:

Thực thể:

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

Không có cách nào bạn muốn vượt qua trong tất cả các phiên bản khẩn cấp có thể, và không có cách nào bạn muốn tính toán logic kinh doanh này trong lớp ứng dụng, vậy chắc chắn đây sẽ là cách thích hợp nhất?

Vì vậy, đây có phải là một lý do hợp lệ để truy cập kho từ miền?

EDIT: Đây cũng có thể là trường hợp trên các phương thức không tĩnh:

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

Câu trả lời:


8

Bạn đang hòa quyện

các thực thể không nên truy cập vào kho

(đó là một gợi ý tốt)

lớp miền không nên truy cập vào kho

(có thể là đề xuất xấu miễn là kho lưu trữ của bạn là một phần của lớp miền, không phải lớp ứng dụng). Trên thực tế, các ví dụ của bạn cho thấy không có trường hợp thực thể nào truy cập vào kho lưu trữ, vì bạn đang sử dụng các phương thức tĩnh không thuộc về bất kỳ thực thể nào.

Nếu bạn không muốn đưa logic tạo đó vào một phương thức tĩnh của lớp thực thể, bạn có thể giới thiệu các lớp nhà máy riêng biệt (như một phần của lớp miền!) Và đặt logic tạo ở đó.

EDIT: theo Updateví dụ của bạn : đã cho rằng _urgencyRepositorystatusRepository là thành viên của lớp Task, được định nghĩa là một loại giao diện nào đó, bây giờ bạn cần đưa chúng vào bất kỳ Taskthực thể nào trước khi bạn có thể sử dụng Updatengay bây giờ (ví dụ: trong Trình xây dựng tác vụ). Hoặc bạn xác định họ là thành viên tĩnh, nhưng hãy cẩn thận, có thể dễ dàng gây ra sự cố đa luồng hoặc chỉ xảy ra sự cố khi bạn cần các kho lưu trữ khác nhau cho các thực thể Nhiệm vụ khác nhau cùng một lúc.

Thiết kế này làm cho việc tạo Taskcác thực thể một cách khó khăn hơn một chút , do đó việc viết các bài kiểm tra đơn vị cho Taskcác thực thể khó hơn, khó hơn để viết các bài kiểm tra tự động tùy thuộc vào các thực thể Nhiệm vụ và bạn tạo ra thêm một chút chi phí bộ nhớ, vì hiện tại mọi thực thể Nhiệm vụ cần phải giữ rằng hai tài liệu tham khảo đến reposities. Tất nhiên, điều đó có thể được chấp nhận trong trường hợp của bạn. Mặt khác, việc tạo một lớp tiện ích riêng biệt TaskUpdatergiúp giữ các tham chiếu đến các kho lưu trữ phù hợp có thể thường xuyên hoặc ít nhất là đôi khi là một giải pháp tốt hơn.

Phần quan trọng là: TaskUpdatersẽ vẫn là một phần của lớp miền! Chỉ vì bạn đặt mã cập nhật hoặc tạo mã đó vào một lớp riêng không có nghĩa là bạn phải chuyển sang lớp khác.


Tôi đã chỉnh sửa để hiển thị điều này áp dụng cho các phương thức không tĩnh nhiều như các phương thức tĩnh. Tôi chưa bao giờ thực sự nghĩ về phương pháp nhà máy không phải là một phần của một thực thể.
Paul T Davies

@PaulTDavies: xem bản chỉnh sửa của tôi
Doc Brown

Tôi đồng ý với những gì bạn đang nói ở đây, nhưng tôi sẽ thêm một đoạn ngắn gọn rút ra quan điểm Status = _statusRepository.GetById(Constants.Status.OutstandingId)quy tắc kinh doanh , một điều bạn có thể đọc là "Doanh nghiệp ra lệnh trạng thái ban đầu của tất cả các nhiệm vụ sẽ là Xuất sắc" và đây là lý do tại sao dòng mã đó không thuộc về kho lưu trữ, mà mối quan tâm duy nhất là quản lý dữ liệu thông qua các hoạt động CRUD.
Jimmy Hoffa

@JimmyHoffa: hm, không có ai ở đây đề nghị đưa loại đường đó vào một trong các lớp kho lưu trữ, cả OP và tôi - vậy quan điểm của bạn là gì?
Doc Brown

Tôi khá thích ý tưởng của TaskUpdater là một dịch vụ thống trị. Có vẻ như hơi khó hiểu khi chỉ giữ nguyên tắc DDD, nhưng điều đó có nghĩa là tôi có thể tránh việc tiêm kho lưu trữ mỗi khi tôi sử dụng Tác vụ.
Paul T Davies

6

Tôi không biết ví dụ trạng thái của bạn là mã thực hay ở đây chỉ để chứng minh, nhưng đối với tôi, bạn nên triển khai Trạng thái dưới dạng Thực thể (không đề cập đến Root tổng hợp) khi ID của nó là hằng số được xác định trong mã - Constants.Status.OutstandingId. Không phải điều đó đánh bại mục đích của các trạng thái "động" mà bạn có thể thêm bao nhiêu tùy ý trong cơ sở dữ liệu?

Tôi muốn nói thêm rằng trong trường hợp của bạn, việc xây dựng một Task(bao gồm cả công việc lấy đúng trạng thái từ StatusRep repository nếu cần) có thể xứng đáng TaskFactorythay vì ở trong Taskchính nó, vì nó là một tập hợp các đối tượng không tầm thường.

Nhưng :

Tôi luôn nói rằng các thực thể không nên truy cập vào kho

Tuyên bố này là không chính xác và quá đơn giản ở mức tốt nhất, gây hiểu lầm và nguy hiểm ở mức tồi tệ nhất.

Nó được chấp nhận khá phổ biến trong các kiến ​​trúc hướng tên miền mà một thực thể không nên biết cách tự lưu trữ - đó là nguyên tắc thiếu hiểu biết dai dẳng. Vì vậy, không có cuộc gọi đến kho lưu trữ của nó để thêm chính nó vào kho lưu trữ. Nó nên biết làm thế nào (và khi nào) để lưu trữ các thực thể khác ? Một lần nữa, trách nhiệm đó dường như thuộc về một đối tượng khác - có thể là một đối tượng nhận thức được bối cảnh thực thi và tiến trình chung của trường hợp sử dụng hiện tại, như dịch vụ lớp Ứng dụng.

Một thực thể có thể sử dụng một kho lưu trữ để lấy một thực thể khác ? 90% thời gian không cần thiết, vì các thực thể mà nó cần thường nằm trong phạm vi tổng hợp của nó hoặc có thể đạt được bằng cách truyền tải các đối tượng khác. Nhưng có những lúc họ không. Ví dụ, nếu bạn có cấu trúc phân cấp, các thực thể thường cần truy cập vào tất cả tổ tiên của họ, một đứa cháu cụ thể, v.v. như một phần của hành vi nội tại của họ. Họ không có một tài liệu tham khảo trực tiếp đến những người họ hàng xa. Sẽ là bất tiện khi truyền những người thân này cho họ như là thông số của hoạt động. Vậy tại sao không sử dụng Kho lưu trữ để lấy chúng - miễn là chúng là gốc tổng hợp?

Có một vài ví dụ khác. Vấn đề là, đôi khi có hành vi bạn không thể đặt trong dịch vụ Miền vì nó có vẻ phù hợp hoàn hảo với một thực thể hiện có. Tuy nhiên, thực thể này cần truy cập vào Kho lưu trữ để hydrat hóa một gốc hoặc một tập hợp các rễ không thể truyền cho nó.

Vì vậy, việc tiếp cận một Repository từ một thực thể không phải là xấu của riêng mình , nó có thể có nhiều hình thức khác nhau mà kết quả từ một loạt các quyết định thiết kế khác nhau, từ thảm họa để có thể chấp nhận.


Tôi không đồng ý rằng một thực thể nên sử dụng một kho lưu trữ để truy cập một thực thể mà nó đã có mối quan hệ - bạn sẽ có thể duyệt qua biểu đồ đối tượng để truy cập thực thể đó. Sử dụng kho lưu trữ theo cách này là tuyệt đối không. Những gì tôi đang thảo luận ở đây là cho rằng thực thể chưa có tài liệu tham khảo, nhưng cần phải tạo một tài liệu trong một số điều kiện kinh doanh.
Paul T Davies

Chà, nếu bạn đã đọc tốt cho tôi, chúng tôi hoàn toàn đồng ý về điều đó ...
guillaume31

2

Đây là một lý do tôi không sử dụng Enums hoặc bảng tra cứu thuần túy trong miền của mình. Tính khẩn cấp và Trạng thái là cả hai trạng thái và có logic liên quan đến trạng thái trực tiếp thuộc về trạng thái (ví dụ: trạng thái nào tôi có thể chuyển sang trạng thái hiện tại của mình). Ngoài ra, bằng cách ghi lại trạng thái dưới dạng giá trị thuần túy, bạn sẽ mất thông tin như thời gian tác vụ ở trạng thái nhất định. Tôi đại diện cho các trạng thái như một hệ thống phân cấp lớp như vậy. (Trong 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>();
  }
}

Việc triển khai CompleteedTaskStatus sẽ khá giống nhau.

Có một số điều cần lưu ý ở đây:

  1. Tôi làm cho các constructor mặc định được bảo vệ. Điều này là do khung công tác có thể gọi nó khi kéo một đối tượng khỏi sự bền bỉ (cả EntityFramework Code-First và NHibernate đều sử dụng các proxy có nguồn gốc từ các đối tượng miền của bạn để thực hiện phép thuật của chúng).

  2. Nhiều người trong số các setters tài sản được bảo vệ cho cùng một lý do. Nếu tôi muốn thay đổi ngày kết thúc của Interval, tôi phải gọi hàm Interval.End () (đây là một phần của Thiết kế hướng miền, cung cấp các hoạt động có ý nghĩa thay vì Đối tượng miền thiếu máu.

  3. Tôi không hiển thị nó ở đây nhưng Nhiệm vụ cũng sẽ ẩn các chi tiết về cách nó lưu trữ trạng thái hiện tại của nó. Tôi thường có một danh sách Lịch sử được bảo vệ mà tôi cho phép công chúng truy vấn nếu họ quan tâm. Mặt khác, tôi hiển thị trạng thái hiện tại dưới dạng getter truy vấn HistoricalStates.Single (state.Duration.End == null).

  4. Hàm TransitionTo rất quan trọng vì nó có thể chứa logic về trạng thái nào là hợp lệ cho quá trình chuyển đổi. Nếu bạn chỉ có một enum, logic đó phải nằm ở chỗ khác.

Hy vọng, điều này giúp bạn hiểu cách tiếp cận DDD tốt hơn một chút.


1
Đây chắc chắn sẽ là cách tiếp cận chính xác nếu các trạng thái khác nhau có hành vi khác nhau như trong ví dụ mẫu trạng thái của bạn và chắc chắn nó cũng giải quyết được vấn đề được thảo luận. Tuy nhiên, tôi sẽ khó có thể biện minh cho một lớp cho mỗi trạng thái nếu chúng chỉ có các giá trị khác nhau, không phải hành vi khác nhau.
Paul T Davies

1

Đôi khi tôi đã cố gắng giải quyết vấn đề tương tự, tôi quyết định tôi muốn có thể gọi Task.UpdateTask () như thế, mặc dù tôi muốn nó là miền cụ thể, trong trường hợp của bạn có thể tôi sẽ gọi nó là Task.ChangeC Category (...) Để chỉ ra một hành động và không chỉ CRUD.

Dù sao, tôi đã thử vấn đề của bạn và nghĩ ra điều này ... hãy ăn bánh của tôi và ăn nó. Ý tưởng là các hành động diễn ra trên thực thể nhưng không tiêm tất cả các phụ thuộc. Thay vào đó công việc được thực hiện trong các phương thức tĩnh để chúng có thể truy cập trạng thái của thực thể. Nhà máy kết hợp tất cả lại với nhau và thông thường sẽ có mọi thứ nó cần để thực hiện công việc mà thực thể cần làm. Mã khách hàng bây giờ trông sạch sẽ và rõ ràng và thực thể của bạn không phụ thuộc vào bất kỳ nội dung lưu trữ nào.

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

    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.