Общий ОО вопрос о наследовании и расширяемости - PullRequest
0 голосов
/ 15 марта 2011

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

Вот что явыполняется (пример кода на 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 ]

3 голосов
/ 16 марта 2011

Прежде всего написанный вами код не будет работать. Каждый раз, когда вы видите 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 );
2 голосов
/ 16 марта 2011

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

Пример 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 следует объединить в строку.

Случай 3: Фигуры

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

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

Теперь вы вводите новый вид фигуры:

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 () для всех объектов LogicGameObject?В этом случае добавление пустого метода Logic () в GameObject кажется лучшим вариантом.В описании работы GameObject вполне можно ожидать, что он сможет знать, что делать с обновлением логики, даже если это ничего не значит.

Заключение

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

1 голос
/ 15 марта 2011

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

1 голос
/ 15 марта 2011

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

0 голосов
/ 17 марта 2011

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

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

    }
}
0 голосов
/ 15 марта 2011

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

...