使用Java接口实现外观模式时的封装保持方案问询
现有模块采用外观模式设计:RestaurantService作为对外暴露的外观接口,整合了KitchenService和ReservationService等内部服务。内部服务类采用包级私有(default)作用域,封装效果良好——客户端仅知晓RestaurantService,对内部服务无感知。
现有核心代码结构如下:
public class RestaurantServiceImpl implements RestaurantService { private KitchenService kitchenService; private ReservationService reservationService; @Override public void bookTable(){ this.reservationService.bookTable(); } @Override public OrderDto placeOrder() { boolean orderAccepted = this.kitchenService.acceptOrder(); // 业务逻辑省略 } } // 包级私有内部服务类 class KitchenService { public boolean acceptOrder(){ // 业务逻辑省略 return true; } } // 包级私有内部服务类 class ReservationService { public void bookTable(){ // 业务逻辑省略 } }
希望将内部服务类抽象为接口以提升扩展性,但Java接口无法用protected修饰,若声明为public则会暴露给客户端,引发两个问题:
- 客户端混淆:无法确定应调用
RestaurantService还是直接调用ReservationService的bookTable()方法; - 代码脆弱:开发者直接调用内部服务会绕过外观类的统一逻辑,导致后续业务变更时出现不一致。
方案1:包级私有内部服务接口(最直接的纯Java方案)
Java中接口默认访问权限为包级私有(即不写public修饰符),仅允许同一包内的类访问。基于此,我们可以将内部服务的接口和实现都保持在同一个包内,且接口不声明public:
// 对外暴露的公共外观接口 public interface RestaurantService { void bookTable(); OrderDto placeOrder(); } public class RestaurantServiceImpl implements RestaurantService { // 依赖包级私有接口 private KitchenService kitchenService; private ReservationService reservationService; @Override public void bookTable(){ this.reservationService.bookTable(); } @Override public OrderDto placeOrder() { boolean orderAccepted = this.kitchenService.acceptOrder(); // 业务逻辑省略 return new OrderDto(); } } // 包级私有内部服务接口(无public修饰符) interface KitchenService { boolean acceptOrder(); } // 包级私有接口实现类 class KitchenServiceImpl implements KitchenService { @Override public boolean acceptOrder(){ // 业务逻辑省略 return true; } } // 包级私有内部服务接口 interface ReservationService { void bookTable(); } // 包级私有接口实现类 class ReservationServiceImpl implements ReservationService { @Override public void bookTable(){ // 业务逻辑省略 } }
这种方案完全保留了原有的封装性:客户端在其他包中无法访问KitchenService和ReservationService接口,只能通过RestaurantService与系统交互。
方案2:Java模块系统封装(Java 9+)
如果项目基于Java 9及以上版本,可以利用模块系统实现更严格的封装:
- 将内部服务的接口和实现放在单独的子包(如
com.restaurant.internal); - 在
module-info.java中仅导出对外暴露的外观接口包,不导出内部服务包:
// module-info.java module restaurant { // 仅导出对外的外观接口包 exports com.restaurant.api; // 内部服务包不导出,其他模块无法访问 // exports com.restaurant.internal; 不写这行 }
此时即使内部服务接口声明为public,其他模块也无法访问,只有同一模块内的RestaurantServiceImpl可以依赖这些内部接口。
方案3:私有嵌套接口与实现(极致封装)
如果希望内部服务完全隐藏在外观类内部,可以将内部接口和实现类定义为外观类的私有嵌套类:
public interface RestaurantService { void bookTable(); OrderDto placeOrder(); } public class RestaurantServiceImpl implements RestaurantService { // 私有嵌套内部服务接口 private interface KitchenService { boolean acceptOrder(); } // 私有嵌套实现类 private class KitchenServiceImpl implements KitchenService { @Override public boolean acceptOrder(){ return true; } } private interface ReservationService { void bookTable(); } private class ReservationServiceImpl implements ReservationService { @Override public void bookTable(){ // 业务逻辑省略 } } private final KitchenService kitchenService = new KitchenServiceImpl(); private final ReservationService reservationService = new ReservationServiceImpl(); @Override public void bookTable(){ reservationService.bookTable(); } @Override public OrderDto placeOrder() { boolean orderAccepted = kitchenService.acceptOrder(); return new OrderDto(); } }
这种方案将内部服务的实现完全封装在外观类内部,外部任何代码都无法访问,适合内部服务逻辑与外观类高度耦合的场景。
方案4:依赖注入框架限定(Spring/CDI等)
如果项目使用依赖注入框架(如Spring),可以通过限定符和包扫描规则实现封装:
- 内部服务接口声明为
public,但实现类添加仅内部可见的限定符(如@Internal自定义注解); - 配置DI框架仅允许外观类注入内部服务,客户端无法通过DI获取内部服务实例;
- 限制内部服务包的扫描范围,仅让外观类所在组件扫描到内部服务。
示例(Spring):
// 自定义内部限定符注解 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Internal { } // 内部服务接口(public) public interface KitchenService { boolean acceptOrder(); } // 内部服务实现类,添加@Internal注解 @Service @Internal public class KitchenServiceImpl implements KitchenService { @Override public boolean acceptOrder(){ return true; } } // 外观类通过限定符注入内部服务 @Service public class RestaurantServiceImpl implements RestaurantService { private final KitchenService kitchenService; private final ReservationService reservationService; // 使用@Internal限定符注入 public RestaurantServiceImpl(@Internal KitchenService kitchenService, @Internal ReservationService reservationService) { this.kitchenService = kitchenService; this.reservationService = reservationService; } // 业务方法省略 }
同时,在Spring配置中可以通过@ComponentScan的basePackages或excludeFilters限制内部服务不被其他组件扫描,进一步确保客户端无法获取内部服务实例。
- 若项目为纯Java且无框架依赖,优先选择方案1(包级私有接口),实现简单且完全保留封装性;
- 若使用Java 9+,**方案2(模块系统)**提供了跨模块的强封装能力;
- 若内部服务与外观类高度耦合,**方案3(私有嵌套类)**是极致封装的选择;
- 若使用DI框架,**方案4(限定符+扫描规则)**能在保持扩展性的同时避免内部服务暴露。
内容的提问来源于stack exchange,提问作者Meena Chaudhary

