文章出處

好久沒寫 DDD 領域驅動設計相關的文章了,嘎嘎!!!

這幾天在開發一個新的項目,雖然不是基于領域驅動設計的,但我想把 DDD 架構設計的一些東西運用在上面,但發現了很多問題,這些在之前的短消息項目中也有,比如我一直想重構短消息 Repository 實現的一些東西,但之前完全沒有頭緒,因為內部的實現錯綜復雜,牽一發而動全身,不知道從哪下手。

正好這次新項目的開發,讓我一步一步代碼設計,所以之前疑惑的問題,可以很清晰的分析并解決,解決問題的過程最終形成了一個 DDD 框架示例,大家可以參考下:

開源地址:https://github.com/yuezhongxin/DDD.Sample

1. 一點一滴-疑惑出現

疑惑就是 Repository 和 IUnitOfWork,以及 Application Service 的調用,這樣說可能很籠統,其實就是這三者如何更好的結合使用?并且讓它們各司其職,發揮出自己的最大作用,下面我舉個例子,可能大家更好理解一些。

首先,關于 IUnitOfWork 的定義實現,網上我搜了很多,很多都太一樣,比如有人這樣定義:

public interface IUnitOfWork
{
    IQueryable<TEntity> Set<TEntity>() where TEntity : class;
    TEntity Add<TEntity>(TEntity entity) where TEntity : class;
    TEntity Attach<TEntity>(TEntity entity) where TEntity : class;
    TEntity Remove<TEntity>(TEntity entity) where TEntity : class;
    void Commit();
    void Rollback();
    IDbContext Context { get; set; }//也有人添加這個
}

是不是感覺有點像 EF 中的 DbContext 呢?所以這也是一個疑惑點,IUnitOfWork 和 DbContext 是什么關系?比如有很多人疑惑:EF 中有 SaveChanges,為什么還有包裹一層 IUnitOfWork?這個問題之前已經討論了無數次,但這些都是紙上進行的,如果你實踐起來可能會是另一種感受。

如果 IUnitOfWork 按照上面的代碼進行設計,那 Repository 會是什么樣的呢?我們來看一下:

public class StudentRepository: IStudentRepository
{
    private IUnitOfWork _unitOfWork;

    public StudentRepository(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public Student Get(int id)
    {
        return _unitOfWork.Set<Student>().Where(x => x.Id == id).FirstOrDefault();
    }

    public void Add(Student student)
    {
        return _unitOfWork.Set<Student>().Add(student);
    }

    //....
}

上面是 Repository 的一種設計,也有人會這樣定義:private IQueryable<Student> _students;,然后在 StudentRepository 構造函數中進行賦值,但不管怎么設計,我們一般會將 Repository 和 IUnitOfWork 結合起來使用,這是一個重要的疑惑點:Repository 和 IUnitOfWork 真的有關系么???

另外,關于 Repository 返回 IQueryable?還是 IEnumerable?可以參考之前的一篇博文,這里我采用的是“概念上的合理”,即 Not IQueryable。

如果 Repository 按照上面的代碼進行設計,那 Application Service 會是什么樣的呢?我們來看一下:

public class StudentService : IStudentService
{
    private IUnitOfWork _unitOfWork;
    private IStudentRepository _studentRepository;

    public StudentService(IUnitOfWork unitOfWork, 
        IStudentRepository studentRepository)
    {
        _unitOfWork = unitOfWork;
        _studentRepository = studentRepository;
    }

    public Student Get(int id)
    {
        //return _unitOfWork.Set<Student>().Where(x => x.Id == id).FirstOrDefault();
        return _studentRepository.Get(id);
    }

    public bool Add(Student student)
    {
        //_unitOfWork.Add<Student>(student);
        _studentRepository.Add(student);
        return _unitOfWork.Commit();
    }

