Проблема проектирования множественного наследования в Java

Как вы справляетесь с единственным наследованием в Java? Вот моя конкретная проблема:

У меня есть три (упрощенных) класса:

public abstract class AbstractWord{
    String kind;    // eg noun, verb, etc

    public String getKind(){ return kind; }

}

public class Word extends AbstractWord{
    public final String word;

    ctor...

    public void setKind(){
        // based on the variable word calculate kind..
    }
}

public class WordDescriptor extends AbstractWord{

    ctor..

    public void setKind(String kind){this.kind = kind;}

}

Это то, что я считаю своей самой базовой реализацией, но я хочу сделать другие реализации.

Допустим, я хочу добавить новую переменную, скажем, wordLength, но я хочу добавить ее с помощью наследования. Это означает, что я НЕ хочу изменять этот исходный класс AbstractWord. То есть что-то вроде этого:

public class Length{
    private int length;

    public int getLength(){return length};
}

public class BetterWord extends AbstractWord AND Length{

    public void setLength(){
        // based on the variable word calculate Length..
    }
}

public class BetterWordDescriptor extends AbstractWord AND length{

    public void setLength(int length){this.length = length;}
}

Я знаю, что java не позволяет мне это сделать, но это сделало мой код очень уродливым. Прямо сейчас, когда я добавляю поле, я просто добавляю его в AbstractWord, но затем мне нужно либо переименовать это AbstractWord (и Word, и WordDescriptor). (Я не могу просто добавить одно поле к другому из-за обратной совместимости, оно нарушает методы equals и тому подобное).

Это кажется довольно распространенной проблемой дизайна, но я ломал голову и не могу придумать красивого решения.

Есть ли шаблон проектирования, который решает эту проблему? У меня есть несколько потенциальных решений, но я хотел посмотреть, есть ли что-то, чего мне не хватает.

спасибо, Джейк

Обновление: длина относится к количеству слогов в слове (извините за отсутствие ясности)


person sixtyfootersdude    schedule 26.01.2010    source источник
comment
Должен ли Length быть классом, а не интерфейсом?   -  person David R Tribble    schedule 27.01.2010
comment
@Loadmaster: Length — это класс, а не интерфейс, поэтому методы длины не нужно дублировать как в Word, так и в WordDescriptor. Для длины эти методы просты, но для других вещей они могут быть очень сложными.   -  person sixtyfootersdude    schedule 27.01.2010
comment
@Jake: Если другие вещи могут быть очень сложными, вам обязательно следует использовать шаблон стратегии.   -  person Gladwin Burboz    schedule 27.01.2010


Ответы (7)


Предпочитайте композицию наследованию.

Решение принимает во внимание, что может быть другой тип слова, которому может потребоваться WordLengthSupport.

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

.

public class WordLength {
    private int length = 0;
    public int getLength(){return length};
    public void setLength(int length){this.length = length};
}

.

public interface WordLengthSupport {
    public WordLength getWordLength();
}

.

public class BetterWord extends AbstractWord 
        implements WordLengthSupport {
    WordLength wordLength;
    public WordLength getWordLength() {
        if(wordLength==null) {
            // each time word changes 
            // make sure to set wordLength to null
            calculateWordLength(); 
        }
        return wordLength;
    }
    private void calculateWordLength() {
        // This method should be 
        //    called in constructor 
        //    or each time word changes 
        int length = // based on the variable word calculate Length..
        this.wordLength = new WordLength();
        this.wordLength.setLength(length);
    }
}

.

public class BetterWordDescriptor extends AbstractWord  
        implements WordLengthSupport {
    WordLength wordLength;
    public WordLength getWordLength(return wordLength);
    public void setWordLength(WordLength wordLength) {
        // Use this to populate WordLength of respective word
        this.wordLength = wordLength;
    }
}

.

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

В этом решении не используется шаблон стратегии, но его можно реорганизовать.

person Gladwin Burboz    schedule 27.01.2010
comment
+1 Вау, это отличное решение, не суперинтуитивное, но я думаю, что оно должно работать довольно чисто. - person sixtyfootersdude; 27.01.2010
comment
Ваша функция private WordLength calculateWordLength() не возвращает значение и должна быть недействительной. - person Joseph Gordon; 27.01.2010
comment
Это точно кросвенир. Я написал это в блокноте, чтобы не проверять синтаксис. Спасибо за указание, я обновлю его. - person Gladwin Burboz; 27.01.2010
comment
Спасибо, Джейк. Я рад, что смог помочь. - person Gladwin Burboz; 27.01.2010

