Create Business Logic in CUBA

开发 CUBA 应用程序时的首要问题之一是: 我的业务逻辑应该放到哪? 本指南将介绍几种选项和各自的优缺点。

将要构建的内容

本指南对 CUBA 宠物诊所 示例进行增强,以演示在 CUBA 应用程序中把业务逻辑写在哪。在本指南中展示了一个计算折扣的业务逻辑, 折扣主要根据宠物在诊所中的到访次数来计算的。

开发环境要求

你的开发环境需要满足以下条件:

下载 并解压本指南的源码,或者使用 git 克隆下来:

示例: CUBA 宠物诊所

这个示例是以 CUBA 宠物诊所项目为基础,而这个项目的基础是众所周知的 Spring 宠物诊所项目。CUBA 宠物诊所应用程序涉及到了宠物诊所的领域模型及与管理一家宠物诊所相关的业务流程。

这个应用程序的领域模型看起来像这样:

领域模型

主要实体是 PetVisit。 一个 Pet 到一家诊所就诊,在诊疗过程(Vist) 会有一名兽医(Vet)负责照顾它。每个宠物都有主人,一个主人可以有多个宠物。一个诊疗过程(Vist)即是一个宠物在主人的帮助下到诊所就诊的活动。

在控制器中的业务逻辑

假设当用户单击宠物浏览器界面上的按钮时应执行折扣计算,最简单的方法是将计算逻辑放在界面对应的控制器类中。

查看诊所应用程序中的 Calculate discount 按钮和界面控制器实现 : 宠物浏览

PetBrowse.java
@UiController("petclinic_Pet.browse")
@UiDescriptor("pet-browse.xml")
@LookupComponent("petsTable")
@LoadDataBeforeShow
public class PetBrowse extends StandardLookup<Pet> {

    @Inject
    private Notifications notifications;

    @Inject
    private Metadata metadata;

    @Inject
    private GroupTable<Pet> petsTable;

    @Subscribe("petsTable.calculateDiscount")
    public void calculateDiscount(Action.ActionPerformedEvent actionPerformedEvent) {

        Pet pet = petsTable.getSingleSelected();

        int discount = calculateDiscount(pet);

        showDiscountCalculatedNotification(pet, discount);
    }

    private int calculateDiscount(Pet pet) {
        int discount = 0;

        int visitAmount = pet.getVisits().size();
        if (visitAmount > 10) {
            discount = 10;
        } else if (visitAmount > 5) {
            discount = 5;
        }
        return discount;
    }

    private void showDiscountCalculatedNotification(Pet pet, int discount) {

        String petName = metadata.getTools().getInstanceName(pet);

        String discountMessage = "Discount for " + petName + ": " + discount + "%";

        notifications.create()
                .setCaption(discountMessage)
                .setType(Notifications.NotificationType.TRAY)
                .show();
    }
}
如果只有一地方会使用这个计算逻辑,并且这个逻辑很简单,不需要分解为几个 小的方法,那么这种将计算逻辑直接放到控制器的做法是可以接受的。

客户端层的 Bean

如果业务逻辑只需要在一个界面内使用,那么把它放到界面控制器中是可行的。 如果业务逻辑要在多个界面中使用,最好不要简单地将代码复制到其它界面。 我们可以将逻辑提取到所有界面控制器都能访问的地方,达到这个目的方法就是在客户端层使用 Sprig Bean。

托管 bean 是一个使用了 @Component 注解的类。 它可以通过 @Inject 注入其他 bean 和界面控制器。如果 bean 具有独立的接口,则可以通过接口而不是类来访问它。

使用客户端层Bean的类图

Please note that in order to be accessible for screen controllers, the bean must be located in global, gui or web modules of the application. In the former case the bean will be also accessible for the middleware.

The example that is implemented as (multiple) client tier beans is to display the contact information of the pet’s owner directly from the pet browser or the pet details screen.

请注意,要使 Bean 可以被界面控制器访问,bean 必须位于应用程序的 globalguiweb 模块中。如果 bean 位于 global 模块,也能被中间件访问。

该示例实现了多个客户端Bean,用于直接从宠物浏览或者宠物详情界面显示宠物主人的联系信息。

这个示例有两个 Bean:

PetContactFetcherBean.java
@Component(PetContactFetcher.NAME)
public class PetContactFetcherBean implements PetContactFetcher {

    @Inject
    private DataManager dataManager;

    @Inject
    private Messages messages;

    @Override
    public Optional<Contact> findContact(Pet pet) {

        Optional<Owner> petOwner = loadOwnerFor(pet);

        if (petOwner.isPresent()) {

            Owner owner = petOwner.get();
            String telephone = owner.getTelephone();
            String email = owner.getEmail();
            String address = formatOwnerAddress(owner);

            if (isAvailable(telephone)) {
                return createContact(telephone, ContactType.TELEPHONE);
            } else if (isAvailable(email)) {
                return createContact(email, ContactType.EMAIL);
            } else if (isAvailable(address)) {
                return createContact(address, ContactType.ADDRESS);
            } else {
                return Optional.empty();
            }
        } else {
            return Optional.empty();
        }
    }