    //....
}

看到上面的代碼,我想你應該明白到底疑惑什么了?在 StudentService 中,StudentRepository 似乎變得有些多余,因為它所做的,UnitOfWork 也都可以做,隨著項目的復雜,這樣就會造成很多的問題,比如:

  • IUnitOfWork 的職責不明確。
  • Repository 的職責不明確。
  • Application Service 很困惑,因為它不知道該使用誰。
  • Application Service 的代碼越來越亂。
  • ....

2. 一步一步-分析解決

其實問題是可以進行溯源,如果一開始的設計變的很糟,那么接下來相關的其它設計,也會變的很糟,我們可以發現,上面出現問題的根源,其實就是一開始 IUnitOfWork 的設計問題,網上有關 IUnitOfWork 的設計實現,簡直五花八門,那我們應該相信誰呢?我們應該相信一開始關于 IUnitOfWork 的定義:http://martinfowler.com/eaaCatalog/unitOfWork.html

  • Unit of Work:維護受業務事務影響的對象列表,并協調變化的寫入和并發問題的解決。工作單元記錄在業務事務過程中對數據庫有影響的所有變化,操作結束后,作為一種結果,工作單元了解所有需要對數據庫做的改變,統一對數據庫操作。

上面的文字定義要結合 IUnitOfWork 圖中的實現,可以更好的理解一些。

我們發現,它和我們一開始的定義差別很大,比如:IQueryable<TEntity> Set<TEntity>() 這樣的定義實現,如果結合上面的定義就不是很恰當,IUnitOfWork 是記錄業務事務過程中對象列表的改變,平常我們所說對數據的增刪改查,你可以理解為 IUnitOfWork 和增刪改有關,和查詢不太相關。

所以,我們完全按照定義,再重新實現一次 IUnitOfWork:

public interface IUnitOfWork
{
    void RegisterNew<TEntity>(TEntity entity) where TEntity : class;
    void RegisterDirty<TEntity>(TEntity entity) where TEntity : class;
    void RegisterClean<TEntity>(TEntity entity) where TEntity : class;
    void RegisterDeleted<TEntity>(TEntity entity) where TEntity : class;
    bool Commit();
    void Rollback();
}

你可以看到,我們完全按照定義進行實現的,甚至是接口名字都一樣,下面我們看一下 IUnitOfWork 的具體實現:

public class EfUnitOfWork : DbContext, IUnitOfWork
{
    public EfUnitOfWork()
        : base("name=db_school")
    { }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>();
        modelBuilder.Entity<Teacher>();

        base.OnModelCreating(modelBuilder);
    }

    public void RegisterNew<TEntity>(TEntity entity)
        where TEntity : class
    {
        base.Set<TEntity>().Add(entity);
    }

    public void RegisterDirty<TEntity>(TEntity entity)
        where TEntity : class
    {
        base.Entry<TEntity>(entity).State = EntityState.Modified;
    }

    public void RegisterClean<TEntity>(TEntity entity)
        where TEntity : class
    {
        base.Entry<TEntity>(entity).State = EntityState.Unchanged;
    }

    public void RegisterDeleted<TEntity>(TEntity entity)
        where TEntity : class
    {
        base.Set<TEntity>().Remove(entity);
    }

    public bool Commit()
    {
        return base.SaveChanges() > 0;
    }

    public void Rollback()
    {
        throw new NotImplementedException();
    }
}

EfUnitOfWork 繼承 DbContext 和 IUnitOfWork,使用 EF 作為對象管理和持久化,這樣實現好像沒有什么問題,我們一般也是這樣做的,其實,這是一個坑,我們后面會講到,另外,之前說到 EF 中有 SaveChanges,為什么還有包裹一層 IUnitOfWork?其實就是說的上面代碼,因為所有的 IUnitOfWork 的實現,我們都是使用的 EF,既然如此,為啥不把 IUnitOfWork 這個空殼拿掉呢?有人會說了,IUnitOfWork 是 DDD 中的概念,巴拉巴拉,不能拿掉,要不然就不是 DDD 了呢?

