Статьи

Внедрение и взаимодействие Scala Traits. Часть I: Основы

 Черты в Scala похожи на интерфейсы, но гораздо более мощные. Они допускают реализации некоторых методов, полей, стеков и т. Д. Но вы когда-нибудь задумывались, как они реализованы поверх JVM? Как можно расширить несколько черт и куда идут реализации? В этой статье, основываясь на моем ответе на StackOverflow , я приведу несколько примеров характеристик и объясню, как scalacих реализовать, каковы недостатки и что вы получаете бесплатно. Часто мы будем смотреть на скомпилированные классы и декомпилировать их в Java. Это не обязательное знание, но я надеюсь, что оно вам понравится. Все примеры скомпилированы для Scala 2.10.1.

Простая черта без реализации методов

Следующая черта:

trait Solver {
    def answer(question: String): Int
}

компилируется до самого скучного интерфейса Java:

public interface Solver {
    int answer(String);
}

В чертах Scala нет ничего особенного. Это также означает, что если вы отправляете
Solver.classкак часть своей библиотеки, пользователи могут безопасно реализовать такой интерфейс из кода Java. Что касается
java/
javac, то это обычный Java-скомпилированный интерфейс.

Черты, имеющие некоторые реализованные методы

Хорошо, но что, если у черты на самом деле есть несколько тел методов?

trait Solver {
 
    def answer(s: String): Int
 
    def ultimateAnswer = answer("Answer to the Ultimate Question of Life, the Universe, and Everything")
 
}

Здесь
ultimateAnswerметод на самом деле имеет некоторую реализацию (тот факт, что он вызывает абстрактный
answer()метод не имеет значения), но
answer()остается нереализованным. Что будет
scalacпроизводить?

public interface Solver {
    int answer(java.lang.String);
    int ultimateAnswer();
}

Ну, это все еще интерфейс, и реализация ушла. Если реализация ушла, что произойдет, когда мы расширим такую ​​черту?

class DummySolver extends Solver {
 
    override def answer(s: String) = 42
 
}

Мы должны реализовать,
answer()но
ultimateAnswerуже доступны через
Solver. Страшно посмотреть, как это выглядит под капотом?
DummySolver.class:

public class DummySolver implements Solver {
 
    public DummySolver() {
        Solver$class.$init$(this);
    }
 
    public int ultimateAnswer() {
        return Solver$class.ultimateAnswer(this);
    }
 
    public int answer(String s) {
        return 42;
    }
 
}

Это … странно … Видимо, мы пропустили один новый файл класса
Solver$class.class. Да, класс назван
Solver$class, это действительно даже в Java. Это
не
Solver.class выражение, которое возвращает
Class[Solver], это, очевидно, обычный Java-класс, названный
Solver$classс помощью множества статических методов. Вот как это выглядит:

public abstract class Solver$class {
 
    public static int ultimateAnswer(Solver $this) {
        return $this.answer("Answer to the Ultimate Question of Life, the Universe, and Everything");
    }
 
    public static void $init$(Solver solver) {}
}

Ты видишь трюк? Scala создал помощника,
Solver$classи методы, которые реализованы в trait, фактически помещаются в этот скрытый класс. Кстати, это не объект-компаньон, это просто вспомогательный класс, невидимый для вас. Но настоящий трюк — это
$thisпараметр, вызываемый как
Solver$class.ultimateAnswer(this). Поскольку технически мы находимся в другом объекте, мы должны каким-то образом получить дескриптор
реального
Solver экземпляра. Это как ООП, но сделано вручную.

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

Расширение нескольких черт с реализациями

Представьте себе расширение нескольких признаков, каждый из которых реализует отдельный набор методов:

trait Foo {
    def foo = "Foo"
}
 
trait Bar {
    def bar = "Bar"
}
 
class Buzz extends Foo with Bar

По аналогии вы, наверное, уже знаете, как
Foo.classи
Bar.classвыглядите так:

public interface Foo {
    String foo();
}
 
public interface Bar {
    String bar();
}

Реализации скрыты в таинственных
...$class.classфайлах, как и прежде:

public abstract class Foo$class {
    public static String foo(Foo) {
        return "Foo";
    }
 
    public static void $init$(Foo) {}
}
 
public abstract class Bar$class {
    public static String bar(Bar) {
        return "Bar";
    }
 
    public static void $init$(Bar) {}
}

Обратите внимание , что статические методы в
Foo$classпринимать экземпляры
в
Fooто время как
Bar$classтребуется ссылка
Bar. Это работает, потому что
Buzzреализует
Fooи
Bar:

public class Buzz implements Foo, Bar {
 
    public Buzz() {
        Foo.class.$init$(this);
        Bar.class.$init$(this);
    }
 
    public String bar() {
        return Bar$class.bar(this);
    }
 
    public String foo() {
        return Foo$class.foo(this);
    }
 
}

