Статьи

Scala черты реализации и взаимодействия.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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 упорядочивает их ( линеаризует ), так что всегда есть один путь от каждого класса к родительскому ( Base ). Порядок определяется обратным порядком смешанных черт (последний выигрывает и становится первым). Почему ты когда-либо …? Оказывается, составные черты отлично подходят для реализации нескольких слоев украшения вокруг реального объекта. Вы можете легко добавлять декораторы и перемещать их. У нас есть простая абстракция калькулятора и одна реализация:

01
02
03
04
05
06
07
08
09
10
trait Calculator {
    def increment(x: Int): Int
}
  
class RealCalculator extends Calculator {
    override def increment(x: Int) = {
        println(s"increment($x)")
        x + 1
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)
}

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

1
2
3
4
5
6
val calc = new RealCalculator
calc: RealCalculator = RealCalculator@bbd9e6
  
scala> calc increment 17
increment(17)
res: Int = 18

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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 более динамичен и зависит от целевого использования, а не от объявления. Мы объясним это позже, но сначала еще один пример: давайте поместим ведение логов перед кэшированием, поэтому независимо от того, был ли кеш попаданием или пропуском, мы всегда получим логи. Более того, мы «отключаем» проверку, просто пропуская ее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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 инструкцию байт-кода, используемую с обычным super . К сожалению, это сложно, но стоит знать и понимать, особенно когда укладка не работает должным образом. Calculator и RealCalculator компилируются в точности так, как вы могли ожидать:

1
2
3
4
5
6
7
8
9
public interface Calculator {
    int increment(int i);
}
  
public class RealCalculator implements Calculator {
    public int increment(int x) {
        return x + 1;
    }
}

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

1
2
3
4
5
class FullBlownCalculator
    extends RealCalculator
       with Logging
       with Caching
       with Validating

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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$* методов. Вот недостающий фрагмент, который позволит нам соединить все точки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 вполне FullBlownCalculator .

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

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

01
02
03
04
05
06
07
08
09
10
11
12
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)

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