Прочитав прекраснейшую книгу «Шаблоны проектирования Head First», я начал проповедовать своим коллегам преимущества шаблонов и принципов проектирования. Во время восхваления достоинств моего любимого паттерна — Стратегического паттерна — мне задали вопрос, который заставил меня задуматься. Стратегия, конечно, использует наследование и композицию, и я был на одной из своих тирад о «программе для интерфейса (или супертипа), а не реализации», когда коллега спросил: «Зачем использовать абстрактный базовый класс вместо конкретного класса?» .
Я мог только придумать: «Ну, вы заставляете свои подклассы реализовывать абстрактные методы и не позволяете им создавать экземпляры ABC». Но, честно говоря, вопрос застал меня врасплох. Это единственные преимущества использования абстрактного базового класса по сравнению с конкретным классом на вершине моей иерархии?
Абстрактный базовый класс против конкретного класса как супертипа
Ответы (8)
Если вам нужно реализовать определенные методы, используйте интерфейс. Если есть общая логика, которую можно извлечь, используйте абстрактный базовый класс. Если базовый набор функций завершен сам по себе, вы можете использовать конкретный класс в качестве основы. Абстрактный базовый класс и интерфейс не могут быть созданы напрямую, и это одно из преимуществ. Если вы можете использовать конкретный тип, вам нужно переопределить методы, и это имеет «запах кода».
Программа для интерфейса, а не для реализации имеет мало общего с абстрактными и конкретными классами. Помните шаблон метода шаблона? Классы, абстрактные или конкретные, являются деталями реализации.
И причина использования абстрактных классов вместо конкретных классов заключается в том, что вы можете вызывать методы, не реализуя их, а вместо этого оставляя их для реализации в подклассах.
Программирование интерфейса — это другое дело: оно определяет, что делает ваш API, а не как он это делает. И это обозначается интерфейсами.
Обратите внимание на одно ключевое отличие — у вас может быть protected abstract
методов, что означает, что это детали реализации. Но все методы интерфейса публичны — часть API.
Да, хотя вы также можете использовать интерфейс, чтобы заставить класс реализовать определенные методы.
Другая причина использования абстрактного класса в отличие от конкретного класса заключается в том, что абстрактный класс, очевидно, не может быть создан. Иногда вы также не хотели бы, чтобы это произошло, поэтому абстрактный класс — это путь.
Прежде всего, шаблон стратегии почти никогда не следует использовать в современном C#. Это в основном для таких языков, как Java, которые не поддерживают указатели на функции, делегаты или функции первого класса. Вы увидите его в более старых версиях C# в таких интерфейсах, как IComparer.
Что касается абстрактного базового класса и конкретного класса, ответ в Java всегда таков: «Что лучше работает в этой ситуации?» Если ваши стратегии могут совместно использовать код, то пусть они это делают.
Шаблоны проектирования — это не инструкции о том, как что-то делать. Это способы классифицировать то, что мы уже сделали.
Абстрактные базовые классы обычно используются в сценариях, где разработчик хочет навязать архитектурный шаблон, в котором определенные задачи должны выполняться одинаковым образом всеми классами, в то время как другие виды поведения зависят от подклассов. пример:
public abstract class Animal{
public void digest(){
}
public abstract void sound(){
}
}
public class Dog extends Animal{
public void sound(){
System.out.println("bark");
}
}
Паттерн стратегии просит дизайнеров использовать композиционное поведение для случаев, когда для поведения существуют семейства алогиртмов.
Если клиент полагается на «контракт [ы] подразумеваемого поведения», он запрограммирован против реализации и против негарантированного поведения. Переопределение метода при соблюдении контракта только выявляет ошибки в клиенте, а не вызывает их.
OTOH, ошибка принятия контрактов, которых нет, с меньшей вероятностью вызовет проблемы, если рассматриваемый метод не является виртуальным, то есть его переопределение не может вызвать проблем, потому что его нельзя переопределить. Только если реализация исходного метода изменена (при соблюдении условий контракта), это может привести к поломке клиента.
Вопрос о том, должен ли базовый класс быть абстрактным или конкретным, ИМХО во многом зависит от того, будет ли полезен объект базового класса, который реализует только поведение, общее для всех объектов в классе. Рассмотрим WaitHandle. Вызов «wait» приведет к блокировке кода до тех пор, пока какое-либо условие не будет выполнено, но не существует общего способа сообщить объекту WaitHandle, что его условие выполнено. Если бы можно было создать экземпляр «WaitHandle», в отличие от возможности создавать экземпляры только производных типов, такой объект должен был бы либо никогда не ждать, либо всегда ждать вечно. Последнее поведение было бы довольно бесполезным; первое могло бы быть полезным, но его можно было бы достичь почти так же хорошо со статически выделенным ManualResetEvent (я думаю, что последний тратит впустую несколько ресурсов, но если он статически выделен, общая потеря ресурсов должна быть тривиальной).
Я думаю, что во многих случаях я бы предпочел использовать ссылки на интерфейс, а не на абстрактный базовый класс, но предоставить с интерфейсом базовый класс, который обеспечивает «реализация модели». Таким образом, в любом месте, где можно было бы использовать ссылку на MyThing, можно было бы указать ссылку на «iMyThing». Вполне может быть, что 99% (или даже 100%) объектов iMyThing на самом деле являются MyThing, но если кому-то когда-нибудь понадобится объект iMyThing, который наследуется от чего-то другого, он может это сделать.
Предпочитайте абстрактные базовые классы в следующих сценариях:
- Базовый класс не может существовать без подкласса => базовый класс просто абстрактен и не может быть создан.
- Базовый класс не может иметь полную или конкретную реализацию метода => Реализация метода в базовом классе является неполной, и только подклассы могут обеспечить полную реализацию.
- Базовый класс предоставляет шаблон для реализации метода, но он по-прежнему зависит от класса Concrete для завершения реализации метода - Template_method_pattern
Простой пример, иллюстрирующий вышеизложенные пункты
Shape
абстрактно и не может существовать без Конкретной формы, такой как Rectangle
. Рисование Shape
не может быть реализовано в классе Shape
, так как разные фигуры имеют разные формулы. Лучший вариант для обработки сценария: оставить draw()
реализацию подклассам.
abstract class Shape{
int x;
int y;
public Shape(int x,int y){
this.x = x;
this.y = y;
}
public abstract void draw();
}
class Rectangle extends Shape{
public Rectangle(int x,int y){
super(x,y);
}
public void draw(){
//Draw Rectangle using x and y : length * width
System.out.println("draw Rectangle with area:"+ (x * y));
}
}
class Triangle extends Shape{
public Triangle(int x,int y){
super(x,y);
}
public void draw(){
//Draw Triangle using x and y : base * height /2
System.out.println("draw Triangle with area:"+ (x * y) / 2);
}
}
class Circle extends Shape{
public Circle(int x,int y){
super(x,y);
}
public void draw(){
//Draw Circle using x as radius ( PI * radius * radius
System.out.println("draw Circle with area:"+ ( 3.14 * x * x ));
}
}
public class AbstractBaseClass{
public static void main(String args[]){
Shape s = new Rectangle(5,10);
s.draw();
s = new Circle(5,10);
s.draw();
s = new Triangle(5,10);
s.draw();
}
}
выход:
draw Rectangle with area:50
draw Circle with area:78.5
draw Triangle with area:25
Приведенный выше код охватывает пункты 1 и 2. Вы можете изменить метод draw()
в качестве метода шаблона, если базовый класс имеет некоторую реализацию и вызывает метод подкласса для выполнения функции draw()
.
Теперь тот же пример с шаблоном метода Template:
abstract class Shape{
int x;
int y;
public Shape(int x,int y){
this.x = x;
this.y = y;
}
public abstract void draw();
// drawShape is template method
public void drawShape(){
System.out.println("Drawing shape from Base class begins");
draw();
System.out.println("Drawing shape from Base class ends");
}
}
class Rectangle extends Shape{
public Rectangle(int x,int y){
super(x,y);
}
public void draw(){
//Draw Rectangle using x and y : length * width
System.out.println("draw Rectangle with area:"+ (x * y));
}
}
class Triangle extends Shape{
public Triangle(int x,int y){
super(x,y);
}
public void draw(){
//Draw Triangle using x and y : base * height /2
System.out.println("draw Triangle with area:"+ (x * y) / 2);
}
}
class Circle extends Shape{
public Circle(int x,int y){
super(x,y);
}
public void draw(){
//Draw Circle using x as radius ( PI * radius * radius
System.out.println("draw Circle with area:"+ ( 3.14 * x * x ));
}
}
public class AbstractBaseClass{
public static void main(String args[]){
Shape s = new Rectangle(5,10);
s.drawShape();
s = new Circle(5,10);
s.drawShape();
s = new Triangle(5,10);
s.drawShape();
}
}
выход:
Drawing shape from Base class begins
draw Rectangle with area:50
Drawing shape from Base class ends
Drawing shape from Base class begins
draw Circle with area:78.5
Drawing shape from Base class ends
Drawing shape from Base class begins
draw Triangle with area:25
Drawing shape from Base class ends
После того, как вы решили, что вам нужно использовать метод abstract
, у вас есть два варианта: либо пользователь interface
, либо класс abstract
. Вы можете объявить свои методы в interface
и определить класс abstract
как класс, реализующий interface
.