Статьи

Anti-Pattern Grails: все является услугой

Контекст

Grails позволяет очень легко разместить любую логику вашего приложения в сервисе. Просто grails create-service и вы готовы идти. По умолчанию существует один экземпляр, который можно вводить в любом месте. Мощный материал и позволяет легко вставать и бегать очень быстро!

Создание нового приложения, следуя так называемым «лучшим практикам» из блогов, подобных этим, и «идиоматический путь Grails», описанный в документах и ​​учебных пособиях, работают в начале, но всегда есть переломный момент — когда приложение выросло разумного размера — где следует начинать следовать другой, может быть, менее Grailsey, стратегии.

Так что может пойти не так, создав сервисы в вашем приложении?

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

Команда действительно приняла этот совет близко к сердцу и начала централизовать свои запросы Альбомов в AlbumService , свои запросы Product в ProductService и так далее.

Вот что я видел, что происходит.

Спринт 1: Жизнь прекрасна

Эта команда начинала очень остро: они почти внедряли бизнес-логику в контроллеры, но могли вовремя внедрить ее в сервисы. Команда grails create-service даже сразу создаст пустой модульный тест — готовый к реализации.

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

Перемотка вперед 6 спринтов.

Спринт 6

Глядя на их код, кажется, что их папка services состоит из десятков классов:

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
27
28
29
30
31
32
33
grails-app/services/
└── example
    ├── AnotherProductService.groovy
    ├── ...
    ├── OrderService.groovy
    ├── OrderingService.groovy
    ├── ...
    ├── Product2Service.groovy
    ├── ProductAccountingService.groovy
    ├── ProductBuilderService.groovy
    ├── ProductCatalogService.groovy
    ├── ProductCreateService.groovy
    ├── ProductFinderService.groovy
    ├── ProductLineFileConverterDoodleService.groovy
    ├── ProductLineMakerService.groovy
    ├── ProductLineReaderService.groovy
    ├── ProductLineService.groovy
    ├── ProductLineTaglibHelperService.groovy
    ├── ProductLineUtilService.groovy
    ├── ProductManagementService.groovy
    ├── ProductManagerService.groovy
    ├── ProductMapperService.groovy
    ├── ProductOrderService.groovy
    ├── ProductReadService.groovy
    ├── ProductSaverService.groovy
    ├── ProductService.groovy
    ├── ProductTemplateOrderBuilderService.groovy
    ├── ProductUtilService.groovy
    ├── ProductWriterService.groovy
    ├── ProductsReadService.groovy
    ├── ProductsService.groovy
    └── ...
1 directory, 532 files

Шаблон

Это случалось со мной несколько раз. Я и команда ценим простоту и мощь Grails. Следовательно, довольно легко начать использовать команды Grails в полном объеме , такие как все команды create-* :

1
2
3
4
5
6
7
grails> create-
create-command                create-controller            
create-domain-class           create-functional-test       
create-integration-test       create-interceptor           
create-scaffold-controller    create-script                
create-service                create-taglib                
create-unit-test

Во многих проектах Grails, аналогичных вымышленному & # 55357; & # 56898; выше, команда create-service была чрезмерно использована , потому что она кажется идиоматическим способом создания «бизнес-логики на уровне сервиса».

Да, эта команда создает хороший пустой тестовый модуль и автоматически представляет собой удобный bean-компонент Spring, вставляемый в контроллеры, библиотеки тегов и другие артефакты Grails.

Да, использование *Service хорошо работает в блогах, демонстрациях и обучающих программах.

Тем не менее, кажется, что мы забыли, что в основном все является «службой» для кого-то другого, но нам не обязательно добавлять постфикс («Служба») к каждому классу как таковому .

Кажется, что люди обычно понимают, когда что-то должно быть контроллером («давайте сделаем create-controller ») или библиотекой тегов («давайте сделаем create-taglib ») и так далее, и для всего остального: boom! «Давайте create-service ».

В любом другом проекте , не связанном с Grails, мы привыкли называть конструктор просто «PersonBuilder», в проектах Grails это вдруг «PersonBuilderService». В любом другом проекте фабрика — это «PersonFactory», в проекте Grails — странная «PersonFactoryService».

Что если «PersonReadService» отвечает за поиск или поиск людей? В течение многих лет люди использовали шаблон Repository для этого, и это может быть отражено с помощью постфикса «Repository», например «PersonRepository».

