На мой взгляд, вместо того, чтобы решать эту проблему, вам следует переосмыслить свой дизайн. Вы абсолютно уверены, что в этой ситуации нельзя использовать полиморфизм - либо напрямую назначать сущность ответственной за операцию, которую вы пытаетесь выполнить, либо использовать шаблон посетителя. Я сталкивался с этой проблемой несколько раз и всегда решал изменить дизайн - это приводило к более четкому коду. Я предлагаю вам сделать то же самое, если только вы не уверены, что использование шрифта — лучшее решение.
Проблема
Чтобы получить пример, хотя бы немного похожий на реальный мир, предположим, что у вас есть следующие сущности:
public abstract class Operation
{
public virtual DateTime PerformedOn { get; set; }
public virtual double Ammount { get; set; }
}
public class OutgoingTransfer : Operation
{
public virtual string TargetAccount { get; set; }
}
public class AtmWithdrawal : Operation
{
public virtual string AtmAddress { get; set; }
}
Естественно, это была бы небольшая часть гораздо большей модели. И теперь вы столкнулись с проблемой: для каждого конкретного типа Операции есть свой способ отображения:
private static void PrintOperation(Operation operation)
{
Console.WriteLine("{0} - {1}", operation.PerformedOn,
operation.Ammount);
}
private static void PrintOperation(OutgoingTransfer operation)
{
Console.WriteLine("{0}: {1}, target account: {2}",
operation.PerformedOn, operation.Ammount,
operation.TargetAccount);
}
private static void PrintOperation(AtmWithdrawal operation)
{
Console.WriteLine("{0}: {1}, atm's address: {2}",
operation.PerformedOn, operation.Ammount,
operation.AtmAddress);
}
Простые, перегруженные методы будут работать в простом случае:
var transfer = new OutgoingTransfer
{
Ammount = -1000,
PerformedOn = DateTime.Now.Date,
TargetAccount = "123123123"
};
var withdrawal = new AtmWithdrawal
{
Ammount = -1000,
PerformedOn = DateTime.Now.Date,
AtmAddress = "Some address"
};
// works as intended
PrintOperation(transfer);
PrintOperation(withdrawal);
К сожалению, перегруженные методы привязываются во время компиляции, поэтому, как только вы вводите массив/список/любые операции, будет вызываться только универсальная перегрузка (операция).
Operation[] operations = { transfer, withdrawal };
foreach (var operation in operations)
{
PrintOperation(operation);
}
Есть два решения этой проблемы, и оба имеют недостатки. Вы можете ввести абстрактный/виртуальный метод в Operation для печати информации в выбранный поток. Но это смешает проблемы с пользовательским интерфейсом в вашей модели, поэтому это неприемлемо для вас (сейчас я покажу вам, как вы можете улучшить это решение, чтобы оно соответствовало вашим ожиданиям).
Вы также можете создать множество if в форме:
if(operation is (ConcreteType))
PrintOperation((ConcreteType)operation);
Это решение уродливо и подвержено ошибкам. Каждый раз, когда вы добавляете/изменяете/удаляете тип операции, вы должны просмотреть каждое место, где вы использовали этот хак, и изменить его. И если вы пропустите одно место, вы, вероятно, сможете поймать только эту среду выполнения - никаких строгих проверок времени компиляции на наличие некоторых ошибок (например, отсутствие одного подтипа).
Кроме того, это решение потерпит неудачу, как только вы введете какой-либо прокси.
Как работает прокси
Приведенный ниже код является ОЧЕНЬ простым прокси (в этой реализации он такой же, как шаблон декоратора, но в целом эти шаблоны не совпадают. Чтобы различить эти два шаблона, потребуется дополнительный код).
public class OperationProxy : Operation
{
private readonly Operation m_innerOperation;
public OperationProxy(Operation innerOperation)
{
if (innerOperation == null)
throw new ArgumentNullException("innerOperation");
m_innerOperation = innerOperation;
}
public override double Ammount
{
get { return m_innerOperation.Ammount; }
set { m_innerOperation.Ammount = value; }
}
public override DateTime PerformedOn
{
get { return m_innerOperation.PerformedOn; }
set { m_innerOperation.PerformedOn = value; }
}
}
Как видите, для всей иерархии существует только один прокси-класс. Почему? Потому что вы должны писать свой код так, чтобы он не зависел от конкретного типа — только от предоставленной абстракции. Этот прокси мог бы вовремя отсрочить загрузку сущностей - может быть, вы вообще не будете его использовать? Может быть, вы будете использовать только 2 сущности из 1000? Зачем тогда их всех грузить?
Таким образом, NHibernate использует прокси, как описано выше (хотя и гораздо более сложный), чтобы отложить загрузку объекта. Он может создать 1 прокси для каждого подтипа, но это разрушит всю цель ленивой загрузки. Если вы внимательно посмотрите, как NHibernate хранит подклассы, вы увидите, что для того, чтобы определить тип объекта, вы должны его загрузить. Таким образом, невозможно иметь конкретные прокси — вы можете иметь только самые абстрактные, OperationProxy.
Хотя решение с ifs некрасиво - это было решение. Теперь, когда вы ввели прокси в свою проблему - это больше не работает. Так что остается только полиморфный метод, что неприемлемо из-за смешивания ответственности пользовательского интерфейса с вашей моделью. Давайте исправим это.
Инверсия зависимостей и шаблон посетителя
Во-первых, давайте посмотрим, как будет выглядеть решение с виртуальными методами (только что добавленный код):
public abstract class Operation
{
public abstract void PrintInformation();
}
public class OutgoingTransfer : Operation
{
public override void PrintInformation()
{
Console.WriteLine("{0}: {1}, target account: {2}",
PerformedOn, Ammount, TargetAccount);
}
}
public class AtmWithdrawal : Operation
{
public override void PrintInformation()
{
Console.WriteLine("{0}: {1}, atm's address: {2}",
PerformedOn, Ammount, AtmAddress);
}
}
public class OperationProxy : Operation
{
public override void PrintInformation()
{
m_innerOperation.PrintInformation();
}
}
И теперь, когда вы звоните:
Operation[] operations = { transfer, withdrawal, proxy };
foreach (var operation in operations)
{
operation.PrintInformation();
}
все работает как шарм.
Чтобы удалить эту зависимость пользовательского интерфейса в модели, давайте создадим интерфейс:
public interface IOperationVisitor
{
void Visit(AtmWithdrawal operation);
void Visit(OutgoingTransfer operation);
}
Давайте изменим модель, чтобы она зависела от этого интерфейса:
А теперь создайте реализацию — ConsoleOutputOperationVisitor (методы PrintInformation я удалил):
public abstract class Operation
{
public abstract void Accept(IOperationVisitor visitor);
}
public class OutgoingTransfer : Operation
{
public override void Accept(IOperationVisitor visitor)
{
visitor.Visit(this);
}
}
public class AtmWithdrawal : Operation
{
public override void Accept(IOperationVisitor visitor)
{
visitor.Visit(this);
}
}
public class OperationProxy : Operation
{
public override void Accept(IOperationVisitor visitor)
{
m_innerOperation.Accept(visitor);
}
}
Что здесь происходит? Когда вы вызываете Accept на операции и передаете посетителя, будет вызвана реализация accept, где будет вызвана соответствующая перегрузка метода Visit (компилятор может определить тип "this"). Таким образом, вы комбинируете «мощь» виртуальных методов и перегрузок, чтобы вызвать соответствующий метод. Как видите, теперь ссылка на пользовательский интерфейс здесь, модель зависит только от интерфейса, который может быть включен в слой модели.
Итак, теперь, чтобы заставить это работать, реализация интерфейса:
public class ConsoleOutputOperationVisitor : IOperationVisitor
{
#region IOperationVisitor Members
public void Visit(AtmWithdrawal operation)
{
Console.WriteLine("{0}: {1}, atm's address: {2}",
operation.PerformedOn, operation.Ammount,
operation.AtmAddress);
}
public void Visit(OutgoingTransfer operation)
{
Console.WriteLine("{0}: {1}, target account: {2}",
operation.PerformedOn, operation.Ammount,
operation.TargetAccount);
}
#endregion
}
И код:
Operation[] operations = { transfer, withdrawal, proxy };
foreach (var operation in operations)
{
operation.Accept(visitor);
}
Я прекрасно понимаю, что это не идеальное решение. Вам все равно придется изменять интерфейс и посетителей по мере добавления новых типов. Но вы получаете проверку времени компиляции и никогда ничего не пропустите. Одна вещь, которую было бы действительно трудно достичь с помощью этого метода, - это получить подключаемые подтипы, но я все равно не уверен, что это правильный сценарий. Вам также придется изменить этот шаблон, чтобы он соответствовал вашим потребностям в конкретном сценарии, но я оставлю это на ваше усмотрение.
person
maciejkow
schedule
30.07.2009