Как реализация может быть заменена во время выполнения для составления объекта (наследование интерфейсов)

Я натолкнулся на следующий пункт the advantage of object composition over class inheritance. Но я часто вижу следующее предложение во многих статьях

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

Но сомнение может быть наивным, так как я новичок. Как реализация может быть заменена во время выполнения? Если мы напишем новую строку кода, разве нам не нужно компилировать, чтобы отразить изменение? Тогда что означает replacing at runtime? довольно запутанным. Или любая другая магия, за сценой происходят события. Может кто-нибудь ответить.

Ответ 1

Подумайте о реализации Stack. Простая реализация Stack использует List за кулисами. Так наивно, вы можете расширить ArrayList. Но теперь, если вам нужен отдельный Stack, поддерживаемый LinkedList, вам нужно будет иметь два класса: ArrayListStack и LinkedListStack. (Этот подход также имеет недостаток в экспонировании методов List на Stack, что нарушает инкапсуляцию).

Если вы использовали композицию вместо этого, List, чтобы вернуть Stack, может быть предоставлен вызывающим, и у вас может быть один класс Stack, который может принимать либо LinkedList, либо ArrayList, в зависимости от по характеристикам времени выполнения, которые пожелает пользователь.

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

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

Примеры реальной жизни

Кстати, я использую пример Stack, потому что он не является чисто гипотетическим. Класс Java Stack на самом деле расширенный Vector, который навсегда оставил багаж синхронизации и характеристики производительности списка с поддержкой массива. В результате использование этого класса сильно обескуражено.

Прекрасный пример правильного использования композиции для коллекции также можно найти в библиотеке Java, в Collections.newSetFromMap(Map). Так как любой Map можно использовать для представления Set (используя фиктивные значения), этот метод возвращает a Set, состоящий из переданного Map. Возвращаемый Set затем наследует характеристики Map, которые он обертывает, например: изменчивость, безопасность потоков и производительность во время выполнения - все без необходимости создавать параллельные реализации Set до ConcurrentHashMap, ImmutableMap, TreeMap и т.д.

Ответ 2

Есть две веские причины предпочесть композицию над наследованием:

  • Предотвращает комбинаторные взрывы в иерархии классов.
  • Может быть изменен во время выполнения

Скажем, что вы пишете систему заказов для пиццерий. У вас почти наверняка будет классная пицца...

public class Pizza {
    public double getPrice() { return BASE_PIZZA_PRICE; }
}

И, при прочих равных условиях, пиццерий, вероятно, продает много пиццы пепперони. Вы можете использовать наследование для этого - PepperoniPizza удовлетворяет отношения "is-a" с пиццей, поэтому звучит правильно.

public class PepperoniPizza extends Pizza {
    public double getPrice() { return super.getPrice() + PEPPERONI_PRICE; }
}

Хорошо, пока все хорошо, правда? Но вы, вероятно, можете видеть, что мы не рассмотрели. Что, если клиент хочет, например, пепперони и грибы? Ну, мы можем добавить класс PepperoniMushroomPizza. У нас уже есть проблема - должен ли PepperoniMushroomPizza продлить пиццу, PepperoniPizza или MushroomPizza?

Но все становится еще хуже. Позвольте сказать, что наш гипотетический пиццерий предлагает размеры Малый, Средний и Большой. И корка тоже меняется - они предлагают толстую, тонкую и правильную корку. Если мы просто используем наследование, у нас есть классы, такие как MediumThickCrustPepperoniPizza, LargeThinCrustMushroomPizza, SmallRegularCrustPepperoniAndMushroomPizza и т.д....

public class LargeThinCrustMushroomPizza extends ThinCrustMushroomPizza {
    // This is not good!
}

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

Вторая проблема (модификация во время выполнения) также вытекает из этого. Предположим, что клиент смотрит на цену своей LargeThinCrustMushroomPizza, gawks и решает, что вместо этого они предпочтут MediumThinCrustMushroomPizza? Теперь вы застреваете, создавая совершенно новый объект, чтобы изменить этот атрибут!

В этом заключается композиция. Мы наблюдаем, что "пицца пепперони" действительно имеет отношения "есть-а" с пиццей, но она также удовлетворяет отношения "has-a" с Пепперони. И он также удовлетворяет "has-a" отношениям с типом коры и размером. Итак, вы переопределяете пиццу с помощью композиции:

public class Pizza { 
    private List<Topping> toppings;
    private Crust crust;
    private Size size;

    //...omitting constructor, getters, setters for brevity...

    public double getPrice() {
        double price = size.getPrice();
        for (Topping topping : toppings) {
            price += topping.getPriceAtSize(size);
        }
        return price;
    }
}

С помощью этой пиццы на основе композиции клиент может выбрать меньший размер (pizza.setSize(new SmallSize())), а цена (getPrice()) будет отвечать соответствующим образом - то есть поведение во время выполнения метода может варьироваться согласно составу времени выполнения объекта.

Это не значит, что наследование плохое. Но там, где можно использовать композицию вместо наследования, чтобы выразить разнообразие объектов (например, пиццы), композиция обычно должна быть предпочтительной.

Ответ 3

Другие ответы говорят об этом немного, но я подумал, что пример того, как поведение может измениться во время выполнения, будет полезным. Предположим, у вас есть интерфейс Printer:

interface Printer {
    void print(Printable printable);
}

class TestPrinter implements Printer {

    public void print(Printable printable) {
        // set an internal state that can be checked later in a test
    }

}

