Статьи

Scala черты реализации и взаимодействия. Часть II: черты линеаризации

Проблему
ужасного
алмаза можно решить, используя черты Scala и процесс, называемый
линеаризацией . Возьмите следующий пример:

trait Base {
    def msg = "Base"
}
 
trait Foo extends Base {
    abstract override def msg = "Foo -> " + super.msg
}
 
trait Bar extends Base {
    abstract override def msg = "Bar -> " + super.msg
}
 
trait Buzz extends Base {
    abstract override def msg = "Buzz -> " + super.msg
}
 
class Riddle extends Base with Foo with Bar with Buzz {
    override def msg = "Riddle -> " + super.msg
}

Теперь позвольте мне задать вам небольшой вопрос: каков результат
(new Riddle).msg?

  1. Riddle -> Base
  2. Riddle -> Buzz -> Base
  3. Riddle -> Foo -> Base
  4. Riddle -> Buzz -> Bar -> Foo -> Base

Это не (1), потому что
Base.msgпереопределяется всеми чертами, которые мы расширяем, так что это не должно быть сюрпризом. Но это также не (2) и (3). Можно ожидать, что либо
Buzzили
Fooнапечатан, помня, что вы можете сложить черты и либо первый или последний (на самом деле: последний) побед. Так почему
Riddle -> Buzz -> Baseневерно? Разве не
Buzz.msgзвонит
super.msgи
Buzzявно заявляет,
Baseчто это родитель? Здесь есть немного магии.

Когда вы складываете несколько признаков, как мы это делали (
extends Base with Foo with Bar with Buzz), компилятор Scala упорядочивает их (
линеаризует ), так что всегда есть один путь от каждого класса до parent (
Base). Порядок определяется обратным порядком смешанных черт (последний
выигрываети становится первым). Почему ты когда-либо …? Оказывается, составные черты отлично подходят для реализации нескольких слоев украшения вокруг реального объекта. Вы можете легко добавлять декораторы и перемещать их.

У нас есть простая абстракция калькулятора и одна реализация:

trait Calculator {
    def increment(x: Int): Int
}
 
class RealCalculator extends Calculator {
    override def increment(x: Int) = {
        println(s"increment($x)")
        x + 1
    }
}

Мы придумали три аспекта, которые мы хотели бы применять выборочно в зависимости от некоторых обстоятельств: регистрация всех
increment()вызовов, кэширование и проверка. Сначала давайте определим их всех:

trait Logging extends Calculator {
    abstract override def increment(x: Int) = {
        println(s"Logging: $x")
        super.increment(x)
    }
}
 
trait Caching extends Calculator {
    abstract override def increment(x: Int) =
        if(x < 10) {    //silly caching...
            println(s"Cache hit: $x")
            x + 1
        } else {
            println(s"Cache miss: $x")
            super.increment(x)
        }
}
 
trait Validating extends Calculator {
    abstract override def increment(x: Int) =
        if(x >= 0) {
            println(s"Validation OK: $x")
            super.increment(x)
        } else
            throw new IllegalArgumentException(x.toString)
}

Создание «сырого» калькулятора конечно возможно:

val calc = new RealCalculator
calc: RealCalculator = RealCalculator@bbd9e6
 
scala> calc increment 17
increment(17)
res: Int = 18

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

scala> val calc = new RealCalculator with Logging with Caching with Validating
calc: RealCalculator with Logging with Caching with Validating = $anon$1@1aea543
 
scala> calc increment 17
Validation OK: 17
Cache miss: 17
Logging: 17
increment(17)
res: Int = 18
 
scala> calc increment 9
Validation OK: 9
Cache hit: 9
res: Int = 10

Видите, как последующие миксины включаются? Конечно, каждый миксин может пропустить
superвызов, например, при попадании в кеш или сбое проверки. Просто чтобы прояснить это — не имеет значения, что каждый из украшающих миксинов
Calculatorопределен как базовая черта.
super.increment()всегда направляется на следующую черту в стеке (предыдущую в объявлении класса). Это означает, что
superон более динамичен и зависит от целевого использования, а не от объявления. Мы объясним это позже, но сначала еще один пример: давайте поместим ведение логов перед кэшированием, поэтому независимо от того, был ли кеш попаданием или пропуском, мы всегда получим логи. Более того, мы «отключаем» проверку, просто пропуская ее:

