Safetica > Resources > Azure Functions: Service factory v souladu se SOLID

Azure Functions: Service factory v souladu se SOLID

Problém

Při implementaci jedné funkce v rámci své práce jsem narazil na následující problém:

  • Měl jsem několik implementací jednoho service interface a potřeboval jsem injektovat jednu z nich na základě typu dat, která má zpracovat.
  • Tyto implementace měly další závislosti, takže jsem potřeboval factory, která za mě sestaví celý dependency tree pomocí .NET dependency injection.
  • Factory musela být také snadno rozšiřitelná, aniž by bylo nutné měnit kód.

S těmito kritérii v hlavě jsem hledal inspiraci na internetu. Bohužel jsem nenašel nic užitečného. Rozhodl jsem se tedy vyřešit problém sám a podělit se s vámi o nápad.

Pro účely demonstrace jsem vytvořil ukázkový projekt, který najdete na GitHubu. V tomto článku vás provedu kódem a vysvětlím myšlenku, abyste ji mohli aplikovat ve svém vlastním projektu.

Funkce

Příkladem je zde funkce AreaCalculator spouštěná přes HTTP, která dokáže vypočítat obsah běžných geometrických tvarů. Spouští se metodou POST s cestou calculateArea/{shape}. Parametr cesty {shape} reprezentuje tvar, pro který bude obsah vypočítán. Data tvaru jsou specifikována jako JSON v těle požadavku.

Jak vidíte, injektujeme zde IAreaCalculatorFactory. Factory poskytuje požadovanou implementaci kalkulátoru na základě typu tvaru parsovaného z parametru cesty. Kalkulátor poté vypočítá obsah konkrétního tvaru a výsledek je vrácen jako JSON v odpovědi OK.

public class AreaCalculator
{
    private readonly IAreaCalculatorFactory _calculatorFactory;
    private readonly ILogger<AreaCalculator> _logger;

    public AreaCalculator(
        IAreaCalculatorFactory calculatorFactory,
        ILogger<AreaCalculator> log)
    {
        _calculatorFactory = calculatorFactory;
        _logger = log;
    }

    [FunctionName(nameof(AreaCalculator))]
    [OpenApiOperation(operationId: "Run", tags: new[] { "name" })]
    [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
    [OpenApiParameter(
        name: "shape",
        In = ParameterLocation.Path,
        Required = true,
        Type = typeof(string),
        Description = "The shape (circle, square, rectangle, or triangle)"
    )]
    [OpenApiResponseWithBody(
        statusCode: HttpStatusCode.OK,
        contentType: "application/json",
        bodyType: typeof(AreaResult),
        Description = "The area of the specified shape, rounded to 2 decimal places"
    )]
    public async Task<IActionResult> RunAsync(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "calculateArea/{shape}")]
        HttpRequest req,
        string shape,
        CancellationToken ct)
    {
        _logger.LogInformation("Executing area calculator for shape: {shape}", shape);

        if (!Enum.TryParse(shape, true, out Shape shapeType))
            return new BadRequestObjectResult($"{shape} shape not supported");

        var calculator = _calculatorFactory.GetAreaCalculatorForShape(shapeType);
        var result = await calculator.CalculateAreaAsync(req.Body, ct);

        return new OkObjectResult(new AreaResult(result));
    }
}

Service Factory

Factory pro výpočet obsahu je přímočará. Drží slovník registrovaných služeb kalkulátoru pro každý dostupný typ tvaru a implementuje metodu GetAreaCalculatorForShape. Tato metoda získává registrované služby kalkulátoru přímo z IServiceProvider na základě poskytnutého typu tvaru.

public class AreaCalculatorFactory : IAreaCalculatorFactory
{
    private readonly Dictionary<Shape, Type> _registeredCalculators;
    private readonly IServiceProvider _serviceProvider;

    public AreaCalculatorFactory(
        IServiceProvider serviceProvider,
        Dictionary<Shape, Type> registeredCalculators)
    {
        _registeredCalculators = registeredCalculators;
        _serviceProvider = serviceProvider;
    }

    public IAreaCalculatorService GetAreaCalculatorForShape(Shape shape)
    {
        if (!_registeredCalculators.TryGetValue(shape, out var calculatorType) || calculatorType == null)
            throw new InvalidOperationException($"No calculator registered for shape {shape}");

        return (IAreaCalculatorService)_serviceProvider.GetRequiredService(calculatorType);
    }
}

Service Factory Builder

Nyní vás možná zajímá, jak lze toto všechno snadno inicializovat a zaregistrovat tak, aby to mohlo být později injektováno tam, kde je to potřeba. Service provider injektovaný do factory je neměnný a musí již obsahovat všechny služby kalkulátoru a jejich strom závislostí.

