本周,在与我在一所高等学校开设的Java课程有关的研讨会上,我注意到学生编写的代码主要-完全是程序性的。 实际上,尽管Java语言自吹自as是一种面向对象的语言,但找到由企业中的专业开发人员开发的此类代码并不少见。 例如, JavaBean规范与OOP的主要原理之一封装直接矛盾。
另一个例子是在Java EE和Spring应用程序中同样广泛使用的控制器,服务和DAO架构。 在这种情况下,实体通常是贫乏的 ,而所有业务逻辑都位于服务层中。 尽管这本身还不错,但该设计将状态与行为分开,与真正的OOP相反。
Java EE和Spring框架都强制执行此分层设计。 例如,在Spring中,每个这样的层都有一个注释: @Controller
, @Service
@Controller
和@Repository
。 在Java EE世界中,只能使@EJB
实例(服务层)具有事务性。
这篇文章旨在尝试调和OOP范例和分层体系结构。 我将使用Spring框架来强调我的观点,因为我对此更加熟悉,但是我相信相同的方法也可以用于纯Java EE应用程序。
一个简单的用例
让我们有一个简单的用例:从IBAN编号中找到具有相关余额的关联帐户。 在标准设计中,可能看起来像这样:
@RestController
classClassicAccountController(privatevalservice:AccountService){
@GetMapping("/classicaccount/{iban}")
fungetAccount(@PathVariable("iban")iban:String)=service.findAccount(iban)
}
@Service
classAccountService(privatevalrepository:ClassicAccountRepository){
funfindAccount(iban:String)=repository.findOne(iban)
}
interfaceClassicAccountRepository:CrudRepository<ClassicAccount,String>
@Entity
@Table(name="ACCOUNT")
classClassicAccount(@Idvariban:String="",varbalance:BigDecimal=BigDecimal.ZERO)
那里有两个问题:
- JPA规范要求无参数构造函数。 因此,可以使用空的IBAN创建
ClassicalAccount
实例。 - 没有IBAN的验证。 需要完整的数据库往返行程以检查IBAN是否有效。
是的,没有货币。 这是一个简单的例子,还记得吗?
合规
为了符合no-args构造函数JPA约束-并且由于我们使用Kotlin,因此可以生成综合构造函数。 这意味着可以通过反射访问构造函数,但不能直接调用构造函数。
<plugin>
<artifactId> kotlin-maven-plugin </artifactId>
<groupId> org.jetbrains.kotlin </groupId>
<version> ${kotlin.version} </version>
<configuration>
<compilerPlugins>
<plugin> jpa </plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId> org.jetbrains.kotlin </groupId>
<artifactId> kotlin-maven-noarg </artifactId>
<version> ${kotlin.version} </version>
</dependency>
</dependencies>
</plugin>
如果您使用Java,那么运气不好,我不知道有任何解决方案。
添加验证
在层架构中,服务层是放置业务逻辑(包括验证)的显而易见的位置:
@Service
classAccountService(privatevalrepository:ClassicAccountRepository){
funfindAccount(iban:String):Account?{
checkIban(iban)
returnrepository.findOne(iban)
}
funcheckIban(iban:String){
if(iban.isBlank())throwIllegalArgumentException("IBAN cannot be blank")
}
}
为了更符合OOP,我们必须决定是否允许无效的IBAN编号。 完全禁止它更容易。
@Entity
@Table(name="ACCOUNT")
classOopAccount(@Idvariban:String,varbalance:BigDecimal=BigDecimal.ZERO){
init{
if(iban.isBlank())throwIllegalArgumentException("IBAN cannot be blank")
}
}
但是,这意味着我们必须首先创建OopAccount
实例以验证OopAccount
即使余额实际上不是0,余额也为0。同样,对于空的IBAN,代码与模型不匹配。 更糟糕的是,要使用存储库,我们必须访问OopAccount
内部状态:
repository.findOne(OopAccount(iban).iban)
面向对象的设计更友好
改善代码状态需要对类模型进行大量修改,将IBAN和帐户分开,以便可以验证前者并访问后者。 IBAN类既充当入口点,又充当帐户的PK。
@Entity
@Table(name="ACCOUNT")
classOopAccount(@EmbeddedIdvariban:Iban,varbalance:BigDecimal)
classIban(@Column(name="iban")valnumber:String,
@Transientprivatevalrepository:OopAccountRepository):Serializable{
init{
if(number.isBlank())throwIllegalArgumentException("IBAN cannot be blank")
}
valaccount
@JsonIgnore
get()=repository.findOne(this)
}
请注意,返回的JSON结构将不同于上面返回的结构。 如果这是一个问题,可以很容易地自定义Jackson以获取所需的结果。
通过这种新设计,控制器需要进行一些更改:
@RestController
classOopAccountController(privatevalrepository:OopAccountRepository){
@GetMapping("/oopaccount/{iban}")
fungetAccount(@PathVariable("iban")number:String):OopAccount{
valiban=Iban(number,repository)
returniban.account
}
}
这种方法的唯一缺点是,需要将存储库注入到控制器中,然后将其显式传递给实体的构造函数。
最后一点
如果将存储库在创建时自动注入到实体中,那就太好了。 嗯,Spring通过面向方面的编程使这成为可能-尽管这不是一个非常知名的功能。 它需要执行以下步骤:
-
向应用程序添加AOP功能
-
有效地添加AOP依赖关系非常简单,只需将相关的启动器依赖关系添加到POM:
<dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-aop </artifactId> </dependency>
然后,必须将应用程序配置为使用它:
@SpringBootApplication @EnableSpringConfigured classOopspringApplication
更新实体
-
- 首先必须将该实体设置为注入目标。 依赖注入将通过自动装配完成。
- 然后,将存储库从构造函数参数移至字段。
- 最后,数据库获取逻辑可以移到实体中:
@Configurable(autowire=Autowire.BY_TYPE) classIban(@Column(name="iban")valnumber:String):Serializable{ @Transient @Autowired privatelateinitvarrepository:OopAccountRepository init{ if(number.isBlank())throwIllegalArgumentException("IBAN cannot be blank") } valaccount @JsonIgnore get()=repository.findOne(this) }
记住,注入电场是邪恶的。
-
纵横编织
-
有两种方法可以将方面编织到中,即编译时编织或加载时编织。 我选择的是以后的配置要容易得多。 它是通过标准Java代理实现的。
- 首先,需要将其作为运行时依赖项添加到POM中:
<dependency> <groupId> org.springframework </groupId> <artifactId> spring-agent </artifactId> <version> 2.5.6 </version> <scope> runtime </scope> </dependency>
- 然后,必须使用代理配置Spring Boot插件:
<plugin> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-maven-plugin </artifactId> <configuration> <agent> ${settings.localRepository}/org/springframework/spring-agent/2.5.6/spring-agent-2.5.6.jar </agent> </configuration> </plugin>
- 最后,必须对应用程序进行相应的配置:
@EnableLoadTimeWeaving classOopspringApplication
- 首先,需要将其作为运行时依赖项添加到POM中:
然后?
当然,此示例省略了设计的重要部分:如何更新帐户余额? 分层方法对此有一个解决方法,但这不是OOP。 考虑一下,一个帐户的余额会发生变化,因为有另一个帐户的转帐。 可以将其建模为:
funOopAccount.transfer(source:OopAccount,amount:BigDecimal){...}
经验丰富的开发人员应该看到一些交易管理要求正在悄悄进行。我将实现留给有动机的读者。 下一步将是缓存值,因为每次读取和写入都访问数据库会降低性能。
结论
我想提出几点。
首先,标题问题的答案是肯定的“是”。 结果是一个真正的OOP代码,同时仍使用所谓的企业级框架-Spring。
但是,迁移到与OOP兼容的设计会带来一些开销。 我们不仅依赖于现场注入,还必须通过加载时编织引入AOP。 第一个是单元测试期间的障碍,第二个是您绝对不需要每个团队使用的技术,因为它们会使应用程序变得更加复杂。 这仅是一个简单的例子。
最后,这种方法有一个很大的缺点:大多数开发人员都不熟悉它。 无论其优势如何,首先必须“限制”他们具有这种思维方式。 这可能是继续使用传统分层体系结构的原因。
我在Google上搜索了一些科学研究,证明OOP在可读性和可维护性方面更好:我没有发现。 我将非常感谢指针。
翻译自: https://blog.frankel.ch/oop-compatible-enterprise-context/