Абстрактные классы отлично подходят для предопределенной функциональности, например, когда известно минимальное точное поведение, которое должен представлять класс, но не то, какие данные он должен использовать для этого или точную реализацию.
abstract class ADataAccess
{
abstract public void Save();
}
Нормальные (не абстрактные) классы могут быть хороши для подобных вещей, но вы должны знать особенности реализации, чтобы иметь возможность их писать.
public class DataAccess
{
public void Save()
{
if ( _is_new )
{
Insert();
}
else if ( _is_modified )
{
Update();
}
}
}
Кроме того, вы можете использовать интерфейсы (индивидуально или по классам, абстрактные или нет) для определения того же рода определения прототипа.
interface ISaveable
{
void Save();
void Insert();
void Update();
}
class UserAccount : ISavable
{
void ISavable.Save() { ... }
void ISavable.Insert() { ... }
void ISavable.Update() { ... }
}
Еще одним вариантом может быть использование дженериков
class GenDataAccess<T>
{
public void Save()
{
...
}
}
Все эти методы могут использоваться для определения определенного прототипа для классов, с которыми нужно работать. Способы убедиться, что код A может общаться с кодом B. И, конечно, вы можете смешивать и сочетать все вышеперечисленное по своему вкусу. Не существует определенного правильного пути, но мне нравится определять интерфейсы и абстрактные классы, а затем обращаться к интерфейсам. Этот способ устраняет некоторые мыслительные требования для «слесарного дела» в классах более высокого уровня, сохраняя при этом максимальную гибкость. (наличие интерфейсов снимает требование использования абстрактного базового класса, но оставляет его в качестве опции).