本周,在与我在一所高等学校开设的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<ClassicAccount, String> @Entity @Table(name = "ACCOUNT") class ClassicAccount(@Id var iban: String = "", var balance: BigDecimal = BigDecimal.ZERO)
那里有两个问题:
注意:不,没有货币。这是一个简单的例子,还记得吗?
Being Compliant
为了遵守无参数构造函数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 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结构将不同于上面返回的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 } }
这种方法的唯一缺点是,需要将存储库注入到控制器中,然后将其显式传递给实体的构造函数。
The Final Touch
如果将存储库在创建时自动注入到实体中,那就太好了。好的,Spring通过面向方面的编程使这成为了可能(尽管这不是一个非常知名的功能)。它需要执行以下步骤:
向应用程序添加AOP功能
有效地添加AOP依赖关系非常简单,只需将相关的启动器依赖关系添加到POM中即可:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
然后,必须将应用程序配置为可以使用它:
@SpringBootApplication @EnableSpringConfigured 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) }
注意:Remember that field-injection is evil.
Aspect Weaving
有两种编织方面的方法,即编译时编织或加载时编织。我选择后者,因为它更容易配置。它是通过标准Java代理实现的。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-agent</artifactId> <version>2.5.6</version> <scope>runtime</scope> </dependency>
<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 class OopspringApplication
接着?
当然,该示例省略了设计的重要部分:如何更新帐户余额。分层方法对此有一个解决方法,但这不是面向对象的。考虑一下,一个帐户的余额会发生变化,因为存在从另一个帐户进行的转帐。可以将其建模为:
fun OopAccount.transfer(source: OopAccount, amount: BigDecimal) { ... }
经验丰富的开发人员应该看到一些交易管理要求正在悄悄溜走。我将实现留给有动机的读者。下一步将是缓存值,因为每次读取和写入都访问数据库会降低性能。
结论
我想提出几点。
首先,标题问题的答案是肯定的“是”。结果是真正的OOP代码,同时仍使用所谓的企业级框架(即Spring)。
但是,迁移到与OOP兼容的设计会带来一些开销。我们不仅依赖于现场注入,还必须通过加载时织入引入AOP。第一个是单元测试期间的障碍,第二个是您绝对不希望每个团队使用的技术,因为它们会使应用程序变得更加复杂。这仅是一个简单的例子。
最后,这种方法有一个巨大的缺点:大多数开发人员都不熟悉它。无论其优势如何,首先必须“限制”他们具有这种思维方式。这可能是继续使用传统分层体系结构的原因。
我已经在Google上搜索科学研究,以证明OOP在可读性和可维护性方面更好:我没有发现。我将非常感谢指针。
原文链接:http://codingdict.com