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 允许您从现有类中提取接口,并且它将重构所有代码以在眨眼之间使用该接口。