Он прав и неправ одновременно.
С такими вещами, как BinaryFormatter
, это не проблема; сериализованный поток содержит метаданные полного типа, поэтому если у вас есть:
[Serializable] abstract class SomeBase {}
[Serializable] class SomeConcrete : SomeBase {}
...
SomeBase obj = new SomeConcrete();
и сериализуйте obj
, затем он включает в себя «Я SomeConcrete
» в потоке. Это делает жизнь простой, но многословной, особенно когда повторяется. Он также хрупкий, так как требует такой же реализации при десериализации; плохо для разных реализаций клиент / сервер или для длительного хранения.
С XmlSerializer
(о котором, как мне кажется, говорит блог), метаданных нет, но имена элементов (или атрибуты xsi:type
) используются, чтобы помочь определить, какой из них используется. Чтобы это работало, сериализатору необходимо заранее знать , какие имена соответствуют каким типам.
Самый простой способ сделать это - украсить базовый класс подклассами, о которых мы знаем. Сериализатор может затем проверить каждый из них (и любые дополнительные специфичные для xml атрибуты), чтобы выяснить, что, когда он видит элемент <someConcreteType>
, он сопоставляется с экземпляром SomeConcrete
(обратите внимание, что имена не должны совпадать, поэтому он не может просто искать его по имени).
[XmlInclude(typeof(SomeConcrete))]
public abstract class SomeBase {}
public class SomeConcrete : SomeBase {}
...
SomeBase obj = new SomeConcrete();
XmlSerializer ser = new XmlSerializer(typeof(SomeBase));
ser.Serialize(Console.Out, obj);
Однако, если он пурист (или данные недоступны), то есть альтернатива; Вы можете указать все эти данные отдельно через перегруженный конструктор для XmlSerializer
. Например, вы можете найти набор известных подтипов из конфигурации (или, может быть, контейнер IoC) и настроить конструктор вручную. Это не очень сложно, но достаточно сложно, чтобы это того не стоило, если вам действительно не нужно 1026 *.
public abstract class SomeBase { } // no [XmlInclude]
public class SomeConcrete : SomeBase { }
...
SomeBase obj = new SomeConcrete();
Type[] extras = {typeof(SomeConcrete)}; // from config
XmlSerializer ser = new XmlSerializer(typeof(SomeBase), extras);
ser.Serialize(Console.Out, obj);
Кроме того, с помощью XmlSerializer
, если вы идете по пользовательскому маршруту ctor, важно кэшировать и повторно использовать экземпляр XmlSerializer
; в противном случае новая динамическая сборка загружается за использование - очень дорого (их нельзя выгружать). Если вы используете простой конструктор, он кэширует и повторно использует модель, поэтому используется только одна модель.
YAGNI требует, чтобы мы выбрали самый простой вариант; использование [XmlInclude]
устраняет необходимость в сложном конструкторе и устраняет необходимость беспокоиться о кэшировании сериализатора. Другой вариант есть и полностью поддерживается.
Ваши последующие вопросы:
По «фабричному шаблону» он говорит о случае, когда ваш код не знает о SomeConcrete
, возможно, из-за IoC / DI или подобных фреймворков; так что вы можете иметь:
SomeBase obj = MyFactory.Create(typeof(SomeBase), someArgsMaybe);
Который вычисляет соответствующую конкретную реализацию SomeBase
, создает ее экземпляр и возвращает обратно. Очевидно, что если наш код не знает о конкретных типах (поскольку они указаны только в файле конфигурации), то мы не можем использовать XmlInclude
; но мы можем проанализировать данные конфигурации и использовать подход ctor (как указано выше). В действительности, в большинстве случаев XmlSerializer
используется с объектами POCO / DTO, так что это искусственная проблема.
И повторно интерфейсы; то же самое, но более гибкий (интерфейс не требует иерархии типов). Но XmlSerializer
не поддерживает эту модель. Честно говоря, жесткий; это не его работа. Его задача - позволить вам хранить и переносить данные. Не реализация. Любые объекты, созданные в xml-схеме, не будут иметь методов . Данные конкретные, а не абстрактные. Пока вы думаете "DTO", дебаты по интерфейсу не являются проблемой. Люди, которые недовольны тем, что не могут использовать интерфейсы на своей границе, не приняли разделения интересов, то есть пытаются:
Client runtime entities <---transport---> Server runtime entities
, а не менее ограничительный
Client runtime entities <---> Client DTO <--- transport--->
Server DTO <---> Server runtime entities
Теперь во многих (большинстве?) Случаях DTO и сущности могут быть одинаковыми; но если вы пытаетесь сделать что-то, что не нравится транспорту, введите DTO; не боритесь с сериализатором. Та же логика применяется, когда люди пытаются написать свой объект:
class Person {
public string AddressLine1 {get;set;}
public string AddressLine2 {get;set;}
}
как xml формы:
<person>
<address line1="..." line2="..."/>
</person>
Если вы хотите этого, введите DTO, соответствующий транспорту, и сопоставьте вашу сущность с DTO:
// (in a different namespace for the DTO stuff)
[XmlType("person"), XmlRoot("person")]
public class Person {
[XmlElement("address")]
public Address Address {get;set;}
}
public class Address {
[XmlAttribute("line1")] public string Line1 {get;set;}
[XmlAttribute("line2")] public string Line2 {get;set;}
}
Это также относится ко всем тем другим игрокам, как:
- зачем мне конструктор без параметров?
- зачем мне нужен сеттер для свойств моей коллекции?
- почему я не могу использовать неизменяемый тип?
- почему мой тип должен быть публичным?
- как мне управлять сложными версиями?
- как мне работать с разными клиентами с разным расположением данных?
- почему я не могу использовать интерфейсы?
- и т. Д. И т. П.
У вас не всегда есть эти проблемы; но если вы это сделаете - введите DTO (или несколько), и ваши проблемы исчезнут. Возвращаясь к вопросу об интерфейсах; типы DTO могут не основываться на интерфейсе, но могут быть типы времени выполнения / бизнес-типы.