scala> class VerboseCalculator extends RealCalculator with Caching with Logging
defined class VerboseCalculator
 
scala> val calc = new VerboseCalculator
calc: VerboseCalculator = VerboseCalculator@f64dcd
 
scala> calc increment 42
Logging: 42
Cache miss: 42
increment(42)
res: Int = 43
 
scala> calc increment 4
Logging: 4
Cache hit: 4
res: Int = 5

Я обещал объяснить, как работает укладка. Вам должно быть действительно любопытно, как superреализован этот «фанк»,
поскольку он не может просто полагаться на
invokespecialинструкцию байт-кода, используемую с normal
super. К сожалению, это сложно, но стоит знать и понимать, особенно когда укладка не работает должным образом.
Calculatorи
RealCalculatorскомпилировать в точности то, что вы могли ожидать:

public interface Calculator {
    int increment(int i);
}
 
public class RealCalculator implements Calculator {
    public int increment(int x) {
        return x + 1;
    }
}

Но как будет реализован следующий класс?

class FullBlownCalculator
    extends RealCalculator
       with Logging
       with Caching
       with Validating

Давайте начнем с самого класса:

public class FullBlownCalculator extends RealCalculator implements Logging, Caching, Validating {
    public int increment(int x) {
        return Validating$class.increment(this, x);
    }
 
    public int Validating$$super$increment(int x) {
        return Caching$class.increment(this, x);
    }
 
    public int Caching$$super$increment(int x) {
        return Logging$class.increment(this, x);
    }
 
    public int Logging$$super$increment(int x) {
        return super.increment(x);
    }
}

Вы видите, что здесь происходит? Прежде чем я покажу реализации всех этих
*$classклассов, уделите немного времени рассмотрению объявлений классов (в частности, порядка черт) и этих неуклюжих
*$$super$*методов. Вот недостающий фрагмент, который позволит нам соединить все точки:

public abstract class Logging$class {
    public static int increment(Logging that, int x) {
        return that.Logging$$super$increment(x);
    }
}
 
public abstract class Caching$class {
    public static int increment(Caching that, int x) {
        return that.Caching$$super$increment(x);
    }
}
 
public abstract class Validating$class {
    public static int increment(Validating that, int x) {
        return that.Validating$$super$increment(x);
    }
}

Не полезно? Давайте медленно пройдем первый шаг. При звонке
FullBlownCalculator, согласно чертам правил укладки,
RealBlownCalculator.increment()следует звонить
Validating.increment(). Как видите,
Validating.increment()перенаправляет
this(себя) в статический
Validating$class.increment()скрытый класс. Этот класс ожидает экземпляр класса
Validating, но, поскольку он
FullBlownCalculatorтакже расширяет эту черту, передача
thisв порядке.

Теперь посмотри на
Validating$class.increment(). Это только вперед
FullBlownCalculator.Validating$$super$increment(x). И когда мы снова вернемся к,
FullBlownCalculatorмы заметим, что этот метод делегирует static
Caching$class.increment(). Отсюда процесс похож. Почему дополнительное делегирование через
staticметод? Миксины не знают, какой класс будет следующим в стеке («следующий
super»). Таким образом они просто делегируют соответствующие виртуальные
$$super$семейство методов. Каждый класс, использующий эти миксины, обязан их реализовывать, предоставляя правильные «супер».

Чтобы представить это в перспективе: компилятор не может просто делегировать прямо из
Validating$class.increment()в
Caching$class.increment(), даже если это
FullBlowCalculatorрабочий процесс. Однако, если мы создадим другой класс, который обращает эти
RealCalculator with Validating with Cachingжестко закодированные зависимости mixins (), больше не будет действительным. За объявление заказа отвечает класс, а не миксин.

Если вы все еще не подписаны, вот полный стек вызовов для
FullBlownCalculator.increment():

val calc = new FullBlownCalculator
calc increment 42
 
FullBlownCalculator.increment(42)
`- Validating$class.increment(calc, 42)
   `- Validating.Validating$$super$increment(42) (on calc)
      `- Caching$class.increment(calc, 42)
         `- Caching.Caching$$super$increment(42) (on calc)
            `- Logging$class.increment(calc, 42)
               `- Logging.Logging$$super$increment(42) (on calc)
                  `- super.increment(42)
                     `- RealCalculator.increment(42) (on calc)

Теперь вы понимаете, почему это называется «
линеаризация »!