Не нарушает ли наследование нескольких классов из базового класса (DTO) какой-либо из принципов SOLID?
Я получаю несколько DTO (объект передачи данных) из базового DTO. У меня есть свойство в базе DTO (isUpdateAvailable), которое является общим для всего производного класса. У меня есть метод, который является общим для случая множественного использования, который берет базовый DTO и использует его либо напрямую, либо путем преобразования его в соответствующий производный DTO.
Я думаю, что это не очень хороший дизайн кода C#. Там не должно быть необходимости конвертировать. Более того, я также слышал, что такой дизайн кода нарушает принцип SOLID.
Я создал короткий пример кода, чтобы описать мою точку зрения. Пожалуйста, посмотрите:
public class UpdateNotification
{
public void ChromeNotification(MyBaseDto baseDto, NotificationType type)
{
OnUpdateAvailable(baseDto, type);
}
public void OutlookUpdateNotification(MyBaseDto baseDto,
NotificationType type)
{
OnUpdateAvailable(baseDto, type);
}
public void OnUpdateAvailable(MyBaseDto baseDto, NotificationType type)
{
if (type == NotificationType.Chrome)
{
// it uses baseDto.IsUpdateAvailable as well as it downcast it
to DerivedAdto and uses other properties
var derivedDto = baseDto as DerivedAdto;
}
if (type == NotificationType.Outlook)
{
// currently it just uses baseDto.IsUpdateAvailable
}
}
public enum NotificationType
{
Chrome,
Outlook
}
}
Я сосредотачиваюсь здесь на использовании объектов DTO, которые являются "MyBaseDto", "DerivedAdto" и "DerivedBdto". Моя текущая структура DTO выглядит следующим образом:
public abstract class MyBaseDto
{
public MyBaseDto(bool isUpdateAvailable)
{
IsUpdateAvailable = isUpdateAvailable;
}
public bool IsUpdateAvailable { get; }
}
public class DerivedAdto : MyBaseDto
{
public DerivedAdto(bool isUpdateAvailable)
: base(isUpdateAvailable)
{
}
public string PropertyA { get; set; }
}
public class DerivedBdto : MyBaseDto
{
public DerivedBdto(bool isUpdateAvailable)
: base(isUpdateAvailable)
{
}
}
Есть ли лучший дизайн для этих классов DTO?
Могу ли я разработать что-то вроде ниже или вы можете предложить лучший подход?
public abstract class MyBaseDto
{
public abstract bool IsUpdateAvailable { get; set;}
}
public class DerivedAdto : MyBaseDto
{
public override bool IsUpdateAvailable { get; set;}
public string PropertyA { get; set; }
}
public class DerivedBdto : MyBaseDto
{
public override bool IsUpdateAvailable { get; set;}
}
Большое спасибо.
3 ответа
Наследство не нарушает никаких принципов. Но это делает:
public void OnUpdateAvailable(MyBaseDto baseDto, NotificationType type)
{
if (type == NotificationType.Chrome)
{
// it uses baseDto.IsUpdateAvailable as well as it downcast it
to DerivedAdto and uses other properties
var derivedDto = baseDto as DerivedAdto;
}
if (type == NotificationType.Outlook)
{
// currently it just uses baseDto.IsUpdateAvailable
}
}
Если бы мне пришлось приспособить его к одному из принципов, это была бы инверсия зависимости (хотя это немного натянуто). Когда вы берете аргумент типа MyBaseDto
вы зависите от абстракции, которая может представлять любое количество производных классов. Но как только вы начнете приводить его к определенным производным типам, вы будете тесно связаны с каждым из этих производных типов.
Когда вы передаете параметр типа MyBaseDto
метод должен знать о своем типе только то, что MyBaseDto
, Как только вы начнете читать его, вы начнете терять преимущество сильной типизации. Что если кто-то ошибся NotificationType
и в результате вы пытаетесь разыграть baseDto
неправильный тип?
Технически вы можете просто изменить тип первого параметра на object
и затем приведите его к тому типу, который вы ожидаете. Но строгая типизация должна гарантировать, что вы уже знаете, какой у вас тип. И все, что вам нужно знать об объекте - это его объявленный тип - тип, указанный в аргументе.
Это не нарушение принципа подстановки Лискова, потому что это нарушение произойдет в классе (MyBaseDto
) и его производные классы. Проблема не в этих классах, а в том, как они используются.
В двух словах, если получите тип A
а затем начать проверку или приведение его в действие, чтобы увидеть, действительно ли это производный тип B
или же C
тогда что-то пошло не так. Все, что нас должно волновать - это объявленный тип, который мы получаем.
Некоторые ответы здесь могут указывать на то, что наследование отлично подходит для классов DTO на единственной основе, что позволяет вам писать общий код, а затем использовать его среди всех производных классов. Однако только потому, что это хорошая идея in general
с точки зрения ООП, это не означает, что это всегда хорошая идея с точки зрения ООП, пожалуйста, обратитесь к [1] для объяснения.
На мой взгляд, DTO (которые я понял, основываясь на опыте и чтении статей, написанных тяжеловесными пользователями), также относятся к этой категории. Я определенно не говорю, что вы не можете использовать наследование, они просто не очень хорошо работают с наследованием. Вы заканчиваете тем, что принудительно добавляете некоторый интерфейс, чтобы использовать некоторые DTO вместе в некотором методе, таком как IUserDto
чтобы сохранить пользовательские данные, некоторый другой базовый класс, чтобы объединить другие использования DTO. пожалуйста, обратитесь к [2] для ознакомления с актуальным исходным кодом, который поддерживает игру с примерно 4 миллионами загрузок приложений. Поэтому, вот еще один момент: DTO должен быть как можно более самоописуемым. Посмотрите на Объект и посмотрите, что "Данные передаются" You (Вы видите, что я здесь сделал?)
Если вы используете DTO, то существует высокая вероятность того, что будет что-то / кто-то, кто будет использовать эти данные, по крайней мере, я так полагаю. Если нет, то почему бы вам не передать данные? Отныне я буду называть потребителя "Клиентом". Как код, который вы указали в UpdateNotification
клиент точно не знает, какой у него DTO, так что попробуйте выйти из игры, что, как указывает @Scott Hannen, явно является запахом кода.
Для дополнительной информации, одно из замечаний, с которыми мы столкнулись во время разработки с унаследованными DTO, было то, что, когда мы сериализовали из базового класса с использованием некоторых известных сериализаторов, они не смогли (по замыслу) сериализовать, чтобы включить производные свойства, или когда мы попытались десериализовать в общий интерфейс или базовый класс, они не смогли включить производные свойства или полностью не сработали. Некоторые сериализаторы включают type
Информацию для решения этой проблемы см. в [3].
[1] Например, я работаю в компании, которая занимается в основном ролевыми играми. Допустим, мы пытаемся создать конкретный класс для монстра, используя наследование по следующему маршруту GameEntity→Movable→Monster→FireMonster→Dragon
, Что делать, если при планировании требуется изменить некоторые параметры Dragon
во время разработки? Как насчет FireGolem
который бы унаследовал от FireMonster
? Как насчет того, чтобы добавить немного FlyingMonster
позже в разработке. Куда это пойдет в таблице наследования? Является FireGolem
будет затронут? - Вероятно, это повлияет. - Поэтому в игровой индустрии мы всегда предпочитаем композицию наследованию. Я не говорю, что мы также не используем наследство.
[2] Пожалуйста, не шокируйте:) У них также есть частичные классы, которые генерируются с помощью инструментов автоматического создания. Смею вас отладить или попытаться понять, что между ними не так. Черт возьми, попытаться изменить одно свойство в IEntity
или другой класс, то вам придется исправить другие 50 других классов.
public interface IDto : IEntity, ICloneable, IHasId<long> {}
public abstract class DtoBase<T> : IDto where T : class, IDto, new(){}
public abstract class UserDtoBase<T> : DtoBase<T> where T : class, IDto, new()
public partial class ProfileDto : UserDtoBase<ProfileDto>
[3] После опробования нескольких сериализаторов, ServiceStack
был единственным, который правильно / сериализовал в / из интерфейса / производного / базового типа. Я не знаю о текущей ситуации с сериализаторами.
Ваш дизайн в порядке, поскольку наследование в ваших классах DTO. Это именно то, для чего предназначено наследование - оно позволяет вам написать общий код в базовом классе и затем использовать его среди всех производных классов.
Что странно - и я не совсем уверен, что вы пытаетесь сделать с помощью звонков OnUpdateAvailable
от ChromeNotification
& OutlookUpdateNotification
, В обоих случаях вы проходите в NotificationType
который, кажется, в любом случае предоставляет ту же информацию, что и вы при вызове двух методов.
Мне кажется, что вы не разработали свой UpdateNotification
Что ж. Ваши объекты DTO кажутся прекрасными все же.