class FilePrinter implements Printer {

    public void print(Printable printable) {
        // Do stuff to print the printable to a file
    }
}

class NetworkPrinter implements Printer {

    public void print(Printable printable) {
        // Connects to a networked printer and tell it to print the printable
    }
}

Все классы принтера теперь могут использоваться для разных целей. TestPrinter может использоваться как макет или stubb, когда мы запускаем тесты. FilePrinter и NetworkPrinter каждый обрабатывает конкретный случай при печати. Итак, предположим, что у нас есть виджет пользовательского интерфейса, где пользователь может нажать кнопку для печати:

class PrintWidget {
    // A collection of printers that keeps track of which printer the user has selected.
    // It could contain a FilePrinter, NetworkPrinter and any other object implementing the
    // Printer interface
    private Selectable<Printer> printers; 

    // A reference to a printable object, could be a document or image or something
    private Printable printable;

    public void onPrintButtonPressed() {
        Printer printer = printers.getSelectedPrinter();
        printer.print(printable);
    }

    // other methods 
}

Теперь во время выполнения, когда пользователь выбирает другой принтер и нажимает кнопку печати, вызывается метод onPrintButtonPressed и выбранный Printer.

Ответ 4

Это Полиморфизм, который является основной концепцией ООП.

Это означает, что состояние с множеством форм или 'способность принимать разные формы. При применении к объектно-ориентированным языкам программирования, таким как Java, он описывает способность языков обрабатывать объекты разных типов и классов с помощью единого однородного интерфейса.

Как отмечено, List - это унифицированный интерфейс, а его различные реализации похожи на ArrayList..... и т.д.

Ответ 5

Это интересно ответить. Я не уверен, что вы использовали шаблон factory или нет. Но если вы тогда понимаете это, то этот пример должен быть хорошим. Позвольте мне попытаться сказать это здесь: Предположим, что у вас есть родительский класс под названием Pet, как определено здесь пакет com.javapapers.sample.designpattern.factorymethod;

//super class that serves as type to be instantiated for factory method pattern
public interface Pet {

 public String speak();

}

И есть несколько подклассов, таких как Dog, Duck и т.д., здесь:

package com.javapapers.sample.designpattern.factorymethod;

//sub class 1 that might get instantiated by a factory method pattern
public class Dog implements Pet {

 public String speak() {
 return "Bark bark...";
 }
}

package com.javapapers.sample.designpattern.factorymethod;

//sub class 2 that might get instantiated by a factory method pattern
public class Duck implements Pet {
 public String speak() {
 return "Quack quack...";
 }
}

И есть класс factory, который возвращает вам Pet в зависимости от типа ввода, пример здесь:

package com.javapapers.sample.designpattern.factorymethod;

//Factory method pattern implementation that instantiates objects based on logic
public class PetFactory {

 public Pet getPet(String petType) {
 Pet pet = null;

 // based on logic factory instantiates an object
 if ("bark".equals(petType))
 pet = new Dog();
 else if ("quack".equals(petType))
 pet = new Duck();
 return pet;
 }
}

Теперь давайте посмотрим, как на время выполнения мы можем иметь разные виды домашних животных, созданные в зависимости от ввода, образец здесь

//using the factory method pattern
public class SampleFactoryMethod {

    public static void main(String args[]) {

        // creating the factory
        PetFactory petFactory = new PetFactory();

        System.out.println("Enter a pets language to get the desired pet");
        String input = "";

        try {
            BufferedReader bufferRead = new BufferedReader(
                    new InputStreamReader(System.in));
            input = bufferRead.readLine();



            // factory instantiates an object
            Pet pet = petFactory.getPet(input);

            // you don't know which object factory created
            System.out.println(pet.speak());

        } catch (IOException e) {
            e.printStackTrace();
        }


    }

}

Теперь, если вы запустите программу для разных типов входов, таких как "кора" или "quack", вы получите отличное домашнее животное. Вы можете изменить вышеуказанную программу, чтобы использовать разные входы и создавать разные Домашние животные.

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

Надеюсь, что это поможет!

Ответ 6

Как реализация может быть заменена во время выполнения?

Позвольте использовать пример кода для яркого дня (цикл, который читает новую строку каждый раз и перепечатывает все прочитанные строки):

List<String> myList = new ArrayList<String>(); // chose first "implementation"

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
    String line = br.readLine(); // do something like read input from the user
    myCounter.resetChronometer(); // hypothetical time counter

    myList.add(line); // add the line (make use my "implementation")
    // and then do some final work, like printing...
    for(String s: myList) {
        System.out.println(s); // print it all...
    }

    //But, hey, I'm keeping track of the time:
    myCounter.stopChronometer();
    if (myCounter.isTimeTakenTooLong())
        // this "implementation" is too slow! I want to replace it.
        // I WILL replace it at runtime (no recompile, not even stopping)
        List<String> swapList = myList; // just to keep track...

        myList = new LinkedList<String>(); // REPLACED implementation! (!!!) <---

        myList.addAll(swapList); // so I don't lose what I did up until now

        // from now on, the loop will operate with the 
        // new implementation of the List<String>
        // was using the ArrayList implementation. Now will use LinkedList
    }
}

Как вы сказали: Это возможно [только], потому что объекты [ myList ] доступны только через их интерфейсы ( List<String> ). (Если бы мы объявили myList как ArrayList<String> myList, это никогда не было бы возможно...)