Mengakses Gudang dari Domain

14

Katakanlah kita memiliki sistem pencatatan tugas, ketika tugas dicatat, pengguna menentukan kategori dan tugas tersebut default ke status 'Luar Biasa'. Anggaplah dalam contoh ini bahwa Kategori dan Status harus diimplementasikan sebagai entitas. Biasanya saya akan melakukan ini:

Lapisan Aplikasi:

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

Kesatuan:

public class Task
{
    //...

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

Saya melakukannya seperti ini karena saya secara konsisten diberitahu bahwa entitas tidak boleh mengakses repositori, tetapi akan lebih masuk akal bagi saya jika saya melakukan ini:

Kesatuan:

public class Task
{
    //...

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

Repositori status adalah dependecy yang disuntikkan, jadi tidak ada ketergantungan nyata, dan ini terasa lebih bagi saya karena itu adalah domain yang membuat keputusan bahwa tugas default ke luar biasa. Versi sebelumnya terasa seperti aplikasi layeer yang membuat keputusan itu. Adakah mengapa kontrak repositori sering kali berada dalam domain jika ini tidak boleh menjadi kemungkinan?

Berikut adalah contoh yang lebih ekstrem, di sini domain memutuskan urgensi:

Kesatuan:

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

Tidak ada cara Anda ingin lulus dalam semua versi Urgency yang mungkin, dan tidak mungkin Anda ingin menghitung logika bisnis ini di lapisan aplikasi, jadi tentu ini akan menjadi cara yang paling tepat?

Jadi apakah ini alasan yang sah untuk mengakses repositori dari domain?

EDIT: Ini juga bisa terjadi pada metode non-statis:

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;
    }
}
Paul T Davies
sumber

Jawaban:

8

Anda mencampurkan

entitas tidak boleh mengakses repositori

(yang merupakan saran yang bagus)

dan

lapisan domain tidak boleh mengakses repositori

(Yang mungkin saran yang buruk selama repositori Anda adalah bagian dari lapisan domain, bukan lapisan aplikasi). Sebenarnya, contoh Anda tidak menunjukkan kasus di mana entitas mengakses repositori, karena Anda menggunakan metode statis yang bukan milik entitas apa pun.

Jika Anda tidak ingin menempatkan logika kreasi itu menjadi metode statis kelas entitas, Anda bisa memperkenalkan kelas pabrik yang terpisah (sebagai bagian dari lapisan domain!) Dan menempatkan logika kreasi di sana.

Sunting: ke Anda Update contoh : mengingat bahwa _urgencyRepositorydan statusRepository merupakan anggota kelas Task, didefinisikan sebagai semacam antarmuka, Anda sekarang perlu menyuntikkannya ke Taskentitas apa pun sebelum Anda dapat menggunakannya Updatesekarang (misalnya dalam konstruktor Tugas). Atau Anda mendefinisikan mereka sebagai anggota statis, tetapi waspadalah, yang dapat dengan mudah menyebabkan masalah multi-threading, atau hanya masalah ketika Anda membutuhkan repositori yang berbeda untuk entitas Tugas yang berbeda secara bersamaan.

Desain ini membuatnya sedikit lebih sulit untuk membuat Taskentitas dalam isolasi, sehingga lebih sulit untuk menulis tes unit untuk Taskentitas, lebih sulit untuk menulis tes otomatis tergantung pada entitas Tugas, dan Anda menghasilkan sedikit lebih banyak memori overhead, karena setiap entitas Tugas sekarang perlu berpendapat bahwa dua referensi untuk repositori. Tentu saja, itu bisa ditoleransi dalam kasus Anda. Di sisi lain, membuat kelas utilitas terpisah TaskUpdateryang membuat referensi ke repositori yang tepat mungkin sering atau paling tidak kadang-kadang solusi yang lebih baik.

Bagian yang penting adalah: TaskUpdaterakan tetap menjadi bagian dari lapisan domain! Hanya karena Anda meletakkan pembaruan atau kode kreasi di kelas yang terpisah tidak berarti Anda harus beralih ke lapisan lain.

