Решают ли примеси хрупкие проблемы базового класса?

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

Приведу пример возможной проблемы. Это простая реализация задачи (Java) на языке Scala, которую профессор дал нам для иллюстрации проблемы.

Рассмотрим следующий базовый класс. Предположим, что это какая-то очень эффективная специальная реализация списка, и что над ним определено больше операций.

class MyList[T] {
    private var list : List[T] = List.empty
    def add(el:T) = {
        list = el::list
    }
    def addAll(toAdd:List[T]) : Unit = {
        if (!toAdd.isEmpty) {
            add(toAdd.head)
            addAll(toAdd.tail)
        }
    }
}

Также обратите внимание на следующую черту, которая должна добавить size к приведенному выше списку.

trait CountingSet[T] extends MyList[T] {
    private var count : Int = 0;
    override def add(el:T) = {
        count = count + 1
        super.add(el)
    }
    override def addAll(toAdd:List[T]) = {
        count = count + toAdd.size;
        super.addAll(toAdd);
    }
    def size : Int = { count }
}

Проблема в том, что сработает трейт или нет, зависит от того, как мы реализуем addAll в базовом классе, т. е. функциональность, предоставляемая базовым классом, «хрупка», как и в случае с обычным extends в Java или любом другом. язык программирования.

Например, если мы запустим следующий код с MyList и CountingSet, как определено выше, мы получим обратно 5, тогда как мы ожидали получить 2.

object Main {
    def main(args:Array[String]) : Unit = {
        val myCountingSet = new MyList[Int] with CountingSet[Int]
        myCountingSet.addAll(List(1,2))
        println(myCountingSet.size) // Prints 5
    }
}

Если мы изменим addAll в базовом классе (!) следующим образом, трейт CountingSet будет работать как положено.

class MyList[T] {
    private var list : List[T] = List.empty
    def add(el:T) = {
        list = el::list
    }
    def addAll(toAdd:List[T]) : Unit = {
        var t = toAdd;
        while(!t.isEmpty) {
            list = t.head::list
            t = t.tail
        }
    }
}

Имейте в виду, что я совсем не эксперт по Scala!


person Semafoor    schedule 23.01.2013    source источник
comment
Ну, может быть, это только я, но я не вижу, как трейты/примеси решают проблемы хрупкого базового класса. Хотя у них много других качеств.   -  person Régis Jean-Gilles    schedule 23.01.2013
comment
Я тоже не вижу, значит не только у вас :). Если они действительно бесполезны, может быть действительно сложно ответить на мой вопрос... Я готов принять решение "ваш профессор не прав", если оно убедительно. Может быть, это как-то связано с чертами, которые не являются «настоящими примесями»?   -  person Semafoor    schedule 23.01.2013


Ответы (1)


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

Черты действительно спасают вас от неприятностей там, где вы уже ожидаете возникновения проблемы; затем вы можете параметризовать свой трейт, чтобы он выполнял соответствующие действия, или смешать нужный вам трейт, который делает подходящее действие. Например, в библиотеке коллекций Scala трейт IndexedSeqOptimized используется не только для обозначения, но и также для реализации различных операций таким образом, который работает хорошо, когда индексация выполняется так же быстро, как и любой другой способ доступа к элементам библиотеки. коллекция. ArrayBuffer, который обертывает массив и, следовательно, имеет очень быстрый индексированный доступ (действительно, индексация — единственный путь в массив!) наследуется от IndexedSeqOptimized. Напротив, Vector можно индексировать достаточно быстро, но быстрее выполнить обход без явного индексирования, так что это не так. Если бы IndexedSeqOptimzed не было чертой, вам бы не повезло, потому что ArrayBuffer находится в изменяемой иерархии, а Vector — в неизменяемой иерархии, поэтому нельзя было бы создать общий абстрактный суперкласс (по крайней мере, не внося полный беспорядок в другие). унаследованная функциональность).

Таким образом, ваши хрупкие проблемы с базовым классом не решены; если вы измените, скажем, Traversable реализацию алгоритма так, чтобы он имел производительность O(n) вместо O(1) (возможно, для экономии места), вы, очевидно, не можете знать, может ли какой-нибудь ребенок использовать это повторно и генерировать производительность O(n^2), что может привести к катастрофическим последствиям. Но если вы знаете, это значительно упрощает исправление: просто подмешайте правильный признак, который имеет O(1) реализацию (и ребенок может делать это всякий раз, когда это необходимо). И это помогает разделить проблемы на концептуально согласованные единицы.

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

