«1. Обзор

В этом руководстве мы рассмотрим концепцию трейтов в Groovy. Они были представлены в выпуске Groovy 2.3.

2. Что такое черты?

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

По этой причине они рассматриваются как интерфейсы, несущие как реализации по умолчанию, так и состояние. Все черты определяются с помощью ключевого слова trait.

3. Методы

Объявление метода в трейте аналогично объявлению любого обычного метода в классе. Однако мы не можем объявлять защищенные или закрытые для пакета методы в трейте.

Давайте посмотрим, как реализованы публичные и приватные методы.

3.1. Публичные методы

Для начала мы рассмотрим, как общедоступные методы реализованы в трейте.

Давайте создадим трейт с именем UserTrait и публичный метод sayHello:

trait UserTrait {
    String sayHello() {
        return "Hello!"
    }
}

После этого мы создадим класс Employee, который реализует UserTrait:

class Employee implements UserTrait {}

Теперь давайте создадим тест для проверки что экземпляр Employee может получить доступ к методу sayHello атрибута UserTrait:

def 'Should return msg string when using Employee.sayHello method provided by User trait' () {
    when:
        def msg = employee.sayHello()
    then:
        msg
        msg instanceof String
        assert msg == "Hello!"
}

3.2. Приватные методы

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

Давайте посмотрим на реализацию кода в UserTrait:

private String greetingMessage() {
    return 'Hello, from a private method!'
}
    
String greet() {
    def msg = greetingMessage()
    println msg
    return msg
}

Обратите внимание, что если мы обращаемся к приватному методу в классе реализации, он сгенерирует исключение MissingMethodException:

def 'Should return MissingMethodException when using Employee.greetingMessage method' () {
    when:
        def exception
        try {
            employee.greetingMessage()
        } catch(Exception e) {
            exception = e
        }
        
    then:
        exception
        exception instanceof groovy.lang.MissingMethodException
        assert exception.message == "No signature of method: com.baeldung.traits.Employee.greetingMessage()"
          + " is applicable for argument types: () values: []"
}

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

3.3. Абстрактные методы

Трейт также может содержать абстрактные методы, которые затем могут быть реализованы в другом классе:

trait UserTrait {
    abstract String name()
    
    String showName() {
       return "Hello, ${name()}!"
    }
}
class Employee implements UserTrait {
    String name() {
        return 'Bob'
    }
}

3.4. Переопределение методов по умолчанию

trait SpeakingTrait {
    String speak() {
        return "Speaking!!"
    }
}
class Dog implements SpeakingTrait {
    String speak() {
        return "Bow Bow!!"
    }
}

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

Трейты не поддерживают защищенные и частные области видимости.

4. Ключевое слово this

trait UserTrait {
    def self() {
        return this 
    }
}

Поведение ключевого слова this похоже на поведение в Java. Мы можем рассматривать черту как суперкласс.

Например, мы создадим метод, который возвращает это в трейте:

5. Интерфейсы

interface Human {
    String lastName()
}
trait UserTrait implements Human {
    String showLastName() {
        return "Hello, ${lastName()}!"
    }
}

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

class Employee implements UserTrait {
    String lastName() {
        return "Marley"
    }
}

Давайте создадим интерфейс и реализуем его в трейте:

trait UserTrait implements Human { 
    String email
    String address
}

Теперь давайте реализуем абстрактный метод интерфейса в классе реализации:

6. Свойства

trait WheelTrait {
    int noOfWheels
}

trait VehicleTrait extends WheelTrait {
    String showWheels() {
        return "Num of Wheels $noOfWheels" 
    } 
}

class Car implements VehicleTrait {}

Мы можем добавлять свойства к трейту так же, как и к любому обычному классу:

trait AddressTrait {                                      
    String residentialAddress
}

trait EmailTrait {                                    
    String email
}

trait Person implements AddressTrait, EmailTrait {}

7. Расширение трейтов

Подобно обычному классу Groovy, трейт может расширять другой трейт с помощью ключевого слова extends:

Мы также можем расширять несколько трейтов с помощью предложения items:

8. Конфликты множественного наследования

Когда класс реализует два или более трейтов, имеющих методы с одинаковой сигнатурой, нам нужно знать, как разрешить конфликты. Давайте посмотрим, как Groovy разрешает такие конфликты по умолчанию, а также как мы можем переопределить разрешение по умолчанию.

8.1. Разрешение конфликтов по умолчанию

trait WalkingTrait {
    String basicAbility() {
        return "Walking!!"
    }
}

trait SpeakingTrait {
    String basicAbility() {
        return "Speaking!!"
    }
}

По умолчанию будет выбран метод из последнего объявленного трейта в предложении Implements.

class Dog implements WalkingTrait, SpeakingTrait {}

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

Во-первых, давайте создадим два трейта с методом, имеющим одинаковую сигнатуру:

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

Поскольку SpeakingTrait объявляется последним, его реализация метода basicAbility будет выбран по умолчанию в классе Dog.

String speakAndWalk() {
    return "Walk and speak!!"
}
String speakAndWalk() {
    return "Speak and walk!!"
}

8.2. Явное разрешение конфликтов

class Dog implements WalkingTrait, SpeakingTrait {
    String speakAndWalk() {
        WalkingTrait.super.speakAndWalk()
    }
}

Теперь, если мы не хотим просто использовать разрешение конфликтов по умолчанию, предоставляемое языком, мы можем переопределить его, явно выбрав метод для вызова, используя ссылку trait.super.method.

Например, давайте добавим еще один метод с той же сигнатурой к нашим двум типам:

trait AnimalTrait {
    String basicBehavior() {
        return "Animalistic!!"
    }
}

Теперь давайте переопределим разрешение конфликтов множественного наследования по умолчанию в нашем классе Dog с помощью ключевого слова super: ~~ ~

def dog = new Dog()
def dogWithTrait = dog.withTraits SpeakingTrait, WalkingTrait, AnimalTrait

9. Реализация трейтов во время выполнения

«Чтобы динамически реализовать трейт, мы можем использовать ключевое слово as, чтобы привести объект к трейту во время выполнения.

Например, давайте создадим AnimalTrait с помощью метода basicBehavior:

Чтобы реализовать сразу несколько признаков, мы можем использовать метод withTraits вместо ключевого слова as: