spring boot项目,需要写一个接口吗?
工作十年接触过无数大大小小的springboot项目,我发现国内公司的项目喜欢把项目层级弄得很复杂,各种类不管是单一实现还是多实现,先写个接口再说。而我在外企同样是spring boot技术的大型项目,项目的层级结构很简单就4到5层,只要用不到接口的地方绝对不会写,能用实体内绝不用抽象类,孰优孰劣,仁者见仁智者见智。下面是国外大神对项目中使用接口的看法:
翻译:
使用 Spring boot 时,您经常使用服务(使用 注释的 bean @Service)。在 Internet 上的许多示例中,您会看到人们为这些服务创建接口。µ 例如,如果我们正在创建一个待办事项列表应用程序,您可能会创建一个TodoService带有TodoServiceImpl实现的接口。
在这篇博文中,我们将了解为什么我们经常这样做,以及是否有必要。
简短的回答
简短的回答很简单。不,您不需要接口。如果您创建一个服务,您可以命名该类本身TodoService并在您的 bean 中自动装配它。例如:
@Service
public class TodoService {
public List<Todo> findAllTodos() {
// TODO: Implement
return new ArrayList<>();
}
}
@Component
public class TodoFacade {
private TodoService service;
public TodoFacade(TodoService service) {
this.service = service;
}
}
无论您使用字段注入还是构造函数注入,您在此处看到的示例都将起作用。@Autowired
那何必呢?
所以,如果我们不需要它……那为什么我们经常写一个?嗯,第一个原因是一个相当历史的原因。但在我们看之前,我们必须解释注解是如何与 Spring 一起工作的。
如果您使用诸如 之类的注释@Cacheable,您希望返回缓存中的结果。Spring 这样做的方式是为您的 bean 创建一个代理并向这些代理添加必要的逻辑。最初,Spring 使用 JDK 动态代理。这些动态代理只能为接口生成,这就是为什么您必须在过去编写接口的原因。
但是,从十多年前开始,Spring 还支持 CGLIB 代理。这些代理不需要单独的接口。从 Spring 3.2 开始,您甚至不必添加单独的库,因为 CGLIB 包含在 Spring 本身中。
松耦合
第二个原因可能是在两个类之间创建松散耦合。通过使用接口,依赖于您的服务的类不再依赖于它的实现。这使您可以独立使用它们。例如:
public interface TodoService {
List<Todo> findAllTodos();
}
@Service
public class TodoServiceImpl {
public List<Todo> findAllTodos() {
// TODO: Implement
return new ArrayList<>();
}
}
@Component
public class TodoFacade {
private TodoService service;
public TodoFacade(TodoService service) {
this.service = service;
}
}
但是,在这个例子中,我认为TodoFacade和TodoServiceImpl属于一起。在此处添加接口会产生额外的复杂性。就个人而言,我认为不值得。
多种实现
松耦合可能有用的一个原因是如果您有多个实现。例如,假设您有两个 a 的实现TodoService,其中一个从内存中检索待办事项列表,另一个从某处的数据库中检索它。
public interface TodoService {
List<Todo> findAllTodos();
}
@Service
public class InMemoryTodoServiceImpl implements TodoService {
public List<Todo> findAllTodos() {
// TODO: Implement
return new ArrayList<>();
}
}
@Service
public class DatabaseTodoServiceImpl implements TodoService {
public List<Todo> findAllTodos() {
// TODO: Implement
return new ArrayList<>();
}
}
@Component
public class TodoFacade {
private TodoService service;
public TodoFacade(TodoService service) {
this.service = service;
}
}
在这种情况下,松散耦合非常有用,因为您TodoFacade不需要知道待办事项是存储在数据库中还是存储在内存中。那不是门面的责任,而是应用程序配置的责任。
您完成这项工作的方式取决于您要实现的目标。如果您TodoFacade必须调用所有实现,那么您应该注入一个集合:
@Component
public class TodoFacade {
private List<TodoService> services;
public TodoFacade(TodoService services) {
this.services = services;
}
}
如果其中一种实现应在 99% 的情况下使用,而另一种仅在非常特殊的情况下使用,则使用@Primary:
@Primary
@Service
public class DatabaseTodoServiceImpl implements TodoService {
public List<Todo> findAllTodos() {
// TODO: Implement
return new ArrayList<>();
}
}
使用@Primary,你告诉 Spring 容器,只要它必须注入一个TodoService. 如果必须使用另一个,则必须通过使用@Qualifier或注入特定实现本身来显式配置它。就个人而言,我会在一个单独的@Configuration类中执行此操作,因为否则,您会TodoFacade再次使用特定于实现的细节来污染您的。
例如:
@Configuration
public class TodoConfiguration {
@Bean
// Using @Qualifier
public TodoFacade todoFacade(@Qualifier("inMemoryTodoService") TodoService service) {
return new TodoFacade(service);
}
@Bean
// Or by using the specific implementation
public TodoFacade todoFacade(InMemoryTodoService service) {
return new TodoFacade(service);
}
}
控制反转
另一种松散耦合是控制反转或 IoC。对我来说,控制反转在处理多个相互依赖的模块时很有用。例如,假设我们有一个OrderService 和一个CustomerService。客户应该能够删除其个人资料,在这种情况下,应取消所有挂单。如果我们在没有接口的情况下实现它,我们会得到这样的结果:
@Service
public class OrderService {
public void cancelOrdersForCustomer(ID customerId) {
// TODO: implement
}
}
@Service
public class CustomerService {
private OrderService orderService;
public CustomerService(OrderService orderService) {
this.orderService = orderService;
}
public void deleteCustomer(ID customerId) {
orderService.cancelOrdersForCustomer(customerId);
// TODO: implement
}
}
如果我们这样做,事情会很快变糟。您的应用程序中的所有域都将绑定在一起,最终您将得到一个高度耦合的应用程序。
CustomerDeletionListener我们可以创建一个接口,而不是这样做:
public interface CustomerDeletionListener {
void onDeleteCustomer(ID customerId);
}
@Service
public class CustomerService {
private List<CustomerDeletionListener> deletionListeners;
public CustomerService(List<CustomerDeletionListener> deletionListeners) {
this.deletionListeners = deletionListeners;
}
public void deleteCustomer(ID customerId) {
deletionListeners.forEach(listener -> listener.onDeleteCustomer(customerId));
// TODO: implement
}
}
@Service
public class OrderService {
public void cancelOrdersForCustomer(ID customerId) {
// TODO: implement
}
}
@Component
public class OrderCustomerDeletionListener implements CustomerDeletionListener {
private OrderService orderService;
public OrderCustomerDeletionListener(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void onDeleteCustomer(ID customerId) {
orderService.cancelOrdersForCustomer(customerId);
}
}
如果你看一下这个例子,你会看到控制反转在起作用。在第一个示例中,如果我们更改cancelOrdersForCustomer()内的方法OrderService,则CustomerService也必须更改。这意味着它OrderService处于控制之中。
在第二个示例中,OrderService不再受控制。当我们更改cancelOrdersForCustomer()模块时,只有OrderCustomerDeletionListener必须更改,这是订单模块的一部分。这意味着它CustomerService处于控制之中。此外,这两种服务都是松散耦合的,因为其中一个不直接依赖于另一个。
虽然第二种方法确实引入了更多的复杂性(一个新类和一个新接口),但它确实使得两个域都不会与另一个域高度耦合。这使它们更容易重构。此侦听器也可以重构为更受事件驱动的架构。这使得重构为领域驱动的模块化设计或微服务架构变得更加容易。
测试
我想谈的最后一件事是测试。有些人会争辩说您需要一个接口,以便您可以拥有一个虚拟实现(因此,有多个实现)。然而,像 Mockito 这样的模拟库解决了这个问题。
如果您正在编写单元测试,则可以使用MockitoExtension:
@ExtendWith(MockitoExtension.class)
public class TodoFacadeTest {
private TodoFacade facade;
@Mock
private TodoService service;
@BeforeEach
void setUp() {
this.facade = new TodoFacade(service);
}
// TODO: implement tests
}
这种方法允许你在不知道服务做什么的情况下正确地测试外观。通过使用Mockito.when(),您可以控制服务模拟应该返回什么,并且通过使用,Mockito.verify()您可以验证是否调用了特定方法。例如:
@Test
void findAll_shouldUseServicefindAllTodos() {
Todo todo = new Todo();
when(service.findAllTodos()).thenReturn(todo);
assertThat(facade.findAll()).containsOnly(todo);
verify(service).findAllTodos();
}
即使您正在编写需要运行 Spring 容器的集成测试,您也可以使用@MockBean注解来模拟 bean。确保您不扫描包含实际实现的包。
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = TodoFacade.class)
public class TodoFacadeTest {
@Autowired
private TodoFacade facade;
@MockBean
private TodoService service;
}
所以在大多数情况下,您在测试时不需要接口。
结论
所以,如果你问我是否应该为你的服务使用接口,我的回答是否定的。唯一的例外是,如果您尝试使用控制反转,或者您有多个实现需要处理。
你可能会想,创建一个接口不是更好吗,以防万一?我也会对此说不。首先,我相信“你不需要它”(YAGNI)原则。这意味着您不应该为了“我可能需要它”而在代码中添加额外的复杂性,因为通常情况下您不需要。其次,即使事实证明你确实需要它,也没有问题。大多数 IDE 允许您从现有类中提取接口,并且它将重构所有代码以在眨眼之间使用该接口。
浙公网安备 33010602011771号