누수 추상화 및 깨끗한 아키텍처 템플릿

소프트웨어 개발의 Wikipedia에 따르면 누출 추상화는 추상화해야 할 세부 사항을 누출하는 추상화입니다. "누설 추상화"라는 용어는 2002년 Joel Spolsky에 의해 대중화되었습니다. Kiczales의 이전 논문은 불완전한 추상화와 관련된 몇 가지 문제를 설명하고 추상화 자체의 사용자 정의를 허용함으로써 문제에 대한 잠재적인 솔루션을 제시합니다.

시스템이 더 복잡해짐에 따라 소프트웨어 개발자는 더 많은 추상화에 의존해야 합니다. 각 추상화는 복잡성을 숨기려고 하므로 개발자가 현대 컴퓨팅의 다양한 변형을 "처리"하는 소프트웨어를 작성할 수 있습니다.
그러나 이 법칙은 신뢰할 수 있는 소프트웨어 개발자가 어쨌든 추상화의 기본 세부 사항을 배워야 한다고 주장합니다.

지난 1년 동안 나는 양식Jason Taylor Clean Architecture Solution Template에 영감을 준 클린 아키텍처의 여러 구현을 보았고 모두 공통 인터페이스IApplicationDbContext를 가지고 있습니다. 이 인터페이스는 사용 중인 기본 데이터 액세스 기술을 숨기는 것을 목표로 하지만 인터페이스를 보면 인터페이스가 Entity Framework Core에 연결되어 있음을 알 수 있습니다. 또한 Jason Taylor는 사용자가 항상 EF Core를 사용하고 있으며 변경되지 않을 것이라고 가정했을 것이라고 추측할 수 있습니다.

유출된 추상화가 실제로 작동하는지 봅시다. UpdateTodoListCommandHandler에 대한 단위 테스트를 구현하고 실제 프로젝트라고 가정하고 Handle 메서드 내부에 일부 논리가 있지만 각 시나리오에 대한 통합을 구현하지 않습니다. 다음은 실제 구현입니다.

namespace CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList
{
    public class UpdateTodoListCommand : IRequest
    {
        public int Id { get; set; }

        public string Title { get; set; }
    }

    public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
    {
        private readonly IApplicationDbContext _context;

        public UpdateTodoListCommandHandler(IApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
        {
            var entity = await _context.TodoLists.FindAsync(request.Id);

            if (entity == null)
            {
                throw new NotFoundException(nameof(TodoList), request.Id);
            }

            entity.Title = request.Title;

            await _context.SaveChangesAsync(cancellationToken);

            return Unit.Value;
        }
    }
}


엔티티가 존재하지 않을 때 NotFoundException가 발생하는지 확인하고 싶습니다.

[Test]
public void Update_TodoList_Throws_Exception_When_Entity_Does_Not_Exist()
{
    // Arrange
    var list = new List<TodoList>();
    var queryable = list.AsQueryable();

    var dbSet = new Mock<DbSet<TodoList>>();
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.Provider).Returns(queryable.Provider);
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.Expression).Returns(queryable.Expression);
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
    dbSet.Setup(d => d.FindAsync(It.IsAny<object[]>())).ReturnsAsync((object[] id) => list.SingleOrDefault(t => t.Id == (int)id[0]));

    var dbContext = new Mock<IApplicationDbContext>();
    dbContext.SetupGet(d => d.TodoLists).Returns(dbSet.Object);

    var sut = new UpdateTodoListCommandHandler(dbContext.Object);
    var command = new UpdateTodoListCommand { Id = 1, Title = "Test" };

    // Act
    var exception = Assert.ThrowsAsync<NotFoundException>(() => sut.Handle(command, new CancellationToken()));

    // Assert
    Assert.NotNull(exception);
}


보시다시피 저는 모의IApplicationDbContext를 시도했으며 인터페이스는 기본 구현을 숨기는 데 도움이 되어야 하지만 이러한 테스트를 구현하려면 모의DbSet 방법을 알아야 합니다. ApplicationDbContext 인터페이스 대신 IApplicationDbContext 클래스를 사용하면 결과가 동일하고 ApplicationDbContext 클래스를 모의하는 데 동일한 양의 코드가 필요합니다.

대신 저장소 패턴을 사용하고 기본 기술을 숨기고 쉽게 조롱할 수 있습니다.IApplicationDbContextITodoListRepository로 바꾸자.

public interface ITodoListRepository
{
    Task<TodoList> GetByIdAsync(int id);

    Task UpdateAsync(TodoList todoList);
}


그리고 핸들러:

public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
{
    private readonly ITodoListRepository _repository;

    public UpdateTodoListCommandHandler(ITodoListRepository repository)
    {
        _repository = repository;
    }

    public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
    {
        var entity = await _repository.GetByIdAsync(request.Id);

        if (entity == null)
        {
            throw new NotFoundException(nameof(TodoList), request.Id);
        }

        entity.Title = request.Title;

        await _repository.UpdateAsync(entity);

        return Unit.Value;
    }
}


이제 테스트 방법을 리팩토링할 수 있습니다.

[Test]
public void Test1()
{
    // Arrange
    var list = new List<TodoList>();

    var repository = new Mock<ITodoListRepository>();
    repository.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync((int id) => list.SingleOrDefault(t => t.Id == id));

    var command = new UpdateTodoListCommand { Title = "Test", Id = 1 };
    var sut = new UpdateTodoListCommandHandler(repository.Object);

    // Act
    var exception = Assert.ThrowsAsync<NotFoundException>(async () => await sut.Handle(command, new CancellationToken()));

    // Act
    Assert.NotNull(exception);
    Assert.AreEqual(typeof(NotFoundException), exception.GetType());
}

좋은 웹페이지 즐겨찾기