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<ClassicAccount, String>

@Entity
@Table(name = "ACCOUNT")
class ClassicAccount(@Id var iban: String = "", var balance: BigDecimal = BigDecimal.ZERO)

那里有两个问题:

  1. JPA规范要求无参数构造函数。因此,可以ClassicalAccount使用空的IBAN创建实例。
  2. 没有IBAN的验证。需要完整往返数据库以检查IBAN是否有效。

注意:不,没有货币。这是一个简单的例子,还记得吗?

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代理实现的。

  1. 首先,需要将其作为运行时依赖项添加到POM中:
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-agent</artifactId>
    <version>2.5.6</version>
    <scope>runtime</scope>
</dependency>
  1. 然后,必须使用代理配置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>
  1. 最后,必须对应用程序进行相应的配置:
@EnableLoadTimeWeaving
class OopspringApplication

接着?

当然,该示例省略了设计的重要部分:如何更新帐户余额。分层方法对此有一个解决方法,但这不是面向对象的。考虑一下,一个帐户的余额会发生变化,因为存在从另一个帐户进行的转帐。可以将其建模为:

fun OopAccount.transfer(source: OopAccount, amount: BigDecimal) { ... }

经验丰富的开发人员应该看到一些交易管理要求正在悄悄溜走。我将实现留给有动机的读者。下一步将是缓存值,因为每次读取和写入都访问数据库会降低性能。

结论

我想提出几点。

首先,标题问题的答案是肯定的“是”。结果是真正的OOP代码,同时仍使用所谓的企业级框架(即Spring)。

但是,迁移到与OOP兼容的设计会带来一些开销。我们不仅依赖于现场注入,还必须通过加载时织入引入AOP。第一个是单元测试期间的障碍,第二个是您绝对不希望每个团队使用的技术,因为它们会使应用程序变得更加复杂。这仅是一个简单的例子。

最后,这种方法有一个巨大的缺点:大多数开发人员都不熟悉它。无论其优势如何,首先必须“限制”他们具有这种思维方式。这可能是继续使用传统分层体系结构的原因。

我已经在Google上搜索科学研究,以证明OOP在可读性和可维护性方面更好:我没有发现。我将非常感谢指针。


原文链接:http://codingdict.com