Даже в Grails строителем может быть Строитель , фабрика, фабрика , маппер, маппер, репозиторий, репозиторий , каракули, каракули и все, что может закончиться чем угодно — вы можете назвать каждый класс так, как хотите.

Что мы можем с этим поделать?

Хватит называть все Сервисом

Во-первых, в следующий раз, когда вы собираетесь создать класс, следуя одному из известных шаблонов проектирования, например, Builder, Factory, Strategy, Template, Adapter, Decorator (хороший обзор см. На sourcemaking.com ) или другим «хорошо известным» Шаблоны Java (EE), такие как Producer, Mapper или что-то, задают себе вопрос:

Это может быть обычный класс в src/main/groovy ?

Переместить и выбрать лучшее имя

  • Да, тогда просто создайте класс в src/main/groovy . Может быть, выбрать хороший пакет, например, example . Если вам не нужно 532 класса в одном пакете, вы всегда можете ввести подпакеты, такие как example.accounting . Дайте ему имя, которое не заканчивается на *Service . Не забудьте вручную добавить связанный *Spec в src\test\groovy .

Вы все еще хотите получить выгоду от Spring и Dependency Injection?

Другими словами, нужен ли вам экземпляр вашего класса, чтобы можно было внедрить его в какие-либо классы Grails, такие как контроллер, как вы привыкли к сервису, как, например, ProductReadService ниже?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// grails-app/controllers/example/HomepageController.groovy
class HomepageController {
    ProductReadService productReadService
 
    def index() { ... }
}
 
// grails-app/services/example/ProductReadService.groovy
class ProductReadService {
    SecurityService securityService
 
    Product findByName(String name) {
        assert securityService.isLoggedIn()
        Product.findByName(name)
    }
}

Базовый контейнер создается Spring Framework.

  • В документах есть отличная глава о Граалях и Весне . Именно эта инфраструктура будет создавать, например, один SecurityService в приложении, внедрять его в свойство «securityService», когда он создает один экземпляр ProductReadService который он внедряет в HomepageController и так далее.
  • SecurityService в этом примере — который может исходить от плагина Security и классов *Service в ваших собственных источниках приложений — все они автоматически выбираются и управляются контейнером Spring и внедряются в любой другой управляемый класс, который в этом нуждается.
  • Это не столько перемещение grails-app/services/example в папку src/main/groovy/example , сколько переименование класса во что-то, что больше не заканчивается в «Service» , — тогда вы теряете автоматическое управление к весне. Это происходит, когда мы, как и предполагалось, после перемещения переименовываем класс ProductReadService в класс ProductRepository .

Да, он хочет, чтобы они были бобом Spring!

Объявление весенних бобов способом Грааля

Конечно, мы можем сделать это сами. Идейным способом Grails является указание bean-компонентов в resources.groovy .

1
2
3
4
5
6
7
// grails-app/conf/spring/resources.groovy
import example.*
beans = {
    productRepository(ProductRepository) {
        securityService = ref('securityService')
    }
}

Мы определили bean-компонент с именем «productRepository» класса ProductRepository и указали, что SecurityService необходимо ProductRepository .

Так изменился исходный код, но поведение не изменилось: теперь вместо него используется ProductRepository .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// grails-app/controllers/example/HomepageController.groovy
class HomepageController {
    ProductRepository productRepository
 
    def index() { ... }
}
 
// src/main/groovy/example/ProductRepository.groovy
class ProductRepository {
    SecurityService securityService
 
    Product findByName(String name) {
        assert securityService.isLoggedIn()
        Person.findByName(name)
    }
}

Это не единственный способ объявить Spring bean .

Объявление Весенние бобы Весенний путь

Мы объявили Spring bean как Grails, но мы также можем объявить bean как Spring.

Хорошо, есть не просто «весенний путь», есть много способов, от старых объявлений XML, сканирования путей к классам до конфигурации в стиле Java.

Наличие (подмножество) ваших 532 классов в resources.groovy может рассматриваться не так уж и лучше, чем конфигурация XML, используемая Spring в первые дни.

Даже через Beans DSL здесь намного мощнее, чем когда-либо был XML (потому что: Groovy), мы не переводим наши автоматически собранные служебные бины для возврата ручного труда, по моему мнению.

