Переопределение и доопределение методов
Часто нужно создать базовое (архитектурное) решение, которое затем другие программисты будут расширять. При этом понятно, что дополнительные действия неизвестны в это момент, но нужно обеспечить, удобство вызова базовых действий. Известно, что для этого можно метод класса переопределить в классе-наследнике, и при этом можно вызвать метод базового класса:
public class C1
{
public virtual void M1()
{
Console.WriteLine("C1.M1");
}
}
если требуется вначале базовые действия, затем дополнительные:
public class C2 : C1
{
public override void M1()
{
base.M1();
Console.WriteLine("C2.M1");
}
}
или наоборот, вначале дополнительные, затем базовые:
public class C2 : C1
{
public override void M1()
{
Console.WriteLine("C2.M1");
base.M1();
}
}
Но что делать когда нужно выполнить базовые действия вначале и в заключение, взяв таким образом дополнительные действия в обрамляющие скобки ?
Методы "до" и "после"
правитьПрямым, но как правило, совершенно не правильным является следующий способ:
public class C1
{
public void BeforM1()
{
Console.WriteLine("C1.BeforM1");
}
public void AfterM1()
{
Console.WriteLine("C1.AfterM1");
}
}
public class C2 : C1
{
public void M1()
{
base.BeforM1();
Console.WriteLine("C2.M1");
base.AfterM1();
}
}
Чем плох этот способ :
- В базовом классе вместо одного общего метода стало два, что в частности может привести к дублированию кода, связанного с локальными переменными;
- Потерян полиформизм, что при глубоком наследовании заставляет вызывать разные методы, а не один не зависимо от типа наследников;
- Программист разрабатывающий метод наследник, должен вызвать базовые методы BeforM1() и AfterM1(), и при этом нет гарантий, что он это не забудет сделать.
Полиформизм можно восстановить, следующим образом:
public class C1
{
public void BeforM1()
{
Console.WriteLine("C1.BeforM1");
}
public void AfterM1()
{
Console.WriteLine("C1.AfterM1");
}
public virtual void M1()
{
BeforM1();
AfterM1();
}
}
public class C2 : C1
{
public override void M1()
{
base.BeforM1();
Console.WriteLine("C2.M1");
base.AfterM1();
}
}
Но тем самым мы добавили еще одну лишнею сущность - метод, который склеивает то, что прежде мы зачем то разделили.
Использование событий
правитьРяд существенных проблем можно убрать применяя события:
public class C1
{
public event EventHandler E1;
public virtual void M1()
{
Console.WriteLine("C1.BeforM1");
if (E1 != null) { E1(this, new EventArgs()); }
Console.WriteLine("C1.AfterM1");
}
}
public class C2 : C1
{
public C2()
{
E1 +=new EventHandler(M1Add);
}
private void M1Add(object sender, EventArgs e)
{
Console.WriteLine("C2.M1");
}
}
Этим способом мы убрали необходимость искусственного разделения базового метода на два. А также предоставили возможность программистам, разрабатывающим метод наследника (прикладные программисты), доопределить действия, указав требуемый метод. В большинстве случаев этого достаточно. Но иногда, для основополагающих архитектурный действий, требуется жестко задать место в системе, в котором прикладные программисты должны прописать дополнительные действия.
Прозрачное разделение методов
правитьНо более элегантным и функционально строгим является следующий прием:
public class C1
{
public void M1()
{
Console.WriteLine("C1.BeforM1");
M1Realise();
Console.WriteLine("C1.AfterM1");
}
protected virtual void M1Realise()
{
Console.WriteLine("C1.M1");
}
}
public class C2 : C1
{
protected override void M1Realise()
{
Console.WriteLine("C2.M1");
}
}
Здесь прикладные программисты вынужденны писать дополнительные методы в четко обозначенном методе M1Realise(), а те кто будет пользоваться этой иерархией класса по прежнему вызывают только метод M1().