Por qué los modelos con anemia son dañinos

Los modelos anémicos son un enfoque tan común en el desarrollo de software que muchas personas ni siquiera saben que lo están usando, incluido yo mismo.

1. ¿Qué es el modelo de anemia?

La encapsulación es una de las tres características de la programación orientada a objetos . Encapsulación significa literalmente empaquetar. En programación, la encapsulación se refiere al uso de tipos de datos abstractos para encapsular datos y operaciones relacionadas con datos para formar un todo inseparable e independiente. Los datos están protegidos dentro del tipo de datos abstractos, y solo algunas interfaces externas están reservadas para que se conecten. con el mundo exterior.

Los modelos anémicos son modelos que separan los datos de las operaciones correspondientes. El modelo desacoplado consta de dos clases separadas: una que contiene la entidad de datos y la otra es un servicio sin estado que opera en la entidad.

Las entidades suelen contener propiedades, setters y getters. Un ejemplo típico se ve así:

public final class Time {
    public final int hour;
    public int minute;
    
    public int getHour() {
        return hour;
    }
    public void setHour(int hour) {
        this.hour = hour;
    }

    public int getMinute() {
        return minute;
    }
    public void setMinute(int minute) {
        this.minute = minute;
    }
}
复制代码

Los servicios sin estado, como su nombre indica, están representados por clases sin estado. La clase tendrá variables miembro. Estas variables miembro no son su estado, sino otros servicios o infraestructura de los que depende, como la base de datos DAO (mybatis mapper). Los servicios encapsulan la lógica empresarial que debería estar en el modelo de entidad. Un ejemplo de un servicio sin estado es el siguiente:

public class TimeService {
    public Time getServerTime() {
        //省略业务逻辑
        ...
    }
}
复制代码

2. ¿Por qué es dañino el modelo de anemia?

Podemos decir que el modelo anémico no se ajusta al diseño orientado a objetos. No es correcto separar la lógica en otras clases en los métodos OOD. Pero esa no es la razón principal por la que los modelos de dominio anémicos son dañinos. Tener OOD puro no es un objetivo en sí mismo. Necesitamos considerar razones más fundamentales. Hay tres de ellos:

2.1 Primero está la capacidad de descubrimiento

Si los datos y las operaciones relacionadas con los datos están encapsulados en una clase, podemos encontrar la clase en el IDE y luego podemos ver la lógica comercial relacionada. Pero en el modelo de anemia, encontramos la clase, solo vemos sus propiedades, también tenemos que saber en qué servicio está la lógica de negocios correspondiente. Cuando hay muchas clases de servicio y muchos métodos de servicio, esta no es necesariamente una tarea fácil, especialmente para los desarrolladores que no se han hecho cargo durante mucho tiempo.

2.2 Repetir

如果开发者不容找到一个相关的逻辑,这就容易导致他去重写一个类似的逻辑。这就是出现了重复。这就违反了DRY(Don't repeat yourself)。

2.3 缺乏封装

缺乏封装是最糟糕的。通过应用代码规范可以比较容易避免前两点。但是,这第三点总是会对你的项目产生有害影响。正是缺乏封装让大多数使用贫血模型的项目代码越来越腐坏。

3.什么是封装?

在本文开头就给出封装的定义。从定义中我们看到两层意思:

  • 封装是信息隐藏:信息隐藏就是将某些类成员设为私有,让其对客户端代码不能直接读写。
  • 封装意味着将数据和操作捆绑在一起。

这其实都是封装的手段,而不是封装的目的。

3.1 封装是一种保护数据一致性约束(invariant)的行为

保护数据一致性约束是指防止调用方将对象的内部数据设置为无效或不一致的状态。通过信息隐藏,以及将数据和操作捆绑在一起实现,我们可以保证对象的一致性约束。 比如前面的 Time 类。我们知道一天是24小时,那么 hour 属性的有效取值是 [0,24]。而 minute 的有效值则是 [0, 60]。这就是时间类的一致性约束。通过将这也的约束封装在如下的 Time 类中,我们就可以放心的去使用 Time,因为一旦输入不合法的 hour 或者 minute,就会抛出异常,不至于让错误的时间对象,继续在其他的业务逻辑中横行,最终被持久化到数据库中。

public final class Time {
    private static final int HOURS_PER_DAY = 24;
    private static final int MINUTES_PER_HOUR = 60;
    public final int hour;
    public final int minute;
    public Time(int hour, int minute) {
        if (hour < 0 || hour >= HOURS_PER_DAY)
            throw new IllegalArgumentException("Hour: " + hour);
        if (minute < 0 || minute >= MINUTES_PER_HOUR)
            throw new IllegalArgumentException("Min: " + minute);
        this.hour = hour;
        this.minute = minute;
    }
... // Remainder omitted
}
复制代码

如果没有这样的封装,每次我们在使用 Time 的时候,都得小心翼翼的,以免设置了非法的值,这对我们的大脑造成了额外的负担。当然这个 Time 的一致性是如此的简单,我们可能并不需要付出多少精力。但是业务上有很多复杂的一致性。比如很多类有开始时间和结束时间,这个结束时间一定要晚于开始时间,这个要比 Time 稍微复杂点(相信大家没少写出过结束时间早过开始时间的 bug )。

更复杂的例子,比如订单的状态,订单状态变化需要严格的一致性约束,订单从一个状态转变为另一个状态,一定是由于发生了某个事件,是一个严格的状态机逻辑。当然你可以说,服务类中也可以实现这些一致性约束。没错,这是可以的,而且很多人肯定是这么做的。只是大多数工程中的服务类都承担了较多职责。不仅承担了业务逻辑,还承担了将业务逻辑、其他领域服务和基础设施服务(数据库、消息队列、缓存等)编排到一起,服务一个客户端请求的处理。也就是说业务逻辑和编排逻辑(通常说的胶水)混到一起,违反了SRP(单一职责)原则。

封装保证了类的一致性约束,同时减轻了我们大脑的精力负载,对任何人来说,项目代码中的众多的复杂的约束不可能靠我们的记忆力和小心翼翼来保证。

3.2 封装带来了测试便利性

实体领域模型是内聚的,不像服务类由众多的依赖。这就让单元测试变得更加容易。

4.为什么贫血模型会变得流行?

贫血模型实际上是一种过程化思维,对于开发人员来说是更加直观的。甚至有些专业的组织机构也曾鼓励过这样的模型,比如著名的J2EE Entity Bean。

MVC 等分层模型建议将领域模型从数据持久化存储和显示层逻辑分开。这可能导致了越来越多的人将业务逻辑在服务实现,这并将很直观,也很方便,

当然有些场景下,贫血模型有时这可能是有益的。

image.png

该图表达以工作时间衡量的开发进度。如图所示,贫血领域模型项目开始的时候速度很快,但是会导致未来维护成本增加。如果你知道项目规模不大或项目不会有频繁的更新,那么贫血领域模型是很好的选择,也可能是最佳选择。

可能正是由于这样那样的原因才会导致贫血模型会变得流行吧。我们应该记住,对于比较复杂的项目,并且会持续迭代更新开放的项目,我们一定要摒弃贫血模型,拥抱领域建模。

参考:

Supongo que te gusta

Origin juejin.im/post/7087425802219814948
Recomendado
Clasificación