person Rex Kerr    schedule 23.01.2013
comment
может полностью -> я думаю, вы имели в виду не может. Кроме того, я до сих пор не понимаю, что особенного в трейтах, что делает их лучше, даже частично (вы говорите, что это помогает и может избавить вас от неприятностей), чем абстрактный класс для предотвращения проблем хрупкого базового класса. Ваши аргументы в пользу IndexedSeqOptimized были бы применимы точно так же, если бы это был абстрактный класс, не так ли? Тот факт, что это признак, делает его более общим и освобождает вас от жесткой иерархии (позволяя вам смешивать его в разных точках иерархии типов), но как это связано с решением проблемы хрупкости базового класса. - person Régis Jean-Gilles; 23.01.2013
comment
@ RégisJean-Gilles - я пытался уточнить. Дело в том, что вы можете использовать трейты везде, где они вам нужны, чтобы обеспечить самосогласованную функциональность, которая может обойти известные различия в реализации. Абстрактные классы можно использовать только один раз, поэтому они на самом деле не позволяют решить проблему (за исключением того, что они полностью ортогональны: создавать, не наследовать). - person Rex Kerr; 23.01.2013
comment
Хорошо, но тот факт, что трейты применимы в большем количестве ситуаций, чем абстрактные классы, не влияет на проблемы с хрупким базовым классом/базовым трейтом. Во всяком случае, проблемы видны только в большем количестве ситуаций с чертами. У меня все еще есть ощущение, что более общая применимость признаков ортогональна хрупким проблемам базового класса и возможным способам их смягчения. - person Régis Jean-Gilles; 24.01.2013
comment
@RégisJean-Gilles - Когда абстрактный класс может решить проблему, но вы не можете использовать его из-за множественного наследования, тогда возможность использовать трейт для выполнения того же самого абсолютно помогает решить проблему. Конечно, поскольку трейты упрощают композицию, вы также можете больше страдать от хрупкости ваших базовых классов, поскольку вы больше полагаетесь на их надежность. Возможно, обоюдоострый меч, но не ортогональный. - person Rex Kerr; 24.01.2013
comment
Кажется, у нас проблемы с пониманием. Либо у меня проблемы со связью, либо проблемы с пониманием (или я слишком устал, чтобы видеть очевидное?), пока не знаю :). Дай мне попробовать снова. Вы говорите, что возможность использовать черту, чтобы делать то же самое, абсолютно помогает решить проблему. Это, безусловно, решает одну проблему, но эта проблема не имеет ничего общего с конкретными проблемами хрупкого базового класса. Если мы не можем договориться об этом, я просто оставлю это. Надеюсь, мы получим другой ответ, который либо изложит другую позицию по этому поводу, либо объяснит ту же точку зрения, что и ваша, так, чтобы я мог понять и с ней согласиться. - person Régis Jean-Gilles; 24.01.2013
comment
@ RégisJean-Gilles - Я хочу сказать, что не существует волшебной палочки против хрупкости. Вы решаете проблему за счет тщательного проектирования. Проблема заключается в одной из операционных зависимостей; если вы даже не замечаете, что есть проблема, вы все равно застряли. Если вы замечаете признаки, но не имеете их, вы все равно вынуждены самостоятельно решать проблему в каждом дочернем классе. С помощью трейтов вы можете инкапсулировать желаемое поведение и широко его использовать; если базовый класс меняется, смотрим на трейт не все. Но ничто не спасет вас от плохо спроектированного базового класса, который небрежно изменен. - person Rex Kerr; 24.01.2013
comment
Я собираюсь принять это, хотя я не полностью понимаю ответ. На мой взгляд, черты могут усугубить проблему в том смысле, что они используются более широко, чем наследование. Также может быть сложно увидеть, какая именно черта ломает класс, так как их может быть много, и они могут влиять и даже ломать друг друга. Однако они очень полезны, и с их помощью вы можете создавать гибкие приложения, в которых не возникает проблема хрупкого базового класса. - person Semafoor; 25.01.2013
comment
@Semafoor - я думаю, что это справедливая характеристика. Они могут быть частью проблемы, частью решения или и тем, и другим, в зависимости от того, как вы их развертываете. Они, по крайней мере, обеспечивают путь к (частичному) решению, которое недоступно, если у вас нет признаков. - person Rex Kerr; 25.01.2013
comment
Действительно ли интерфейсы подвержены проблеме хрупкого базового класса? В конце концов, нет никакой реализации, на которую можно положиться или переопределить. - person jmrah; 13.11.2019