在 CUBA 中创建业务逻辑
开发 CUBA 应用程序时的首要问题之一是: 我的业务逻辑应该放到哪? 本指南将介绍几种选项和各自的优缺点。
将要构建的内容
本指南对 CUBA 宠物诊所 示例进行增强,以演示在 CUBA 应用程序中把业务逻辑写在哪。在本指南中展示了一个计算折扣的业务逻辑, 折扣主要根据宠物在诊所中的就诊次数来计算的。
开发环境要求
您的开发环境需要满足以下条件:
-
文件编辑器或者IDE (推荐使用 IntelliJ IDEA )
-
独立运行版或者IDEA插件版的 CUBA Studio (可选)
-
CUBA CLI (可选)
下载 并解压本指南的源码,或者使用 git 克隆下来:
示例: CUBA 宠物诊所
这个示例是以 CUBA 宠物诊所项目为基础,而这个项目的基础是众所周知的 Spring 宠物诊所项目。CUBA 宠物诊所应用程序涉及到了宠物诊所的领域模型及与管理一家宠物诊所相关的业务流程。
这个应用程序的领域模型如下:
主要的实体是 Pet 和 Visit。 Pet 到诊所就诊,就诊时(Vist) 会有一名兽医(Vet)负责照顾它。每个宠物都有主人,一个主人可以有多个宠物。一次就诊(Vist)即是一个宠物在主人的帮助下到诊所诊治的活动。
在控制器中的业务逻辑
假设当用户单击宠物浏览界面上的按钮时应执行折扣计算,最简单的方法是将计算逻辑放在界面对应的控制器类中。
查看诊所应用程序中的 Calculate discount 按钮和界面控制器实现 : 宠物浏览。
@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 可以被界面控制器访问,bean 必须位于应用程序的 global 、gui 或 web 模块中。如果 bean 位于 global 模块,也能被中间件访问。
该示例实现了多个客户端Bean,用于直接从宠物浏览或者宠物详情界面显示宠物主人的联系信息。
这个示例有两个 Bean:
-
PetContactFetcher 处理对应联系人数据的获取
-
PetContactDisplay 已通知的方式显示联系人信息
@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();
}
}
//...
}
@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,因此在中间件及客户端层都可以使用它。
中间件服务
关于业务逻辑存放位置的另一个选择是 中间件服务。这种服务是业务逻辑最合适的地方,因为它达到了以下目标:
-
业务逻辑可适用于所有类型的客户端,包括Polymer UI。
-
在业务逻辑中也可以使用只能用于中间件的API,比如事务等。
要从客户端调用中间件业务逻辑,需要创建服务。 CUBA Studio 简化了服务的创建过程:
-
在 CUBA 项目树中选择 Middleware 部分,在右键菜单中点击 New > Service。
-
将服务接口的名字修改为
DiseaseWarningMailingService
,Bean 类的名称和服务名称会做相应修改。 点击 OK 。 -
在编辑器中打印服务接口,创建一个方法并在服务类中实现它。
在诊所应用程序中,创建了下列服务:DiseaseWarningMailingService
。它发送病症警告邮件给受疾病威胁的宠物的主人。
-
DiseaseWarningMailingService - 服务接口
-
CreateDiseaseWarningMailing - 使用这个服务的界面控制器
DiseaseWarningMailingService
服务的实现如下:
@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
界面控制器如下所示:
@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。