C#. Как правильно написать конвертацию одного абстрактного типа к другому?
Предисловие. Чтобы как-то описать проблему я написал консольное приложение (см. код ниже).
Суть задачи. Пусть есть два протокола передачи сообщений: St и Gv. Есть два абстрактных типа:
- StMessage
- GvMessage
Нужно реализовать конвертацию сообщений типа StMessage в GvMessage. Реализации StMessage содержат уникальный набор полей для своего типа сообщений. После конвертации необходимо вывести сообщение в консоли.
Вот, что у меня получилось. Кода достаточно много, для удобства разбил на регионы, поэтому лучше посмотреть его в редакторе кода. Код:
using System;
namespace TestHelloWorld.ConsoleApp
{
internal static class Program
{
private static void Main(string[] args)
{
LaunchTestOfStatusMessageConverting();
LaunchTestOfSpeedMessageConverting();
}
private static void LaunchTestOfStatusMessageConverting()
{
var stMessage = new StMessageStatus
{
Status = "Pending"
};
var converter = GetConverterForMessage(stMessage);
var gvMessage = converter.Convert(stMessage);
var printer = new ConsoleMessagePrinter();
printer.PrintMessage(gvMessage);
}
private static void LaunchTestOfSpeedMessageConverting()
{
var stMessage = new StMessageSpeed
{
Speed = 42
};
var converter = GetConverterForMessage(stMessage);
var gvMessage = converter.Convert(stMessage);
var printer = new ConsoleMessagePrinter();
printer.PrintMessage(gvMessage);
}
private static IMessageConverter GetConverterForMessage(StMessage stMessage)
{
switch (stMessage.MessageType)
{
case StMessageType.Speed:
return new SpeedMessageConverter();
case StMessageType.Status:
return new StatusMessageConverter();
default:
throw new ArgumentOutOfRangeException();
}
}
}
#region [ GvMessages ]
public enum GvMessageType
{
Speed,
Status
}
public abstract class GvMessage
{
public abstract GvMessageType MessageType { get; }
}
public class GvMessageSpeed : GvMessage
{
public override GvMessageType MessageType => GvMessageType.Speed;
public int Speed { get; set; }
}
public class GvMessageStatus : GvMessage
{
public override GvMessageType MessageType => GvMessageType.Status;
public string Status { get; set; }
}
#endregion
#region [ StMessages ]
public enum StMessageType
{
Speed,
Status
}
public abstract class StMessage
{
public abstract StMessageType MessageType { get; }
}
public class StMessageSpeed : StMessage
{
public override StMessageType MessageType => StMessageType.Speed;
public int Speed { get; set; }
}
public class StMessageStatus : StMessage
{
public override StMessageType MessageType => StMessageType.Status;
public string Status { get; set; }
}
#endregion
#region [ Converters ]
public interface IMessageConverter
{
GvMessage Convert(StMessage stMessage);
}
public class StatusMessageConverter : IMessageConverter
{
public GvMessage Convert(StMessage stMessage)
{
var stMessageStatus = stMessage as StMessageStatus;
if (stMessageStatus == null)
{
throw new ArgumentException($"Message is not of {nameof(StMessageStatus)} type", nameof(stMessage));
}
var gvMessageStatus = new GvMessageStatus
{
Status = stMessageStatus.Status
};
return gvMessageStatus;
}
}
public class SpeedMessageConverter : IMessageConverter
{
public GvMessage Convert(StMessage stMessage)
{
var stMessageSpeed = stMessage as StMessageSpeed;
if (stMessageSpeed == null)
{
throw new ArgumentException($"Message is not of {nameof(StMessageStatus)} type", nameof(stMessage));
}
var gvMessageSpeed = new GvMessageSpeed
{
Speed = stMessageSpeed.Speed
};
return gvMessageSpeed;
}
}
#endregion
#region [ Message printers ]
public interface IMessagePrinter
{
void PrintMessage(GvMessage message);
}
public class ConsoleMessagePrinter : IMessagePrinter
{
public void PrintMessage(GvMessage message)
{
string messageAsText;
switch (message.MessageType)
{
case GvMessageType.Speed:
var speedMessage = (GvMessageSpeed) message;
messageAsText = $"Gv speed message. Speed={speedMessage.Speed}";
break;
case GvMessageType.Status:
var statusMessage = (GvMessageStatus)message;
messageAsText = $"Gv status message. Status={statusMessage.Status}";
break;
default:
throw new ArgumentOutOfRangeException();
}
Console.WriteLine(messageAsText);
}
}
#endregion
}
Не могу понять, насколько правильно я решал эту задачу с помощью средств языка C#. Мне кажется, здесь что-то не так. Как минимум, мне не нравятся обратные преобразования объекта из базового типа в конкретный.
Прошу проинспектировать мой подход к решению данной задачи и, если что-то не так, предложить свой вариант решения.
Ответы (2 шт):
Мне кажется, вполне можно убрать свойство MessageType. Ведь оно, по сути, дублирует тип самих классов.
После удаления этого свойства в шести классах, меняем некоторые методы.
Код двух изменённых методов. Вместо свитча по свойству используется проверка по типу самого объекта.
private static IMessageConverter GetConverterForMessage(StMessage stMessage)
{
switch (stMessage)
{
case StMessageSpeed _:
return new SpeedMessageConverter();
case StMessageStatus _:
return new StatusMessageConverter();
default:
throw new ArgumentOutOfRangeException();
}
}
public void PrintMessage(GvMessage message)
{
string messageAsText;
switch (message)
{
case GvMessageSpeed speedMessage:
messageAsText = $"Gv speed message. Speed={speedMessage.Speed}";
break;
case GvMessageStatus statusMessage:
messageAsText = $"Gv status message. Status={statusMessage.Status}";
break;
default:
throw new ArgumentOutOfRangeException();
}
Console.WriteLine(messageAsText);
}
Предлагаю следующий вариант с применением диспетчеризации с помощью dynamic.
Класс-конвертер:
public class MessageConverter
{
public GvMessage Convert(StMessage stMessage)
{
return Convert((dynamic)stMessage);
}
private GvMessage Convert(StMessageStatus messageStatus)
{
return new GvMessageStatus
{
Status = messageStatus.Status
};
}
private GvMessage Convert(StMessageSpeed messageSpeed)
{
return new GvMessageSpeed
{
Speed = messageSpeed.Speed
};
}
}
Используем его в методах LaunchTest*:
var converter = new MessageConverter();
var gvMessage = converter.Convert(stMessage);
Ещё одно предложение. Перегружаем метод ToString:
public class GvMessageSpeed : GvMessage
{
public int Speed { get; set; }
public override string ToString() => $"Gv speed message. Speed={Speed}";
}
public class GvMessageStatus : GvMessage
{
public string Status { get; set; }
public override string ToString() => $"Gv status message. Status={Status}";
}
Это позволяет сильно упростить класс принтера.
А в принципе можно его и совсем удалить.
public class ConsoleMessagePrinter : IMessagePrinter
{
public void PrintMessage(GvMessage message)
{
Console.WriteLine(message);
}
}
Самый простой вариант получается, если объекты StMessage осведомлены о существовании объектов другого типа. Тогда прямо на них можно возложить обязанности конвертации. При этом мы полностью избавляемся от дополнительных классов конвертеров.
using System;
namespace TestHelloWorld.ConsoleApp
{
internal static class Program
{
private static void Main(string[] args)
{
LaunchTestOfStatusMessageConverting();
LaunchTestOfSpeedMessageConverting();
}
private static void LaunchTestOfStatusMessageConverting()
{
StMessage stMessage = new StMessageStatus { Status = "Pending" };
GvMessage gvMessage = stMessage.ToGvMessage();
var printer = new ConsoleMessagePrinter();
printer.PrintMessage(gvMessage);
}
private static void LaunchTestOfSpeedMessageConverting()
{
StMessage stMessage = new StMessageSpeed { Speed = 42 };
GvMessage gvMessage = stMessage.ToGvMessage();
var printer = new ConsoleMessagePrinter();
printer.PrintMessage(gvMessage);
}
}
public abstract class GvMessage
{
public abstract string GetString();
}
public class GvMessageSpeed : GvMessage
{
public int Speed { get; set; }
public override string GetString() => $"Gv speed message. Speed={Speed}";
}
public class GvMessageStatus : GvMessage
{
public string Status { get; set; }
public override string GetString() => $"Gv status message. Status={Status}";
}
public abstract class StMessage
{
public abstract GvMessage ToGvMessage();
}
public class StMessageSpeed : StMessage
{
public int Speed { get; set; }
public override GvMessage ToGvMessage() => new GvMessageSpeed { Speed = Speed };
}
public class StMessageStatus : StMessage
{
public string Status { get; set; }
public override GvMessage ToGvMessage() => new GvMessageStatus { Status = Status };
}
public interface IMessagePrinter
{
void PrintMessage(GvMessage message);
}
public class ConsoleMessagePrinter : IMessagePrinter
{
public void PrintMessage(GvMessage message) => Console.WriteLine(message.GetString());
}
}
Если протоколы St и Gv ничего не знают друг о друге - то, как бы вы ни исхитрялись, вы не сможете написать такой конвертер, который бы работал после обновления любого из протоколов. Стоит добавить в один из протоколов новое сообщение - и конвертер придётся тоже обновлять, и это верно для любого конвертера.
Самый простой вариант конвертера выглядит вот так:
public static StMessage ToSt(this GvMessage src) => src switch {
GvMessageStatus msg => new StMessageStatus { Status = msg.Status },
GvMessageSpeed msg => new StMessageSpeed { Speed = msg.Speed },
_ => throw new ArgumentException(...),
};
Единственный недостаток подобного подхода - компилятор не сможет проследить что вы обработали все возможные сообщения. Если вам это важно - придётся применить паттерн visitor, тут придётся уже написать заметно больше кода:
public interface GvMessageVisitor<R> {
R VisitStatus(string status);
R VisitSpeed(int speed);
}
partial class GvMessage {
public abstract R Visit<R>(GvMessageVisitor<R> visitor);
}
partial class GvMessageStatus {
public override R Visit<R>(GvMessageVisitor<R> visitor) => visitor.VisitStatus(Status);
}
partial class GvMessageSpeed {
public override R Visit<R>(GvMessageVisitor<R> visitor) => visitor.VisitSpeed(Speed);
}
// ...
class GvToStVisitor : GvMessageVisitor<StMessage> {
public StMessage VisitStatus(string status) => new StMessageStatus { Status = status };
public StMessage VisitSpeed(int speed) => new StMessageSpeed { Speed = speed };
}
public static StMessage ToSt(this GvMessage src) => src.Visit(new GvToStVisitor());
Теперь ваш конвертер не сможет скомпилироваться если вы не учли все возможные сообщения.
Наконец, последний из предлагаемых мною подходов. Раз уж у вас есть два очень похожих протокола - почему бы не попытаться выделить предметную область и не создать её модель, сделав оба протокола зависимыми от этой модели, а не наоборот?
При этом преобразование из транспортной модели в основную будет происходить напрямую, а обратное - через тот же visitor:
public interface Message {
R Visit<R>(MessageVisitor<R> visitor);
}
public interface MessageVisitor<R> {
R VisitStatus(string status);
R VisitSpeed(int speed);
}
public sealed class SpeedMessage : Message {
public int Speed { get; }
public SpeedMessage(int speed) {
Speed = speed;
}
public R Visit<R>(MessageVisitor<R> visitor) => visitor.VisitSpeed(Speed);
}
// ...
partial class GvMessage {
public abstract Message ToMessage();
}
partial class GvMessageSpeed {
public override Message ToMessage() => new SpeedMessage(Speed);
}
partial class GvMessageStatus {
public override Message ToMessage() => new StatusMessage(Status);
}
public class GvMessageFactory : IMessageVisitor<GvMessage> {
public GvMessage VisitSpeed(int speed) => new GvSpeedMessage(speed);
public GvMessage VisitStatus(string status) => new GvStatusMessage(status);
}
При таком подходе, вполне возможно, вам могут даже не понадобиться отдельные классы для протокола, достаточно будет правильного (де)сериализатора.