← Барлық мақалаларға оралу

Мен жобаларымда қолданатын архитектура

Жарияланды

Бұл post ішінде мен real-world projects-та қолданатын және practice ішінде жақсы нәтиже берген application architecture туралы бөліскім келеді.

  • Application architecture жобалағанда мен мына requirements қоямын:
  • Application тестпен оңай жабылуы керек.
  • Оны жаңа .NET versions-қа оңай update және migrate жасауға болу керек.
  • Code simple болуы керек, сонда maintain ету жеңілдейді және new developers project-ке тезірек onboard болады.
  • Data provider оңай ауыстырылуы керек, мысалы database орнына external service. Бұл multiple development teams бар large companies ішінде жиі кездеседі.
  • Architecture-ға impact ететін libraries қолданбаған дұрыс, мысалы Revo, Marten, MediatR және тағы басқалар.

Практикада осы criteria-ға сай келмейтін көптеген projects көрдім. Оларды support ету және new features қосу қиын болды, әрі tests те болмады.

Бұл architecture clean және тек үш layer қолданады: Api, Core және Infrastructure. Бірақ оны traditional three-tier architecture-пен шатастырмау керек; біздің жағдайда dependency inversion principle қолданылады. Мысалы, business layer (Core) Infrastructure layer-ді directly қолданбайды және оған reference жасамайды.

Project structure example:

Api.csproj

├── Controllers
│  └── ProductController.cs

Core.csproj

├── RepositoriesContracts
│  └── IProductRepository.cs
├── UseCases
│  └── GetProduct
│    ├── GetProductUseCase.cs
│    └── IGetProductUseCase.cs

Infrastructure.csproj

├── Repositories
│  └── ProductRepository.cs

Тек үш layer қолдану long term ішінде confusion және abstraction leaks болдырмауға көмектеседі. Барлық business logic Core layer ішінде орналасады. Көбіне application core logic әртүрлі sources-тен data алу, оны aggregate және map жасау, кейін frontend-ке жіберу немесе incoming data өңдеп сақтау, не external service-ке forward етуге келіп тіреледі.

Негізгі requirements-тың бірі - business layer өзгертпей data source ауыстыру мүмкіндігі. Бұл маңызды, өйткені data sources жиі өзгереді: мысалы, бұрын REST арқылы жасалған нәрсе кейін Kafka-ға жіберілуі мүмкін; немесе бұрын database-тен келген data енді басқа team жариялаған REST API-дан келуі мүмкін.

Мұны қолдау үшін Core layer ішіндегі business logic data-мен тек interfaces арқылы жұмыс істейді. Менің жағдайда бұл interfaces data source type жасыру үшін Repository suffix қолданады.

Data source-тен қайтатын data model IProductRepository ішіндегі басқа model-ге mapped болуы керек. Бұл mapping Infrastructure layer ішінде жасалуы тиіс. Есте сақтаңыз: Core layer басқа project-ке reference жасамауы керек, бірақ Infrastructure Core-ға reference жасай алады, өйткені ол IProductRepository interface implementation береді.

Call chain-ді қарайық.

API layer ішіндегі endpoint Core layer-де орналасқан GetProductUseCase шақырады.

[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    private readonly IGetProductUseCase _getProductUseCase;

    public ProductController(IGetProductUseCase getProductUseCase)
    {
        _getProductUseCase = getProductUseCase;
    }
    [HttpGet("{id}")]
    public async Task<ActionResult<ProductResponse>> GetProduct(int id)
    {
        var productCoreResult = await _getProductUseCase.Execute(id);
        var productResponse = productCoreResult.ToResponse();

        return Ok(productResponse);
    }
}

Core layer ішінде use-case interface және оның implementation бар; implementation Core ішінде анықталған IProductRepository interface шақырады.

public class GetProductUseCase : IGetProductUseCase
{
    private readonly IProductRepository _productRepository;

    public GetProductUseCase(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<ProductCoreResult> Execute(int id)
    {
        var productDataResult = await _productRepository.GetById(id);
        var productCoreResult = productDataResult.ToResult();
        return productCoreResult;
    }
}

Infrastructure layer ішінде ProductRepository implementation және барлық database-related logic орналасады.

public class ProductRepository : IProductRepository
{
    public async Task<ProductDataResult> GetById(int id)
    {
        const string query = "SELECT Id, Name, Price FROM Products WHERE Id = @Id";

        using var connection = new SqlConnection("DbConnection");
        await connection.OpenAsync();

        var productSourceModel = await connection
            .QuerySingleOrDefaultAsync<ProductSourceModel>(query, new { Id = id });

        var productDataResult = productSourceModel.ToDataResult();

        return productDataResult;
    }
}

Бұл abstraction unit tests жазуды да жеңілдетеді. Біздің team Copilot қолдана бастағалы GitHub Copilot бұл task-ты өте жақсы орындайды.

Мен бұл architecture-ны үш жылдан астам қолдандым, және ол өте жақсы жұмыс істеді. Team ішіндегі басқа projects-та да қолдана бастадық. Осы уақыт ішінде data sources бірнеше рет өзгерді, бірақ major code rewrites қажет болған жоқ. Architecture simple болғандықтан, Copilot unit tests тиімді generates етеді, және көп жағдайда олар бірден жұмыс істейді.