Вернуть производный класс из базового класса

Мы разрабатываем внутренний пакет NuGet, который содержит классы ServiceRes и ServiceRes<T>, которые можно использовать для переноса ответа уровня сервиса в общую структуру. Это код, который у нас есть на данный момент:

public class ServiceRes
{
    protected internal ServiceRes() { }

    public bool IsSuccess { get; set; }

    public string? Message { get; set; }

    public static ServiceRes Success()
    {
        return Success(message: null);
    }

    public static ServiceRes Success(string? message)
    {
        return new ServiceRes
        {
            IsSuccess = true,
            Message = message
        };
    }

    public static ServiceRes MapFrom<TPrev>(ServiceRes<TPrev> othr)
        where TPrev : class
    {
        return new ServiceRes
        {
            IsSuccess = othr.IsSuccess,
            Message = othr.Message
        };
    }
}

public class ServiceRes<T> : ServiceRes where T : class
{
    protected internal ServiceRes() { }

    public T? ResponseObj { get; set; }

    public static ServiceRes<T> Success(T responseObj)
    {
        return new ServiceRes<T>
        {
            IsSuccess = true,
            Message = null,
            ResponseObj = responseObj
        };
    }

    public static new ServiceRes<T> MapFrom<TPrev>(ServiceRes<TPrev> othr)
        where TPrev : class
    {
        return MapFrom(othr, null);
    }

    public static ServiceRes<T> MapFrom<TPrev>(ServiceRes<TPrev> othr, Func<TPrev?, T?>? responseObjMapper)
        where TPrev : class
    {
        return new ServiceRes<T>
        {
            IsSuccess = othr.IsSuccess,
            Message = othr.Message,
            ResponseObj = responseObjMapper?.Invoke(othr.ResponseObj)
        };
    }
}

У нас есть проект веб-приложения .NET Core, который ссылается на наш частный пакет NuGet. Мы можем использовать его следующим образом:

public class ServiceResTest
{
    private class Obj1
    {
        public string? Name { get; set; }
    }

    private class Obj2
    {
        public string? Value { get; set; }
    }
    
    public void Test()
    {
        // base usage
        var x1 = ServiceRes.Success();
        var x2 = ServiceRes.Success(message: "User created successfully");
        var x3 = ServiceRes<Obj1>.Success(new Obj1());

        // mapping
        var x4 = ServiceRes.MapFrom(x3); // ServiceResult<Obj1> -> ServiceResult
        var x5 = ServiceRes<Obj2>.MapFrom(x3); // ServiceResult<Obj1> -> ServiceResult<Obj2> (ResponseObj = null)
        var x6 = ServiceRes<Obj2>.MapFrom(x3, x => new Obj2() // ServiceResult<Obj1> -> ServiceResult<Obj2>
        {
            Value = $"{x?.Name}_value"
        }); 
    }
}

Однако в нашем веб-приложении есть некоторые расширенные варианты использования, которые требуют расширения класса BaseService<T>. Мы хотели бы иметь возможность сопоставлять ServiceResult<T> с ServiceResultExt<T>, желательно аналогично существующим методам MapFrom. Комментарии ниже указывают, где процесс завершается неудачей.

public class ServiceResExt<T> : ServiceRes<T> where T : class
{
    public int FailedLoginsCount { get; set; }

    public static ServiceResExt<T> SuccessExt(T responseObj, int failedLoginsCount)
    {
        var baseRes = ServiceRes<T>.Success(responseObj);
        // map ServiceRes<T> to ServiceResExt<T>
        var x1 = ServiceResExt<T>.MapFrom(baseRes); // x1 is of type ServiceRes<T>
        // we would like to assing parameter failedLoginsCount to property FailedLoginsCount 
        x1.FailedLoginsCount = failedLoginsCount;
        return x1;
    }
}

Как лучше всего обрабатывать случаи, когда у нас есть класс, расширяющий ServiceRes<T> (например, ServiceResExt<T>), и мы хотели бы использовать существующие методы (например, Success), но сопоставить ответ с ServiceResExt<T>? Может ли логика отображения производных классов быть размещена внутри класса ServiceRes<T>, чтобы она была общей для всех классов, которые наследуют от него (мы не хотим, чтобы кто-либо вручную создавал экземпляр класса ServiceRes, следовательно, конструктор protected internal)? Может ли ServiceRes<T> вернуть экземпляр производного класса?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
56
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете использовать защищенный виртуальный метод для создания правильного типа объекта при сопоставлении.

Я бы написал это примерно так:

public class ServiceRes
{
    protected ServiceRes(bool isSuccess, string message) 
        => ( IsSuccess, Message ) = ( isSuccess, message );

    public static ServiceRes Success => new (true, "");
    public static ServiceRes Fail(string message) => new(false, message);
    public bool IsSuccess { get; }
    public string Message { get; }
}

public class ServiceRes<T> : ServiceRes
    where T : class
{
    private readonly T responseObj;
    protected ServiceRes(T response, bool isSuccess, string message) : base(isSuccess, message)
        => responseObj = response;
    protected virtual ServiceRes<T2> Create<T2>(T2 v) where T2 : class => new(v, IsSuccess, Message);

    public T Response => IsSuccess ? responseObj : throw new InvalidOperationException();
    public new static ServiceRes<T> Success(T response)  => new(response, true,  "");
    public new static ServiceRes<T> Fail(string message) => new(null, false,message);
    public ServiceRes<T2> MapFrom<T2>(Func<T, T2> select) where T2 : class 
        => IsSuccess ? Create(select(Response)) : Create<T2>(null);
}

public class ServiceResExt<T> : ServiceRes<T>
    where T : class
{
    protected ServiceResExt(T value, bool isSuccess, string message, int failedLoginsCount) : base(value, isSuccess, message) 
        => FailedLoginsCount = failedLoginsCount;
    protected virtual ServiceResExt<T2> CreateExt<T2>(T2 v) where T2 : class
        => new(v, IsSuccess, Message, FailedLoginsCount);
    protected override ServiceRes<T2> Create<T2>(T2 v) => CreateExt(v);

    public int FailedLoginsCount { get; }
    public new static ServiceResExt<T> Success(T response, int failedLoginsCount) => new(response, true, "", failedLoginsCount);
    public new static ServiceResExt<T> Fail(string message, int failedLoginsCount) => new(null, false, message, failedLoginsCount);
    public new ServiceResExt<T2> MapFrom<T2>(Func<T, T2> select) where T2 : class 
        => IsSuccess ? CreateExt(select(Response)) : CreateExt<T2>(null);
}

Нет смысла отображать базовый класс ServiceRes, поэтому в нем отсутствуют такие методы.

И я не уверен, что это достигнет вашей цели — наличие логики отображения в базе ServiceRes<T>. Но в этом не должно быть такой уж логики, и вы можете заменить ServiceResExt.MapFrom на (ServiceResExt<T2>)base.MapFrom(select), даже если это будет несколько менее безопасно, если будут добавлены дополнительные расширения.

Но он должен сохранять один и тот же внешний тип до и после отображения, независимо от типа ссылки. А также вернуть тот же внешний тип ссылки из-за сокрытия метода MapFrom.

Возможно, вам также придется добавить немного ?, я написал это без ссылочных типов, допускающих значение NULL. Я бы порекомендовал сделать эти классы неизменяемыми, поскольку публичные сеттеры кажутся странными и опасными.

Другие вопросы по теме