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