Doc Brown
sumber
Saya telah mengedit untuk menunjukkan ini berlaku untuk metode non-statis sebanyak yang statis. Saya tidak pernah benar-benar berpikir metode pabrik tidak menjadi bagian dari suatu entitas.
Paul T Davies
@ PaulTDavies: lihat hasil edit saya
Doc Brown
Saya setuju dengan apa yang Anda katakan di sini, tetapi saya akan menambahkan bagian singkat yang menggambarkan titik yang Status = _statusRepository.GetById(Constants.Status.OutstandingId)merupakan aturan bisnis , yang dapat Anda baca sebagai "Bisnis menentukan status awal semua tugas akan menjadi Luar Biasa" dan inilah sebabnya baris kode itu tidak termasuk dalam repositori, yang hanya menyangkut manajemen data melalui operasi CRUD.
Jimmy Hoffa
@JimmyHoffa: hm, tidak ada seorang pun di sini yang menyarankan untuk meletakkan garis semacam itu ke dalam salah satu kelas repositori, baik OP maupun saya - jadi apa maksud Anda?
Doc Brown
Saya sangat menyukai gagasan dari TaskUpdater sebagai layanan domian. Entah bagaimana itu tampak seperti sedikit kebohongan hanya untuk mempertahankan prinsip-prinsip DDD, tetapi itu berarti bahwa saya dapat menghindari menyuntikkan repositori setiap kali saya menggunakan Tugas.
Paul T Davies
6

Saya tidak tahu apakah contoh status Anda adalah kode nyata atau di sini hanya untuk demonstrasi, tetapi tampaknya aneh bagi saya bahwa Anda harus menerapkan Status sebagai Entitas (belum lagi disebut Agregat Root) ketika ID-nya didefinisikan konstan dalam kode - Constants.Status.OutstandingId. Bukankah itu mengalahkan tujuan status "dinamis" yang dapat Anda tambahkan sebanyak yang Anda inginkan dalam basis data?

Saya akan menambahkan bahwa dalam kasus Anda, konstruksi a Task(termasuk pekerjaan mendapatkan status yang tepat dari StatusRepository jika perlu) mungkin layak TaskFactorydaripada tetap di dalam Taskdirinya sendiri, karena itu adalah kumpulan objek non-sepele.

Tapi:

Saya secara konsisten diberitahu bahwa entitas tidak boleh mengakses repositori

Pernyataan ini tidak tepat dan terlalu sederhana, paling menyesatkan dan berbahaya paling buruk.

Sudah cukup umum diterima dalam arsitektur berbasis domain bahwa suatu entitas seharusnya tidak tahu cara menyimpannya sendiri - itulah prinsip ketidaktahuan yang gigih. Jadi tidak ada panggilan ke repositori untuk menambahkan dirinya ke repositori. Haruskah ia tahu bagaimana (dan kapan) menyimpan entitas lain ? Sekali lagi, tanggung jawab itu tampaknya berada di objek lain - mungkin objek yang menyadari konteks eksekusi dan kemajuan keseluruhan kasus penggunaan saat ini, seperti layanan lapisan Aplikasi.

Bisakah entitas menggunakan repositori untuk mengambil entitas lain ? 90% dari waktu yang seharusnya tidak harus, karena entitas yang dibutuhkan biasanya dalam lingkup agregat atau diperoleh dengan melintasi objek lain. Tetapi ada kalanya mereka tidak. Jika Anda mengambil struktur hierarkis, misalnya, entitas sering perlu mengakses semua leluhur mereka, cucu tertentu, dll. Sebagai bagian dari perilaku intrinsik mereka. Mereka tidak memiliki referensi langsung ke kerabat terpencil ini. Akan merepotkan untuk menyerahkan kerabat ini kepada mereka sebagai parameter operasi. Jadi mengapa tidak menggunakan Repositori untuk mendapatkannya - asalkan mereka adalah akar agregat?

Ada beberapa contoh lainnya. Masalahnya adalah, terkadang ada perilaku yang tidak dapat Anda tempatkan dalam layanan Domain karena tampaknya cocok dengan entitas yang ada. Namun, entitas ini perlu mengakses Repositori untuk menghidrasi root atau kumpulan root yang tidak dapat diteruskan ke sana.

Jadi mengakses Repository dari Entitas tidak buruk dalam dirinya sendiri , dapat mengambil bentuk yang berbeda yang hasil dari dari berbagai keputusan desain mulai dari bencana ke diterima.

