Переопределение и доопределение методов

Часто нужно создать базовое (архитектурное) решение, которое затем другие программисты будут расширять. При этом понятно, что дополнительные действия неизвестны в это момент, но нужно обеспечить, удобство вызова базовых действий. Известно, что для этого можно метод класса переопределить в классе-наследнике, и при этом можно вызвать метод базового класса:

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();
	}
}

Чем плох этот способ :

  1. В базовом классе вместо одного общего метода стало два, что в частности может привести к дублированию кода, связанного с локальными переменными;
  2. Потерян полиформизм, что при глубоком наследовании заставляет вызывать разные методы, а не один не зависимо от типа наследников;
  3. Программист разрабатывающий метод наследник, должен вызвать базовые методы 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().