Kde a jak tedy zaregistrovat kalkulátory? Odpověď je AreaCalculatorFactoryBuilder. Bere ve svém konstruktoru kolekci služeb, což znamená, že můžete zaregistrovat konkrétní služby kalkulátoru pro konkrétní typy tvarů a zároveň pomocí metody RegisterCalculatorForType uložit mapování Shape → IAreaCalculatorService.

public class AreaCalculatorFactoryBuilder
{
    private readonly Dictionary<Shape, Type> _registeredCalculators;
    private readonly IServiceCollection _services;

    public AreaCalculatorFactoryBuilder(IServiceCollection services)
    {
        _registeredCalculators = new Dictionary<Shape, Type>();
        _services = services;
    }

    public AreaCalculatorFactoryBuilder RegisterCalculatorForType<TCalculator>(Shape shape)
        where TCalculator : IAreaCalculatorService
    {
        _services.AddScoped(typeof(TCalculator));
        _registeredCalculators.Add(shape, typeof(TCalculator));

        return this;
    }

    public IAreaCalculatorFactory Build(IServiceProvider serviceProvider)
    {
        return new AreaCalculatorFactory(serviceProvider, _registeredCalculators);
    }
}

Registrace služeb

Factory a její služby jsou poté registrovány tak, jak je uvedeno níže. AreaCalculatorFactoryBuilder je inicializován předem a metoda Build je volána uvnitř delegátu service provideru, když je potřeba implementace IAreaCalculatorFactory.

Vidíte zde také registraci vlastního JSON parseru používaného v rámci služeb kalkulátoru k deserializaci streamu těla požadavku do dat potřebných pro výpočet obsahu. To ukazuje, že služby poskytované factory mohou mít další závislosti injektované pomocí .NET dependency injection.

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddServices(this IServiceCollection services)
    {
        // Parsers
        services.AddScoped<ICustomParser, ShapeParser>();

        // Area calculators
        var areaCalculatorFactoryBuilder = new AreaCalculatorFactoryBuilder(services)
            .RegisterCalculatorForType<TriangleAreaCalculator>(Shape.Triangle)
            .RegisterCalculatorForType<SquareAreaCalculator>(Shape.Square)
            .RegisterCalculatorForType<RectangleAreaCalculator>(Shape.Rectangle)
            .RegisterCalculatorForType<CircleAreaCalculator>(Shape.Circle);

        services.AddScoped(x => areaCalculatorFactoryBuilder.Build(x));

        return services;
    }
}

Vysvětlení zbývajícího kódu

Nakonec zde je krátké vysvětlení implementací CircleAreaCalculator a ShapeParser. Jak vidíte, CircleAreaCalculator je implementací abstraktní třídy AreaCalculatorBase. AreaCalculatorBase se stará o parsování a poskytuje data konkrétním implementacím, aby mohly provádět požadované výpočty. Parser používá vestavěný .NET JSON serializer s vlastními možnostmi pro deserializaci streamu do požadovaného typu.

public class CircleAreaCalculator : AreaCalculatorBase<Circle>
{
    public CircleAreaCalculator(ICustomParser shapeParser) : base(shapeParser) { }

    protected override double CalculateArea(Circle circle)
        => Math.Pow(circle.R, 2) * Math.PI;
}

public abstract class AreaCalculatorBase<TShape> : IAreaCalculatorService
{
    private readonly ICustomParser _shapeParser;

    public AreaCalculatorBase(ICustomParser shapeParser)
    {
        _shapeParser = shapeParser;
    }

    private async Task<TShape> GetShapeAsync(Stream stream, CancellationToken ct)
        => await _shapeParser.ParseJsonBodyAsync<TShape>(stream, ct);

    public async Task<double> CalculateAreaAsync(Stream stream, CancellationToken ct)
        => CalculateArea(await GetShapeAsync(stream, ct));

    protected abstract double CalculateArea(TShape shape);
}

public class ShapeParser : ICustomParser
{
    private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };

    public async Task<T> ParseJsonBodyAsync<T>(Stream stream, CancellationToken ct = default)
    {
        T? val = await JsonSerializer.DeserializeAsync<T>(stream, SerializerOptions, ct);

        if (val == null)
        {
            throw new JsonException("Null payload");
        }

        return val;
    }
}

Shrnutí

Výsledné řešení je:

  • Snadno použitelné — využívá vestavěnou .NET dependency injection.
  • Otevřené pro rozšíření, uzavřené pro modifikaci — podporu pro další tvary lze přidat bez změny stávajícího kódu.
  • Snadné na implementaci — lze to udělat ve třech krocích:
  1. Implementujte service factory pomocí IServiceProvider pro využití .NET dependency injection.
  2. Implementujte service factory builder pro zjednodušení registrace služeb a vytváření factory.
  3. Poskytněte delegát service factory, který sestaví požadovanou factory v případě potřeby.

Ukázkový projekt lze spustit lokálně. Stačí spustit function app a otevřít v prohlížeči http://localhost:{function-app-port}/api/swagger/ui. Tam můžete použít Swagger UI k provádění API volání.

Similar posts