Абстрактный базовый класс против конкретного класса как супертипа

Прочитав прекраснейшую книгу «Шаблоны проектирования Head First», я начал проповедовать своим коллегам преимущества шаблонов и принципов проектирования. Во время восхваления достоинств моего любимого паттерна — Стратегического паттерна — мне задали вопрос, который заставил меня задуматься. Стратегия, конечно, использует наследование и композицию, и я был на одной из своих тирад о «программе для интерфейса (или супертипа), а не реализации», когда коллега спросил: «Зачем использовать абстрактный базовый класс вместо конкретного класса?» .
Я мог только придумать: «Ну, вы заставляете свои подклассы реализовывать абстрактные методы и не позволяете им создавать экземпляры ABC». Но, честно говоря, вопрос застал меня врасплох. Это единственные преимущества использования абстрактного базового класса по сравнению с конкретным классом на вершине моей иерархии?


person Tony D    schedule 28.06.2010    source источник
comment
По-моему ДА. Но я думаю, что очень важной особенностью языка является заставить кого-то, кто наследует этот класс, реализовать метод. Иногда вам нужно создать общий класс, но вы не можете реализовать все функции, потому что это было бы слишком специфично.   -  person KroaX    schedule 28.06.2010


Ответы (8)


Если вам нужно реализовать определенные методы, используйте интерфейс. Если есть общая логика, которую можно извлечь, используйте абстрактный базовый класс. Если базовый набор функций завершен сам по себе, вы можете использовать конкретный класс в качестве основы. Абстрактный базовый класс и интерфейс не могут быть созданы напрямую, и это одно из преимуществ. Если вы можете использовать конкретный тип, вам нужно переопределить методы, и это имеет «запах кода».

person NerdFury    schedule 28.06.2010
comment
Хм? Почему метод переопределяет запах? - person ladenedge; 28.06.2010
comment
Код с виртуальными методами гораздо сложнее изменить в будущем, не нарушая некий подразумеваемый поведенческий контракт. Поэтому в .NET они решили, что переопределение метода должно быть сознательным решением и должно быть тщательно обдумано. Так что любой переопределяемый метод без инструкций для разработчика — это запах кода. - person Jonathan Allen; 28.06.2010
comment
Если у вас есть класс, и вы кодируете поведение определенным образом. Затем позже наследуйте от этого класса и решите, что вы хотите изменить поведение, IMO, в этом есть запах. Я думаю, что разработчик должен реорганизовать контракт поведения (либо интерфейс, либо абстрактный базовый класс) и создать две конкретные реализации, которые имеют явно определенные причины для существования. Наличие абстрактного базового класса или интерфейса делает расширение более явным в том, как изменить или заменить поведение. Переопределения больше похожи на грубую силу. - person NerdFury; 28.06.2010

Программа для интерфейса, а не для реализации имеет мало общего с абстрактными и конкретными классами. Помните шаблон метода шаблона? Классы, абстрактные или конкретные, являются деталями реализации.

И причина использования абстрактных классов вместо конкретных классов заключается в том, что вы можете вызывать методы, не реализуя их, а вместо этого оставляя их для реализации в подклассах.

Программирование интерфейса — это другое дело: оно определяет, что делает ваш API, а не как он это делает. И это обозначается интерфейсами.

Обратите внимание на одно ключевое отличие — у вас может быть protected abstract методов, что означает, что это детали реализации. Но все методы интерфейса публичны — часть API.

person Bozho    schedule 28.06.2010
comment
Чтобы добавить к этому, Program для реализации будет делать такие вещи, как использование refelection для доступа к закрытым полям класса. - person Jonathan Allen; 28.06.2010

Да, хотя вы также можете использовать интерфейс, чтобы заставить класс реализовать определенные методы.

Другая причина использования абстрактного класса в отличие от конкретного класса заключается в том, что абстрактный класс, очевидно, не может быть создан. Иногда вы также не хотели бы, чтобы это произошло, поэтому абстрактный класс — это путь.

person Danny Drogt    schedule 28.06.2010

Прежде всего, шаблон стратегии почти никогда не следует использовать в современном C#. Это в основном для таких языков, как Java, которые не поддерживают указатели на функции, делегаты или функции первого класса. Вы увидите его в более старых версиях C# в таких интерфейсах, как IComparer.

Что касается абстрактного базового класса и конкретного класса, ответ в Java всегда таков: «Что лучше работает в этой ситуации?» Если ваши стратегии могут совместно использовать код, то пусть они это делают.

Шаблоны проектирования — это не инструкции о том, как что-то делать. Это способы классифицировать то, что мы уже сделали.

person Jonathan Allen    schedule 28.06.2010

Абстрактные базовые классы обычно используются в сценариях, где разработчик хочет навязать архитектурный шаблон, в котором определенные задачи должны выполняться одинаковым образом всеми классами, в то время как другие виды поведения зависят от подклассов. пример:

public abstract class Animal{

public void digest(){

}

public abstract void sound(){

}
}

public class Dog extends Animal{
public void sound(){
    System.out.println("bark");
}
}

Паттерн стратегии просит дизайнеров использовать композиционное поведение для случаев, когда для поведения существуют семейства алогиртмов.

person frictionlesspulley    schedule 29.06.2010

Если клиент полагается на «контракт [ы] подразумеваемого поведения», он запрограммирован против реализации и против негарантированного поведения. Переопределение метода при соблюдении контракта только выявляет ошибки в клиенте, а не вызывает их.

OTOH, ошибка принятия контрактов, которых нет, с меньшей вероятностью вызовет проблемы, если рассматриваемый метод не является виртуальным, то есть его переопределение не может вызвать проблем, потому что его нельзя переопределить. Только если реализация исходного метода изменена (при соблюдении условий контракта), это может привести к поломке клиента.

person jyoungdev    schedule 28.06.2010

Вопрос о том, должен ли базовый класс быть абстрактным или конкретным, ИМХО во многом зависит от того, будет ли полезен объект базового класса, который реализует только поведение, общее для всех объектов в классе. Рассмотрим WaitHandle. Вызов «wait» приведет к блокировке кода до тех пор, пока какое-либо условие не будет выполнено, но не существует общего способа сообщить объекту WaitHandle, что его условие выполнено. Если бы можно было создать экземпляр «WaitHandle», в отличие от возможности создавать экземпляры только производных типов, такой объект должен был бы либо никогда не ждать, либо всегда ждать вечно. Последнее поведение было бы довольно бесполезным; первое могло бы быть полезным, но его можно было бы достичь почти так же хорошо со статически выделенным ManualResetEvent (я думаю, что последний тратит впустую несколько ресурсов, но если он статически выделен, общая потеря ресурсов должна быть тривиальной).

Я думаю, что во многих случаях я бы предпочел использовать ссылки на интерфейс, а не на абстрактный базовый класс, но предоставить с интерфейсом базовый класс, который обеспечивает «реализация модели». Таким образом, в любом месте, где можно было бы использовать ссылку на MyThing, можно было бы указать ссылку на «iMyThing». Вполне может быть, что 99% (или даже 100%) объектов iMyThing на самом деле являются MyThing, но если кому-то когда-нибудь понадобится объект iMyThing, который наследуется от чего-то другого, он может это сделать.

person supercat    schedule 28.06.2010

Предпочитайте абстрактные базовые классы в следующих сценариях:

  1. Базовый класс не может существовать без подкласса => базовый класс просто абстрактен и не может быть создан.
  2. Базовый класс не может иметь полную или конкретную реализацию метода => Реализация метода в базовом классе является неполной, и только подклассы могут обеспечить полную реализацию.
  3. Базовый класс предоставляет шаблон для реализации метода, но он по-прежнему зависит от класса 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.

person Ravindra babu    schedule 03.06.2016