Общий ОО вопрос о наследовании и расширяемости

Итак, в модели наследования с одним родителем, что является лучшим решением для того, чтобы сделать код расширяемым для будущих изменений при сохранении того же интерфейса (я хотел бы подчеркнуть тот факт, что эти изменения не могут быть известны во время первоначальной реализации, основной фокус мой вопрос заключается в том, чтобы исследовать лучший механизм / шаблон для поддержки этих изменений по мере их появления)? Я знаю, что это очень простой вопрос ОО, и ниже я привожу пример того, как я это делал, но мне было интересно, есть ли лучшее решение этой общей проблемы.

Вот что я делал (пример кода на Java):

В начале созданы следующие два класса и интерфейс:

public class Foo
{
    protected int z;
}

public interface FooHandler
{
    void handleFoo(Foo foo);
}

public class DefaultFooHandler implements FooHandler
{
    @Override
    public void handleFoo(Foo foo)
    {
        //do something here
    }
}

Система использует только переменные / поля типа FooHandler, и этот объект (в данном случае DefaultFooHandler) создается в нескольких четко определенных местах (возможно, есть FooHandlerFactory), чтобы компенсировать любые изменения, которые могут произойти в будущем.

Затем в какой-то момент в будущем возникает необходимость в расширении Foo для добавления некоторой функциональности. Итак, созданы два новых класса:

public class ImprovedFoo extends Foo
{
    protected double k;
}

public class ImprovedFooHandler extends DefaultFooHandler
{
    @Override
    public void handleFoo(Foo foo)
    {
        if(foo instanceof ImprovedFoo)
        {
            handleImprovedFoo((ImprovedFoo)foo);
            return;
        }
        if(foo instanceof Foo)
        {
            super.handleFoo(foo);
            return;
        }
    }

    public void handleImprovedFoo(ImprovedFoo foo)
    {
        //do something involving ImprovedFoo
    }
}

В приведенном выше примере меня раздражает то, что if-statements которые появляются в ImprovedFooHandler.handleFoo

Есть ли способ избежать использования if-statements и instanceof оператор?

6 ответов

Решение

Прежде всего, написанный вами код не будет работать. Каждый раз, когда вы видите instanceof а также if...else Вместе будьте очень осторожны. Порядок этих проверок очень важен. В вашем случае вы никогда не будете казнить handleImpovedFoo, Угадай почему:)

Это абсолютно нормально, у вас есть эти instanceof заявления. Иногда это единственный способ обеспечить другое поведение для подтипа. Но здесь вы можете использовать другой трюк: использовать простой Map, Сопоставить классы foo-иерархии с экземплярами fooHandler-иерархии.

Map<Class<? extends Foo>, FooHandler> map ...

map.put( Foo.class, new FooHandler() );
map.put( ImprovedFoo.class, new ImprovedFooHandler() );

Foo foo ...; // here comes an unknown foo 

map.get( foo.getClass() ).handleFoo( foo );

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

Случай 1: Виртуальная файловая система

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

interface IFolder
{
     IFolder subFolder(String Name);
     void delete(String filename);
     void removeFolder(); // must be empty
     IFile openFile(String Name);
     List<String> getFiles();
}

В следующей версии вашего программного обеспечения вы хотите добавить возможность удаления каталога и всего его содержимого. Назовите это removeTree. Вы не можете просто добавить removeTree в IFolder, потому что это сломает всех пользователей IFolder. Вместо:

interface IFolder2 implements IFolder
{
     void removeTree();    
}

Всякий раз, когда клиент регистрирует IFolder (а не IFolder2), регистрируйтесь

new IFolder2Adapter(folder)

Вместо этого и используйте IFolder2 во всем приложении. Большая часть вашего кода не должна беспокоиться о разнице в том, что поддерживали старые версии IFolder.

Случай 2: лучшие строки

У вас есть строковый класс, который поддерживает различные функции.

class String
{
     String substring(int start, end);
}

Вы решаете добавить поиск строки в новой версии и таким образом реализовать:

class SearchableString extends String
{
    int find(String);
}

Это просто глупо, SearchableString следует объединить в String.

Случай 3: формы

У вас есть симулятор формы, который позволяет вам получить области формы.

class Shape
{
    double Area();
    static List<Shape> allShapes; // forgive evil staticness
}

Теперь вы вводите новый вид Shape:

class DrawableShape extends Shape
{
    void Draw(Painter paint);
}

Мы могли бы добавить пустой метод Draw по умолчанию в Shape. Но кажется неправильным, чтобы у Shape был метод Draw, потому что формы вообще не предназначены для рисования. Чертежу действительно нужен список DrawableShapes, а не список Shapes, который предоставляется. На самом деле, возможно, DrawableShape вообще не должен быть Shape.

Случай 4: Запчасти

Предположим, что у нас есть Автомобиль:

class Car
{
    Motor getMotor();
    Wheels getWheels();
}

void maintain(Car car)
{
    car.getMotor().changeOil();
    car.getWheels().rotate();
}

Конечно, вы знаете, где-нибудь в будущем, кто-то сделает лучшую машину.

class BetterCar extends Car
{
    Highbeams getHighBeams();
}

