关于Clean Architecture中Ports and Adapters模式的Java包封装疑问
这是Ports and Adapters模式里典型的封装与依赖控制问题,核心是既要隐藏具体实现类,又要能在启动/组装层(Main)创建实例,有几种合规的实现方式:
1. 包内提供public工厂类/方法
这是最贴合书中设计意图的方案。在database包中新增一个public的工厂类,通过它暴露创建Orders接口实例的方法——因为同包内的类可以访问包访问权限(默认无修饰符)的JdbcOrdersRepository,工厂类作为对外的“出口”,既不暴露实现类,又能提供实例。
示例代码:
// database包下的public工厂类 public class OrdersRepositoryFactory { // 静态方法返回Orders接口实例 public static Orders createJdbcRepository() { // 同包内可直接实例化非public的JdbcOrdersRepository return new JdbcOrdersRepository(); } }
在Main组件中,直接通过工厂方法获取实例,全程依赖的是domain层的Orders接口,完全符合依赖倒置原则:
public class Main { public static void main(String[] args) { Orders ordersRepo = OrdersRepositoryFactory.createJdbcRepository(); OrdersService service = new OrdersServiceImpl(ordersRepo); // 后续业务逻辑... } }
2. 利用Java 9+模块系统的定向导出
如果项目基于Java模块系统,可以通过module-info.java做更精细的权限控制:将database包定向导出给Main所在的模块,这样只有指定模块能访问包内的包访问权限类,其他模块无法访问。
示例module-info.java:
// database模块的module-info.java module com.example.database { // 仅将database包导出给main模块 exports com.example.database to com.example.main; // 依赖domain模块(因为要实现Orders接口) requires com.example.domain; }
此时Main模块就能直接实例化JdbcOrdersRepository(前提是Main模块依赖database模块),同时其他模块无法访问这个类,兼顾了封装性和实例创建需求。
3. 反射(不推荐)
技术上可以通过Java反射绕过访问修饰符创建实例,但这种方式完全违背了书中封装实现类、控制依赖的设计初衷,会破坏架构的整洁性,仅作为技术可能性了解即可:
public class Main { public static void main(String[] args) throws Exception { Class<?> repoClass = Class.forName("com.example.database.JdbcOrdersRepository"); Constructor<?> constructor = repoClass.getDeclaredConstructor(); constructor.setAccessible(true); // 绕过访问权限检查 Orders ordersRepo = (Orders) constructor.newInstance(); } }
补充:书中设计的核心逻辑
Simon Brown这么设计的目的是强制依赖抽象而非具体实现:Main组件不需要知道JdbcOrdersRepository的存在,只需要依赖Orders接口,实例的创建交给专门的工厂或依赖注入容器(比如Spring)来处理,这样能保证domain层不被具体实现污染,同时实现层的变化不会影响上层组件。
内容的提问来源于stack exchange,提问作者fflo