Вот как это будет выглядеть:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
beans = {
    anotherProductRepository(AnotherProductRepository) {
        securityService = ref('securityService')
        orderingService = ref('orderingService')
    }
    productLineReader(ProductLineReader)
    productFinder(ProductFinder) {
        productRepository = ref('productRepository')
        productLineService = ref('productLineService')
    }
    productRepository(ProductRepository) {
        securityService = ref('securityService')
        productReader = ref('productReader')
        productWriter = ref('productWriter')
    }
    orderingService(OrderingService) {
        securityService = ref('securityService')
        productRepository = ref('productRepository')
    }
    ...

Есть случаи, когда resources.groovy прекрасно работает, но почему бы не избавиться от котельной и не использовать современные возможности Spring, которые есть в нашем распоряжении?

Попробуйте вместо этого компонентное сканирование .

  • Только один раз установите аннотацию Spring @ComponentScan нашего класса Application.groovy
01
02
03
04
05
06
07
08
09
10
11
12
13
// grails-app/init/example/Application.groovy
package example
 
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import org.springframework.context.annotation.ComponentScan
 
@ComponentScan
class Application extends GrailsAutoConfiguration {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }
}

Это заставляет Spring при запуске приложения сканировать все компоненты на пути к классам в пакете «example» и регистрировать их как Spring bean-компоненты. Или укажите @ComponentScan("example") чтобы быть более явным.

Каковы эти «компоненты» вы говорите? Все классы @Component аннотацией Spring стереотипа @Component . Или @Service или @Repository которые являются просто специализациями.

  • Аннотируйте наши классы, чтобы сделать их кандидатами на автоопределение
1
2
3
4
5
6
7
8
9
import org.springframework.stereotype.Component
 
@Component
// or @Repository in this particular case
class ProductRepository {
    SecurityService securityService
 
    Product findByName(String name) { .. }
}
  • В тот момент, когда мы перезапускаем наше приложение, мы получим NullPointerException когда попытаемся вызвать что-либо на securityService .Spring больше не признает, что он должен что-то делать со свойством — теперь это просто свойство. Чтобы @Autowired SecurityService , нам нужно аннотировать свойство с помощью @Autowired , но это в основном то же самое, что инъекция сеттера, которую мы уже @Autowired в начале. А @Autowired — это плита, которая нам не нужна.

Пока мы на этом, я рекомендую использовать конструктор-инъекцию , что означает, что мы создаем (или позволяем IDE создавать) конструктор.
* Мы делаем зависимости ProductRepository явными .
* Spring автоматически «связывает» наш конструктор, если он у нас ровно один, и вводит все параметры конструктора

1
2
3
4
5
6
7
8
9
import org.springframework.stereotype.Component
 
@Component
class ProductRepository {
    final SecurityService securityService
 
    ProductRepository(SecurityService securityService) {
        this.securityService = securityService
    }

Это оно.

Кстати, наличие явного конструктора со всеми обязательными зависимостями — это всегда хорошая практика — будь то бин Spring или нет.

  • Наконец, верните resources.groovy в его исходное пустое состояние — мы его больше не используем.

Наименование важно

Теперь, если бы мы сделали это с оригинальными 532 классами, мы могли бы получить более приятное дерево файлов. Например

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
grails-app/services/
└── example
    ├── OrderService.groovy
    ├── ProductService.groovy
    └── SecurityService.groovy
src/main/groovy/
└── example
    ├── order
    │   ├── OrderBuilder.groovy
    │   └── OrderRepository.groovy
    └── product
        ├── ProductBuilder.groovy
        ├── ProductFinder.groovy
        ├── ProductLineReader.groovy
        ├── ProductLineTaglibHelper.groovy
        ├── ProductMapper.groovy
        ├── ProductRepository.groovy
        ├── ProductUtils.groovy
        └── ProductWriter.groovy

Некоторые фактические *Service классы *Service cal по-прежнему находятся в grails-app/services а остальные могут стать классами с четкими именами, аккуратно помещенными в src/main/groovy , в то время как вы все еще наслаждаетесь преимуществом их использования в качестве бинов Spring.

Если вы и команда на ранних этапах процесса примете решение о надлежащих соглашениях об именах (пакеты, префиксы классов и т. Д.), Вам не нужно переупорядочивать все, как мы это делали сейчас. Вместе с командой создайте и назовите ваши занятия в организованном месте.