Здесь мы можем использовать шаблон посетителя.

void maintain(Car car)
{
     car.visit( new Maintainer() );
}

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

Случай 5: игровые объекты У нас есть игра с множеством объектов, которые можно увидеть на экране.

class GameObject
{
   void Draw(Painter painter);
   void Destroy();
   void Move(Point point);
}

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

class LogicGameObject extends GameObject
{
    void Logic();
}

Как мы вызываем Logic() для всех объектов LogicGame Object? В этом случае добавление пустого метода Logic() в Game Object кажется наилучшим вариантом. В описании работы Game Object вполне можно ожидать, что он сможет знать, что делать с обновлением логики, даже если это ничего не значит.

Заключение

Лучший способ справиться с этой ситуацией зависит от конкретной ситуации. Вот почему я поставил вопрос о том, почему вы не хотите добавлять функциональность в Foo. Лучший способ расширения Foo зависит от того, что именно вы делаете. То, что вы видите с помощью instanceof/, - это признак того, что вы не расширили объект наилучшим образом.

Да, не нарушайте LSP, что вы делаете здесь. Рассматривали ли вы шаблон стратегии?

В подобных ситуациях я обычно использую фабрику, чтобы получить соответствующий FooHandler для типа Foo, который у меня есть. В этом случае все еще будет набор if, но они будут на фабрике, а не в реализации обработчика.

Это выглядит как простой простой случай для базового полиморфизма. Дайте Foo метод с именем что-то вроде DontWorryI’LHandleThisMyself() (гм, кроме апострофов и более разумного имени). FooHandler просто вызывает этот метод любого Foo, который он дает. Производные классы Foo переопределяют этот метод по своему усмотрению. Пример в вопросе, кажется, имеет вещи наизнанку.

С помощью шаблона посетителя вы можете сделать что-то вроде этого,

abstract class absFoo {}
class Foo extends absFoo
{
    protected int z;

}
class ImprovedFoo extends absFoo
{
    protected double k;

}
interface FooHandler {
    void accept(IFooVisitor visitor, absFoo foo);
}
class DefaultFooHandler implements FooHandler
{
    public void accept(IFooVisitor visitor, absFoo foo)
    {
        visitor.visit(this, foo);
    }
    public void handleFoo(absFoo foo) {
        System.out.println("DefaultFooHandler");
    }
 }
class ImprovedFooHandler implements FooHandler
{
    public void handleFoo(absFoo foo)
    {
        System.out.println("ImprovedFooHandler");
    }

    public void accept(IFooVisitor visitor, absFoo foo) {
        visitor.visit(this, foo);
    }

}

interface IFooVisitor {
    public void visit(DefaultFooHandler fooHandler, absFoo foo);
    public void visit(ImprovedFooHandler fooHandler, absFoo foo);
}

class FooVisitor implements IFooVisitor{
    public void visit(DefaultFooHandler fHandler, absFoo foo) {
        fHandler.handleFoo(foo);
    }

    public void visit(ImprovedFooHandler iFhandler, absFoo foo) {
        iFhandler.handleFoo(foo);
    }


}

public class Visitor {
    public static void main(String args[]) {
        absFoo df = new Foo();
        absFoo idf = new ImprovedFoo();

        FooHandler handler = new ImprovedFooHandler();

        IFooVisitor visitor = new FooVisitor();
        handler.accept(visitor, idf);

    }
}

Но это не гарантирует, что только Foo может быть передано в DefaultFooHandler. Это позволяет ImprovedFoo также может быть передан DefaultFooHandler. Чтобы преодолеть, что-то подобное можно сделать

class Foo
{
    protected int z;

}
class ImprovedFoo
{
    protected double k;

}

interface FooHandler {
    void accept(IFooVisitor visitor);
}

class DefaultFooHandler implements FooHandler
{
    private Foo iFoo;

    public DefaultFooHandler(Foo foo) {
        this.iFoo = foo;
    }

    public void accept(IFooVisitor visitor)
    {
        visitor.visit(this);
    }
    public void handleFoo() {
        System.out.println("DefaultFooHandler");
    }
 }

class ImprovedFooHandler implements FooHandler
{
    private ImprovedFoo iFoo;

    public ImprovedFooHandler(ImprovedFoo iFoo) {
        this.iFoo = iFoo;
    }

    public void handleFoo()
    {
        System.out.println("ImprovedFooHandler");
    }

    public void accept(IFooVisitor visitor) {
        visitor.visit(this);
    }

}

interface IFooVisitor {
    public void visit(DefaultFooHandler fooHandler);
    public void visit(ImprovedFooHandler fooHandler);
}

class FooVisitor implements IFooVisitor{
    public void visit(DefaultFooHandler fHandler) {
        fHandler.handleFoo();
    }

    public void visit(ImprovedFooHandler iFhandler) {
        iFhandler.handleFoo();
    }


}
public class Visitor {
    public static void main(String args[]) {
        FooHandler handler = new DefaultFooHandler(new Foo());
        FooHandler handler2 = new ImprovedFooHandler(new ImprovedFoo());

        IFooVisitor visitor = new FooVisitor();
        handler.accept(visitor);

        handler2.accept(visitor);

    }
}
Другие вопросы по тегам