guillaume31
sumber
Saya tidak setuju bahwa suatu entitas harus menggunakan repositori untuk mengakses entitas yang sudah memiliki hubungannya dengan - Anda harus dapat melintasi grafik objek untuk mengakses entitas itu. Menggunakan repositori dengan cara ini adalah mutlak tidak, tidak. Apa yang saya diskusikan di sini adalah bahwa entitas belum memiliki referensi, tetapi perlu membuatnya di bawah beberapa kondisi bisnis.
Paul T Davies
Nah, jika Anda sudah membaca saya dengan baik, kami sepenuhnya setuju untuk itu ...
guillaume31
2

Ini adalah salah satu alasan saya tidak menggunakan Enums atau tabel pencarian murni dalam domain saya. Urgensi dan Status adalah kedua Negara dan ada logika yang terkait dengan keadaan yang termasuk dalam keadaan secara langsung (misalnya status apa yang dapat saya transisikan ke keadaan saat ini). Juga, dengan merekam keadaan sebagai nilai murni Anda kehilangan informasi seperti berapa lama tugas itu dalam keadaan tertentu. Saya mewakili status sebagai hierarki kelas seperti itu. (Dalam 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>();
  }
}

Implementasi CompletedTaskStatus akan hampir sama.

Ada beberapa hal yang perlu diperhatikan di sini:

  1. Saya membuat konstruktor default terlindungi. Ini agar kerangka dapat menyebutnya saat menarik objek dari kegigihan (baik EntityFramework Code-first dan NHibernate menggunakan proxy yang berasal dari objek domain Anda untuk melakukan keajaibannya).

  2. Banyak pemukim properti dilindungi karena alasan yang sama. Jika saya ingin mengubah tanggal akhir suatu Interval, saya harus memanggil fungsi Interval.End () (ini adalah bagian dari Desain Domain Driven, memberikan operasi yang berarti daripada Objek Domain Anemik.

  3. Saya tidak menunjukkannya di sini, tetapi Task juga akan menyembunyikan detail bagaimana ia menyimpan statusnya saat ini. Saya biasanya memiliki daftar HistoricalStates yang dilindungi yang saya izinkan untuk ditanyakan kepada publik jika mereka tertarik. Kalau tidak, saya mengekspos keadaan saat ini sebagai pengambil yang menanyakan HistoricalStates.Single (state.Duration.End == null).

  4. Fungsi TransitionTo signifikan karena dapat berisi logika tentang negara mana yang valid untuk transisi. Jika Anda hanya memiliki enum, logika itu harus terletak di tempat lain.

Semoga ini membantu Anda memahami pendekatan DDD sedikit lebih baik.

Michael Brown
sumber
1
Ini tentu akan menjadi pendekatan yang tepat jika negara yang berbeda memiliki perilaku yang berbeda seperti dalam contoh pola negara Anda, dan tentu saja memecahkan masalah yang dibahas juga. Namun, saya akan merasa sulit untuk membenarkan kelas untuk setiap negara jika mereka hanya memiliki nilai yang berbeda, bukan perilaku yang berbeda.
Paul T Davies
1

Saya telah mencoba untuk memecahkan masalah yang sama untuk beberapa waktu, saya memutuskan saya ingin dapat memanggil Task.UpdateTask () seperti itu, meskipun saya lebih suka itu akan menjadi spesifik domain, dalam kasus Anda mungkin saya akan menyebutnya Task.ChangeCategory (...) untuk menunjukkan suatu tindakan dan bukan hanya CRUD.

Bagaimanapun, saya mencoba masalah Anda dan datang dengan ini ... ambil kue saya dan memakannya juga. Idenya adalah bahwa tindakan terjadi pada entitas tetapi tanpa injeksi semua dependensi. Sebaliknya pekerjaan dilakukan dalam metode statis sehingga mereka dapat mengakses status entitas. Pabrik menyatukan semuanya dan biasanya akan memiliki semua yang diperlukan untuk melakukan pekerjaan yang perlu dilakukan entitas. Kode klien sekarang terlihat bersih dan jelas dan entitas Anda tidak bergantung pada injeksi repositori.

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

    }
}
Mike
sumber