Просто используйте композицию вместо наследования:

a BetterWord является AbstractWord, который имеет Length:

public class BetterWord extends AbstractWord {

    private Length length;

    public void setLength(int value){
        length.setLength(value);
    }
}

ИЗМЕНИТЬ

Если API нужен объект типа Length, просто добавьте геттер:

public class BetterWord extends AbstractWord {

    private Length length;

    public void setLength(int value){
        length.setLength(value);
    }

    public Length getLength() {
        return length
    }
}

Или переименуйте реализацию Length в LengthImpl и определите интерфейс Length, поскольку класс может реализовывать несколько интерфейсов.

person Andreas Dolk    schedule 26.01.2010
comment
@Andreas, полностью согласен с использованием has-a по сравнению с is-a, но я бы подумал, что установка длины слова - это то, что не будет изменено для времени жизни объекта определенного слова. - person Rob Wells; 27.01.2010
comment
Если есть API, которому нужен объект типа Length, это не сработает. Смотрите мое решение ниже. - person Gladwin Burboz; 27.01.2010

В вашем конкретном примере вы можете использовать шаблон декоратора в сочетании с интерфейсами, чтобы дополнить ваш Word класс дополнительный функционал; например

// *Optional* interface, useful if we wish to reference Words along with
// other classes that support the concept of "length".
public interface Length {
  int getLength();
}

// Decorator class that wraps another Word and provides additional
// functionality.  Could add any additional fields here too.
public class WordExt extends AbstractWord implements Length {
  private final Word word;

  public class(Word word) {
    this.word = word;
  }

  public int getLength() {
    return word.getKind().length();
  }
}

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

person Adamski    schedule 26.01.2010
comment
WordExt имеет две копии вида атрибута. Первый objWordExt.kind унаследован от AbstractWord. Второй objWordExt.word.kind, который он получает из слова переменной экземпляра. - person Gladwin Burboz; 27.01.2010
comment
Я думал использовать что-то подобное, но в данном случае это не сработает, потому что некоторые методы по длине довольно длинные и сложные. Используя декоратор, мне нужно было бы реализовать их как в классе Word, так и в классе WordDescriptor. Спасибо за предложение. - person sixtyfootersdude; 27.01.2010
comment
@GB: Хороший вопрос. Обычно я определял интерфейс Word, который WordExt реализовывал бы, чтобы обойти избыточную копию вида. - person Adamski; 27.01.2010

Глядя на это, первое, что я чувствую, это то, что ваша модель немного сложна.

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

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

Например, зачем вам WordDescriptor, расширяющий AbstractWord? Превратится ли слово из глагола в прилагательное? Я бы подумал, что тип слова был установлен при создании объекта и не изменится в течение всего времени существования объекта Word. Если у вас есть объект Word для слова «Австралия», вид слова не изменится в течение всего времени существования объекта.

Хм. Возможно, у вас может быть объект Word, представляющий слово «лай», после создания экземпляра объекта с типом «глагола» для описания звука, издаваемого собакой. Затем вы понимаете, что на самом деле вам нужно было иметь объект Word для представления существительного, описывающего покрытие дерева. Возможно, но может существовать и кора собаки, и кора дерева.

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

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

Когда я говорю, что класс B расширяет класс A, могу ли я сказать, что класс B «является» классом A и что я специализируюсь на его поведении?

Например, базовый класс Animal может быть расширен для предоставления специализированного класса Kangaroo. Тогда вы можете сказать, что «кенгуру» — это «Животное». Вы специализируетесь на поведении.

Затем посмотрите на атрибуты, у кенгуру есть атрибут местоположения, описывающий, где он находится. Тогда вы можете сказать, что кенгуру «имеет» местоположение. Расположение кенгуру «есть-а» не имеет смысла.

Точно так же слово «имеет» длину. И утверждение, что длина Word "is-a" просто не имеет смысла.

Кстати, все ссылки на Австралию в этом посте сделаны специально для празднования Дня Австралии, который отмечается сегодня, 26 января!