    //...
}

这两个bean 用在了 宠物浏览界面宠物详情界面。 使用方法如下:

PetEdit.java
@UiController("petclinic_Pet.edit")
@UiDescriptor("pet-edit.xml")
@EditedEntityContainer("petCt")
@LoadDataBeforeShow
public class PetEdit extends StandardEditor<Pet> {

    @Inject
    private PetContactFetcher petContactFetcher;

    @Inject
    private PetContactDisplay petContactDisplay;

    @Subscribe("fetchContact")
    public void fetchContact() {

        Pet pet = getEditedEntity();

        Optional<Contact> contactInformation = petContactFetcher.findContact(pet);

        petContactDisplay.displayContact(contactInformation, this);
    }
}

与第一个将业务逻辑放在控制器内的方法相比,将业务逻辑放在独立的 bean 这种方法的优势在于代码重用性更高,可以在不同的地方使用相同的业务逻辑。

PetContactDisplay 是一个 web 模块的 bean,因此它只能在客户端层使用。 PetContactFetcher, 换句话说 ,是一个在 global 模块的 bean,因此在中间件及客户端层都可以使用它。

中间件服务

关于业务逻辑存放位置的下一个方法是 https://doc.cuba-platform.cn/manual-7.0-chs/services.html [中间件服务]。这种服务是业务逻辑最合适的地方,因为它达到了以下目标:

  • 业务逻辑将适用于所有类型的客户端,包括Polymer UI。

  • 在业务逻辑中也可以使用只能用于中间件的API,比如事务等。

使用服务的类图

要从客户端调用中间件业务逻辑,需要创建服务。 CUBA Studio 简化了服务的创建过程:

  • 在 CUBA 项目树中选择 Middleware 部分,在右键菜单中点击 New > Service

  • 将服务接口的名字修改为 DiseaseWarningMailingService,Bean 类的名称和服务名称会做相应修改。 点击 OK

  • 在编辑器中打印服务接口,创建一个方法并在服务类中实现它。

在诊所应用程序中,创建了下列服务: DiseaseWarningMailingService。它发送病症警告邮件给受疾病威胁的宠物的主人。

DiseaseWarningMailingServiceBean.java
@Service(DiseaseWarningMailingService.NAME) (1)
public class DiseaseWarningMailingServiceBean implements DiseaseWarningMailingService {

    @Inject
    private DataManager dataManager;

    @Inject
    private EmailerAPI emailerAPI;

    @Override
    public int warnAboutDisease(PetType petType, String disease, String city) { (2)

        List<Pet> petsInDiseaseCity = findPetsInDiseaseCity(petType, city);

        List<Pet> petsWithEmail = filterPetsWithValidOwnersEmail(petsInDiseaseCity);

        petsWithEmail.forEach(pet -> sendEmailToPetsOwner(pet, disease, city));

        return petsWithEmail.size();
    }

    // ...

}
1 DiseaseWarningMailingServiceBean 作为中间件服务实现的声明
2 业务逻辑接口的实现

UI 控制器的使用与客户端层bean的调用非常相似。CreateDiseaseWarningMailing 界面控制器如下所示:

CreateDiseaseWarningMailing.java
@UiController
@UiDescriptor("create-disease-warning-mailing.xml")
public class CreateDiseaseWarningMailing extends Screen {

    @Inject
    private DiseaseWarningMailingService diseaseWarningMailingService; (1)

    @Inject
    private Notifications notifications;

    @Subscribe("createDiseaseWarningMailing")
    protected void createDiseaseWarningMailing(Action.ActionPerformedEvent event) {

        int endangeredPets = diseaseWarningMailingService.warnAboutDisease(petType.getValue(),
                disease.getValue(), city.getValue()); (2)

        closeWithCommit().then(() ->
                notifications.create()
                        .setCaption(endangeredPets + " Owner(s) of endangered Pets have been notified")
                        .setType(Notifications.NotificationType.TRAY)
                        .show()
        );
    }
}
1 通过接口定义注入中间层服务
2 在控制器操作(action)中的业务逻辑调用

有了这个中间件服务,现在可以在 Web 层的 UI 界面上共享创建疾病警告邮件的逻辑。 另外,任何其他中间件代码都可以调用该逻辑,因为它不再依赖于Web层。 此外,可以通过 REST API触发这个逻辑,同时也可以将这些 API 暴露给其它技术 ,如 Polymer UI。

总结

CUBA 应用程序中涉及到业务逻辑的放置位置时,有几种选择。 控制器是一个不错且简单开端,虽然它在共享代码时有一些缺点。 客户端 bean 在一定程度上解决了这个问题。 中间件服务允许跨不同客户端(例如,Polymer Client)共享业务逻辑,这种方式也是放置业务逻辑的最佳方式。

另外,有几种高级技术,比如 JMX Beans实体监听器 也允许创建在特定场景下要执行的业务逻辑。对于特定场景,在什么位置放置业务逻辑应视具体情况而定。但是前面提到的的几个主要位置:界面控制器、客户端Spring Bean及 中间件服务是一个不错的起点。