Tidy Spring - Services






4.82/5 (3投票s)
一份关于如何在 Spring 应用程序中高效工作的实践列表。本部分侧重于服务。
Spring 已成为 Java 开发人员中非常受欢迎的选择。毕竟,它是一个拥有海量有用功能的出色项目。我决定与您分享我用于在 Spring 应用程序中高效工作、避免不必要的框架耦合并实现整洁应用程序架构的实践列表。一套完整的实践内容在一篇文章中篇幅过长,因此我将其分成了几个小部分。在这篇文章中,我将重点介绍服务。
什么不是(好的)服务?
如果您读过我其他的文章,您可能会注意到我主要针对的是许多应用程序中存在的糟糕实践。这篇也不例外。在 Spring 应用程序中出现像这样的类并不少见
@Service
public class MyObjectService {
@Autowired
private MyObjectRepository myObjectRepository;
public MyObject findOne(Long id) {
return myObjectRepository.findOne(id);
}
public List<MyObject> findAll() {
return myObjectRepository.findAll();
}
public MyObject save(MyObject myObject) {
return myObjectRepository.save(myObject);
}
public void delete(MyObject myObject) {
myObjectRepository.delete(myObject);
}
}
然后,我们可以想到的与 MyObject
相关的所有其他方法都进入了这个服务。当然,每种业务对象类型都有自己的服务。一旦应用程序的增长超出了基本 CRUD,人们就会尝试利用他们“好用”的服务,并将它们混杂在一起,形成意大利面条式代码。
我可以写很多关于为什么这是个坏主意的文章,但我会把这个留给您,而是专注于正确的方法。
什么是(好的)服务?
服务是一个类,代表一个单一的应用程序用例或其中的一部分。因此,我们说它包含应用程序特定的业务规则(可以参考《整洁架构》中的 Interactors)。在大多数情况下,它会创建并协调其他对象以满足用例的要求。服务类属于应用程序的“业务部分”。这意味着它们不包含 Spring 依赖(但仍然是 Spring 应用程序中非常重要的一部分)。
如何命名服务?
当然,用用例的名称来命名!如果您正在实现网上商店的下单流程,那么就将其命名为 PlaceOrder
或 OrderPlacement
。任何描述用例背后过程的名称,并且仅限于此!
有一篇关于在 Ruby 中命名服务对象的很棒的 gist,同样适用于 Spring 服务,在此处(认真阅读!)。
此外,还有一些词我们想要避免,比如“Service
”或“Manager
”。这些词没有增加任何价值,而且像磁铁一样吸引问题。想象一下,如果我们给下单服务命名为 OrderService
,会发生什么?您认为取消订单会放在哪里?查看订单详情又会放在哪里?OrderService
或 OrderManager
告诉类的唯一信息是它包含与 order
相关的*某些*内容。名称必须具有描述性和精确性。
输入和输出
由于服务靠近“业务部分”的边界,因此它们应该使用简单的数据结构作为输入和输出。一个 String
的映射、一个带有 public
字段的类或带有访问器的 private
字段可能有效。原因是我们要将所有业务规则都封闭在业务组件内部。因此,领域对象不应该泄露到外部。服务负责将数据结构转换为领域对象并反之(当然,如果需要,它可以为此使用一个辅助类)。这个想法源于六边形架构和整洁架构。
有状态还是无状态?
这取决于。在大多数情况下,无状态单例 bean 就足够了。如果不够,将其设置为有状态也没有问题。我见过一些完全围绕无状态服务/ Bean 构建的应用程序,在某些情况下,效果非常糟糕。如果服务的显式实现是无状态的,那就这样。但一旦它需要一些状态,例如,很多参数通过服务的方法传递下来,您就应该使其有状态。顺便说一下,这适用于所有 Bean,不仅仅是服务。
如何创建服务实例?
因为我假设业务类不了解框架,那么显然我不能使用任何 Spring 注解,比如 @Service
或 @Component
。但还有其他一些选择
- 控制器中的
new
- 看起来可能有些糟糕的耦合,但在某些情况下可能足够了,例如,一个没有依赖项的简单服务 - 主组件中的
@Configuration
类内的@Bean
- 适用于无状态服务 - 由控制器内部使用的、带有
@Component
注解的工厂类 - 适用于有状态服务
创建服务时,我们通过构造函数或一系列 setter 方法注入依赖项。与许多人所说的不同,我认为使用构造函数注入而不是 setter 并没有太大的优势,尤其是在您有很多依赖项的情况下。另一方面,大量的依赖项可能表明设计存在问题。
示例
让我们更详细地看一下下单的例子。我们将假设处理新订单包含几个步骤
- 保存订单
- 创建与订单关联的发票
- 将发票发送给客户
- 将包裹发送给客户
当然,我们的服务不必自己实现所有这些。相反,它将协调其协作者
这张图可能(让它)看起来很复杂,但代码可能非常简单
public class OrderPlacement {
// collaborators
public void execute(OrderData orderData) {
Order order = orderRepository.save(toOrder(orderData));
Invoice invoice = invoiceRepository.save(invoiceFactory.make(order));
mailer.send(invoice);
parcelSender.send(parcelFactory.make(order));
}
// toOrder, setters
}
服务似乎适合无状态,因此我们可以将其创建为 @Configuration
类中的单例 Bean
@Configuration
public class OrderServices {
@Bean
public OrderPlacement orderPlacement() {
return new OrderPlacement(...);
}
// other services
}
最后一步,我们在控制器中自动装配 Bean
@Controller
public class OrderController {
private OrderPlacement orderPlacement;
@Autowired
public OrderController(OrderPlacement orderPlacement) {
this.orderPlacement = orderPlacement;
}
// @RequestMapping etc.
public void placeOrder(PlaceOrderRequest request) {
// validate the request etc.
orderPlacement.execute(toOrderData(request));
}
// toOrderData(PlaceOrderRequest request)
}
请注意,控制器*可以*,但*不一定*需要与服务具有相同的输入。
分区 (Partitioning)
我们可以想象,下单过程比这几个步骤要复杂得多。当然,其中任何一个步骤都可能更加复杂,例如,可能需要更多的操作、日志记录或更新业务指标。在这种情况下,在一个服务中实现所有这些将过于冗长(或者并非如此,您已经阅读了我之前链接的 gist?)。我们可以通过将过程拆分成更小的服务来解决这个问题,这些服务将成为原始服务的直接协作者
进行这种划分的一个明显好处(也是执行这种划分的另一个原因)是,较小的服务很可能具有可重用性。
结论
服务关乎用例。它们协调其他对象/服务以满足用例背后的需求。服务的好名称是其所代表的(部分)用例背后过程的名称。服务消费和生成简单的数据结构,从而保护业务逻辑不泄露到边界之外。它可以是有状态的,也可以是无状态的,这会影响我们创建它的方式。一旦我们发现我们的服务太大了,或者我们想重用它的某一部分,我们就可以将其拆分成更小的服务。