ХТН

person Rob Wells    schedule 26.01.2010
comment
Желаю вам радостного Дня Австралии. Я согласен с вашими комментариями к WordDescriptor. Будет лучше, если у AbstractWord есть поле (с геттером и сеттером) WordDescriptor. - person Gladwin Burboz; 27.01.2010

(Я не могу просто добавить одно поле к другому из-за обратной совместимости, оно нарушает методы equals и тому подобное).

Это не нарушит совместимость источника. Нет, если вы не делаете что-то действительно сумасшедшее в своих методах equals.

И переименование ваших классов, как правило, не способ справиться с двоичной совместимостью.

person Anon.    schedule 26.01.2010
comment
Равенство для WordDescriptor основано на всех полях => нарушение обратной сопоставимости. - person sixtyfootersdude; 27.01.2010
comment
Я думаю, вы обнаружите, что если два дескриптора Word в остальном равны, они будут иметь одинаковую длину слова независимо от того, что происходит. Так что нет, это не нарушает совместимость источника. - person Anon.; 27.01.2010

Проблема не в том, «как поступить с одиночным наследованием». Чего вам не хватает, так это не шаблона проектирования, а обучения разработке API отдельно от реализации.

Я бы реализовал это так:

public interface WordDescriptor {
    void getKind();
    Word getWord();
}

public interface Word {
    String getWord();
}

public class SimpleWord implements Word {
    private String word;

    public SimpleWord(String word) { this.word = word; }
    public String getWord() { return word; }
}

public class SimpleWordDescriptor implements WordDescriptor {
    private Word word;
    private String kind;

    public SimpleWordDescriptor(Word word, String kind) {
        this.word = word;
        this.kind = kind; // even better if WordDescriptor can figure it out internally
    }

    public Word getWord() { return word; }

    public String getKind() { return kind; }
}

С этой базовой настройкой, когда вы хотите ввести свойство длины, все, что вам нужно сделать, это следующее:

public interface LengthDescriptor {
    int getLength();
}

public class BetterWordDescriptor extends SimpleWordDescriptor
                                  implements LengthDescriptor {
    public BetterWordDescriptor(Word word, String kind) {
        super(word, kind);
    }

    public int getLength() { getWord().length(); }        
}

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

person aberrant80    schedule 27.01.2010
comment
Спасибо за комментарий, однако, возможно, мне было неясно, что такое WordDescriptor. Дескриптор слова не содержит слова. Один WordDescriptor может описывать множество слов. Например, WordDescriptor = существительное может относиться ко всем существительным. Поэтому не имеет смысла, если WordDescriptor содержит слово. - person sixtyfootersdude; 27.01.2010

/** * Первый пример */

class FieldsOfClassA {
    public int field1;
    public char field2;
}

interface IClassA {
    public FieldsOfClassA getFieldsA();
}

class CClassA implements IClassA {
    private FieldsOfClassA fields;

    @Override
    public FieldsOfClassA getFieldsA() {
        return fields;
    }
}

/**
 * seems ok for now
 * but let's inherit this sht
 */


class FieldsOfClassB {

    public int field3;
    public char field4;
}

interface IClassB extends IClassA {

    public FieldsOfClassA getFieldsA();
    public FieldsOfClassB getFieldsB();
}

class CClassB implements IClassB {

    private FieldsOfClassA fieldsA;
    private FieldsOfClassB fieldsB;

    @Override
    public FieldsOfClassA getFieldsA() {
        return fieldsA;
    }

    @Override
    public FieldsOfClassB getFieldsB() {
        return fieldsB;
    }
}

/**

  • вау, этот монстр стал больше

  • представь что тебе понадобится 4 лвл наследства

  • столько времени ушло бы на то, чтобы написать этот ад

  • Я даже не говорю, что пользователь этих айфейсов подумает

  • какие поля мне понадобятся поляA поляB поляC или другое

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

Когда вы думаете об объектно-ориентированном программировании

вам нужны БОЛЬШИЕ модели с 6-7 уровнями множественного наследования

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

Если ваши модели требуют 2 уровня наследования, перестаньте притворяться, используя OO

Вы можете легко реализовать это на любом языке, даже на процедурном, таком как язык C или Basic */

person Dss D    schedule 04.09.2015