上面的問題先放在這個,如果 EfUnitOfWork 這樣實現 IUnitOfWork,那 Repository 會怎樣?看下面的代碼:

public class StudentRepository: IStudentRepository
{
    private IQueryable<Student> _students;

    public StudentRepository(IUnitOfWork unitOfWork)
    {
        _students = unitOfWork.Set<Student>();//沒有了之前的 Set<TEntity>(),咋辦啊???
    }

    public Student Get(int id)
    {
        return _students.Where(x => x.Id == id).FirstOrDefault();
    }

    //....   
}

代碼進行到這,突然進行不下去了,咋辦呢?如果你回過頭去修改 IUnitOfWork 的接口,比如增加 Set<TEntity>(),這時候將又回到開始的問題,一切將前功盡棄,這么解決呢?這時候要停下來,思考 Repository 和 IUnitOfWork 的關系,也就是之前提到的,它們倆有關系么???

IUnitOfWork 的定義上面說過了,我們再看下 Repository 的定義:

  • Repository:協調領域和數據映射層,利用類似于集合的接口來訪問領域對象。

重點在于訪問,Repository 是用來訪問領域對象的,所以,之前我們在 IRepository 中定義 Add、Update、Renove 等等接口,我覺得這些不是很恰當,因為對象列表的更改,我們可以用 IUnitOfWork 記錄和實現,Repository 和 IUnitOfWork 應該是平級的概念,如果在 Repository 中去使用 IUnitOfWork,就有點違背其定義了。

IUnitOfWork 有其 EfUnitOfWork 的實現,難道我們還要搞一個 IRepository 對應的 EfRepository 實現?很顯然,如果這樣設計是非常冗余的,這時候,你是不是想到了我們還沒有提到的 IDbContext 呢???沒錯就是它,讓 UnitOfWork 和 Repository 都依賴于 IDbContext,而不是依賴于具體的 EF,這樣也就沒有了之前一直提到的 UnitOfWork 和 EF 的問題,具體怎么做呢?我們得先定義 IDbContext:

public interface IDbContext
{
    DbSet<TEntity> Set<TEntity>()
        where TEntity : class;

    DbEntityEntry<TEntity> Entry<TEntity>(TEntity entity)
        where TEntity : class;

    int SaveChanges();
}

IDbContext 的作用,就是提供對象列表的一切操作接口,接下來實現一個 SchoolDbContext:

public class SchoolDbContext : DbContext, IDbContext
{
    public SchoolDbContext()
        : base("name=db_school")
    { }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>();
        modelBuilder.Entity<Teacher>();

        base.OnModelCreating(modelBuilder);
    }
}

SchoolDbContext 有點像我們之前的 EfUnitOfWork,但它和 IUnitOfWork 沒有任何關系,它的作用就是提供具體的對象持久化和訪問,我們一般會放在 Infrastructure 層。另外,可以看到 SchoolDbContext 似乎并沒有實現 IDbContext 的接口,為什么呢?因為我們繼承了 DbContext,這些接口都在 DbContext 中進行進行實現了,你可以按 F12 進行查看。

接下來我們要對 EfUnitOfWork 進行改造了,IUnitOfWork 和 EF 已經沒有半毛錢關系了,所以我們命名直接去掉 Ef,具體實現代碼:

public class UnitOfWork : IUnitOfWork
{
    private IDbContext _dbContext;