И то, foo()и
другое
bar()передают
this, но это нормально.

Черты с полями

Поля — еще одна интересная особенность черт. На этот раз давайте воспользуемся реальным примером из
проекта
Spring Data . Как вы знаете, интерфейсы могут требовать реализации определенных методов. Однако они не могут заставить вас предоставить определенные поля (или предоставить поля самостоятельно). Это ограничение становится болезненным при работе с
Auditable<U, ID>расширением интерфейса
Persistable<ID>. Хотя эти интерфейсы просто существуют , чтобы убедиться , что некоторые поля присутствуют в вашем
@Entityклассе, а именно
createdBy,
createdDate,
lastModifiedBy,
lastModifiedDateи
idэто не может быть выражено чисто. Вместо этого вы должны реализовать следующие методы в каждом расширении сущности
Auditable:

       U getCreatedBy();
    void setCreatedBy(final U createdBy);
DateTime getCreatedDate();
    void setCreatedDate(final DateTime creationDate);
       U getLastModifiedBy();
    void setLastModifiedBy(final U lastModifiedBy);
DateTime getLastModifiedDate();
    void setLastModifiedDate(final DateTime lastModifiedDate);
 
      ID getId();
 boolean isNew();

Более того, каждый отдельный класс, конечно, должен определять поля, выделенные выше. Не чувствует
СУХОЙ вообще. К счастью, черты могут нам сильно помочь
. В признаке мы можем определить, какие поля должны быть созданы в каждом классе, расширяющем эту особенность:

trait IAmAuditable[ID <: java.io.Serializable] extends Auditable[User, ID] {
 
    var createdBy: User = _
 
    def getCreatedBy = createdBy
 
    def setCreatedBy(createdBy: User) {
        this.createdBy = createdBy
    }
 
    var createdDate: DateTime = _
 
    def getCreatedDate = createdDate
 
    def setCreatedDate(creationDate: DateTime) {
        this.createdDate = creationDate
    }
 
    var lastModifiedBy: User = _
 
    def getLastModifiedBy = lastModifiedBy
 
    def setLastModifiedBy(lastModifiedBy: User) {
        this.lastModifiedBy = lastModifiedBy
    }
 
    var lastModifiedDate: DateTime = _
 
    def getLastModifiedDate = lastModifiedDate
 
    def setLastModifiedDate(lastModifiedDate: DateTime) {
        this.lastModifiedDate = lastModifiedDate
    }
 
    var id: ID = _
 
    def getId = id
 
    def isNew = id == null
}

Но подождите, в Scala есть встроенная поддержка геттеров / сеттеров в стиле POJO! Таким образом, мы можем сократить это до:

class IAmAuditable[ID <: java.io.Serializable] extends Auditable[User, ID] {
 
    @BeanProperty var createdBy: User = _
 
    @BeanProperty var createdDate: DateTime = _
 
    @BeanProperty var lastModifiedBy: User = _
 
    @BeanProperty var lastModifiedDate: DateTime = _
 
    @BeanProperty var id: ID = _
 
    def isNew = id == null
}

Генерируемые компилятором методы получения и установки автоматически реализуют интерфейс. Отныне каждый класс, желающий предоставить возможности аудита, может расширить эту черту:

@Entity
class Person extends IAmAuditable[String] {
 
    //...
 
}

Все поля, геттеры и сеттеры есть. Но как это реализовано? Давайте посмотрим на сгенерированный
Person.class:

public class Person implements IAmAuditable<java.lang.String> {
    private User createdBy;
    private DateTime createdDate;
    private User lastModifiedBy;
    private DateTime lastModifiedDate;
    private java.io.Serializable id;
 
    public User createdBy() //...
    public void createdBy_$eq(User) //...
    public User getCreatedBy() //...
    public void setCreatedBy(User) //...
 
    public DateTime createdDate() //...
    public void createdDate_$eq(DateTime) //...
    public DateTime getCreatedDate() //...
    public void setCreatedDate(DateTime) //...
 
    public boolean isNew();
 
    //...
}

На самом деле есть гораздо больше, но вы поняли идею. Таким образом, поля не реорганизованы в отдельный класс, но этого следовало ожидать. Вместо этого поля копируются в каждый отдельный класс, расширяющий эту особенность. В Java мы могли бы использовать абстрактный базовый класс для этого, но было бы неплохо зарезервировать наследование для реальных отношений
is-a и не использовать его для держателей фиктивных полей. В Scala есть такой признак, который объединяет общие поля, которые мы можем использовать в нескольких классах.

А как насчет совместимости Java? Ну,
IAmAuditableкомпилируется до интерфейса Java, поэтому у него вообще нет полей. Если вы реализуете это из Java, вы ничего особенного не получите.

Мы рассмотрели основные варианты использования черт в Scala и их реализацию. В следующей статье мы рассмотрим, как работают миксины и составные модификации.