    public UnitOfWork(IDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void RegisterNew<TEntity>(TEntity entity)
        where TEntity : class
    {
        _dbContext.Set<TEntity>().Add(entity);
    }

    public void RegisterDirty<TEntity>(TEntity entity) 
        where TEntity : class
    {
        _dbContext.Entry<TEntity>(entity).State = EntityState.Modified;
    }

    public void RegisterClean<TEntity>(TEntity entity) 
        where TEntity : class
    {
        _dbContext.Entry<TEntity>(entity).State = EntityState.Unchanged;
    }

    public void RegisterDeleted<TEntity>(TEntity entity) 
        where TEntity : class
    {
        _dbContext.Set<TEntity>().Remove(entity);
    }

    public bool Commit()
    {
        return _dbContext.SaveChanges() > 0;
    }

    public void Rollback()
    {
        throw new NotImplementedException();
    }
}

UnitOfWork 脫離了 EF, 是不是有種小清新的感覺?UnitOfWork 依賴于 IDbContext,所以,UnitOfWork 并不關心用哪種具體技術進行實現,你只需要給我對象訪問和持久化接口就可以了,下面再看一下之前進行不下去的 Repository:

public class StudentRepository: IStudentRepository
{
    private IQueryable<Student> _students;

    public StudentRepository(IDbContext dbContext)
    {
        _students = dbContext.Set<Student>();
    }

    public Student Get(int id)
    {
        return _students.Where(x => x.Id == id).FirstOrDefault();
    }
}

Repository 脫離了 IUnitOfWork,也有種小清新的感覺,Repository 的定義中說到,訪問領域對象的集合,這個我們可以從 IDbContext 中進行操作,再來看一下 Application Service 中的代碼:

public class StudentService : IStudentService
{
    private IUnitOfWork _unitOfWork;
    private IStudentRepository _studentRepository;
    private ITeacherRepository _teacherRepository;

    public StudentService(IUnitOfWork unitOfWork, 
        IStudentRepository studentRepository,
        ITeacherRepository teacherRepository)
    {
        _unitOfWork = unitOfWork;
        _studentRepository = studentRepository;
        _teacherRepository = teacherRepository;
    }

    public Student Get(int id)
    {
        return _studentRepository.Get(id);
    }

    public bool Add(string name)
    {
        var student = new Student { Name = name };
        var teacher = _teacherRepository.Get(1);
        teacher.StudentCount++;

        _unitOfWork.RegisterNew(student);
        _unitOfWork.RegisterDirty(teacher);
        return _unitOfWork.Commit();
    }
}

需要仔細看下 Add 中的方法,為了測試同一上下文中不同實體的操作,所以,我后面又增加了 Teacher 實體,因為這個方法比較有代表性,我大致分解下過程:

  1. 通過 _teacherRepository 獲取 Teacher 對象,注意這個操作通過 TeacherRepository 中的 IDbContext 完成。
  2. teacher 對象的學生數量 +1。
  3. 通過 _unitOfWork.RegisterNew 標識添加實體對象 student。
  4. 通過 _unitOfWork.RegisterDirty 標識修改實體對象 teacher。
  5. 通過 _unitOfWork.Commit 提交更改。

這個方法測試的主要目的是,通過 Repository 獲取對象,并進行相應修改,然后用 UnitOfWork 提交修改,另外,在這個過程中,也會有其它對象的一些操作,測試是可行的,可以很好的避免之前修改 Student 需要通過 StudentRepository,修改 Teacher 需要通過 TeacherRepository,然后 Commit 兩次,別問我為什么?因為我之前就這樣干過。。。

最后的最后,附上測試代碼:

public class StudentServiceTest
{
    private IStudentService _studentService;

    public StudentServiceTest()
    {
        var container = new UnityContainer();
        container.RegisterType<IDbContext, SchoolDbContext>();
        container.RegisterType<IUnitOfWork, UnitOfWork>();
        container.RegisterType<IStudentRepository, StudentRepository>();
        container.RegisterType<ITeacherRepository, TeacherRepository>();
        container.RegisterType<IStudentService, StudentService>();

        _studentService = container.Resolve<IStudentService>();
    }

    [Fact]
    public void GetByIdTest()
    {
        var student = _studentService.Get(1);
        Assert.NotNull(student);
    }

    [Fact]
    public void AddTest()
    {
        var result = _studentService.Add("xishuai");
        Assert.True(result);
    }
}

文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()