Skip to content

23 泛化建模(中):可以不用泛化吗?

你好,我是钟敬。

上节课,我们学习了“泛化”,并用这个技术完成了“在子项目上报工时”的需求。如果你是第一次学的话,可能会觉得理解起来有一些难度吧。

今天,我们继续这个建模过程,首先考虑“在子项目上报工时”这个需求的更多细节,增加更多的灵活性。

然后,我们来考虑另一个需求——“为内部项目报工时”。我们会采用使用泛化和 使用泛化两种方式为这个需求建模,并且比较两者的异同,以便你更进一步理解泛化的思路。事实上,用不用泛化有时不是一个非黑即白的决定,而是一种权衡。

还是我当项目经理,你当架构师。

增强灵活性

我们先回顾一下上节课建立的模型。

到现在为止,我们的模型已经基本可以满足按 项目子项目普通工时项 来报工时的需求了。

我们简单复述一下目前的需求。

  • “零零后公司”只能按项目登记工时。
  • “九零后公司”只能按子项目登记工时。
  • “八零后公司”既可以按项目也可按子项目,还可以按普通工时项登记工时。

现在我开始向你提几个更深入的问题。

第一个问题 是:怎么控制“零零后公司” 只能 按项目,而不能按子项目报工时呢?反之,怎么控制“九零后公司”只能按子项目报工时呢?

为了解决这个问题,你决定为每个租户增加一个配置参数。

所有租户级的参数,都在 租户参数 这个实体里。我们商量了一下,把新的参数命名为 项目工时粒度,有三个选项,分别是:“项目”“子项目”和“两可”。对于“零零后公司”只要把这个参数设置成“项目”,就说明只能按 项目 来报工时。对于其他两个公司的需求,道理是一样的。

这样也解决了另一个问题,就是,万一“零零后公司”将来也想按子项目来报工时的话,我们只需要改一下这个参数的设置就可以了。

我的 第二个问题 是:对于多数公司,按 普通工时项 报工时应该是一个很普遍的需求。虽然“零零后公司”现在不需要这样报工时,但将来如果要,怎么办呢?

你告诉我:“ 普通工时项 在数据库里会映射成一个表。对于“零零后公司”,这个表里没有记录,那么自然就只能按项目报工时。将来如果需要按普通工时来报,例如“学习时间”,那么只需要把“学习时间”这条记录插到表里就可以使用了。所以模型不需要任何修改。

这时候你又反问了我 一个问题:“八零后公司”既可以按 项目 也可按 子项目 来报工时,也就是参数里的“两可”。但是“两可”的具体含义是什么呢?因为其实可以有两种不同的理解。

第一种理解 是,任何项目都“两可”。比如有一个项目A,那么每一个员工,既可以把工时报到项目A整体上,而不区分子项目,也可以把工时报到项目A的某个子项目上,完全取决于员工自己的选择。

第二种理解 是,对于某一个具体项目而言,只能在“按项目”和“按子项目”中选择一种。例如,项目A,只能按项目整体报工时;项目B,只能按子项目报工时。而对于整个公司而言,由于同时存在A和B两种情况,所以在整体上是“两可”。

我思考了一下,说:“确实有这两种细微的差别。但是如果在租户的参数上,再区分这两种情况的话,可能反而会把用户搞糊涂。”

事实上,之所以有“两可”这个选项,无非是想给项目自身一个灵活性,如果把这个选项放在项目级别,由项目经理来决定,似乎更合适。但是全局的参数仍然可以保留,作为默认值,具体项目可以修改默认值。基于这个思路,你尝试这样修改了模型。

你对模型做了三点改动。

第一,把租户参数里原来的“项目工时粒度”,改成了“ 默认 项目工时粒度”。也就是说,这个选项可以被具体的 项目 覆盖。

第二,你用一个注解,明确了“两可”的含义,也就是你所说的第一种理解:“两可”指的是,对于一个项目,既可以把工时报在项目整体上,也可以报在它的子项目上,取决于员工自己的选择。

第三,在 项目 实体上增加了一个 工时粒度 属性,它的可选项和租户参数里是一样的。这样就可以在 项目 中进行细粒度的配置了。

有了这几个改动,就可以在租户参数里设置最常用的选项作为默认值,然后让项目经理在项目级别决定是否要覆盖默认值。这样就能满足“八零后公司”的需求了。

但是这样的话,对于“零零后公司”,岂不是也可以由项目经理自行配置了?这显然不符合当前的需求。

于是你又增加了一个租户级别的参数。

这个参数就是“是否可覆盖默认项目工时粒度”。对于“零零后公司”和“九零后公司”,把这个参数设为“否”,对于“八零后公司”设为“是”。这样一来,就可以同时满足这三个公司的不同需求了。

而且,不只是这三家公司,对于更多的公司的类似需求来说,上面的设计也足以满足了。当然,如果以后再遇到更“古怪”的需求,我们可以遵循类似的思路去增加系统的灵活性。

我们再思考一下,前面的做法算不算“过度设计”呢?

我的看法是这样的:如果是给某一家公司量身定做一个系统的话,本来不需要考虑这么多。但作为一个 SaaS 系统,要面对各种不同的用户,所以就要把系统设计得灵活一些了。假如局限于某个特定的公司,这样做可能有“过度设计”的嫌疑。但对于 SaaS 应用,这样的设计应该是恰当的。

非客户项目的泛化

现在,我们再来考虑“九零后公司”的另一个需求,并尝试用泛化来建模。

这个需求是这样的:九零后公司有一些公司内部的项目,比如说几个小伙伴一起搞一个内部开源的项目。和客户项目不同,这些内部项目没有合同,也不需要专门分配人力,而是谁有空,就可以随时参与。

于是你问我:“是不是可以说,客户项目和内部项目统称为项目,他们的共性是都可以报工时?”

我说:“可以这么理解。”

你紧跟着问:“这么说,内部项目岂不就是普通工时项?是不是可以直接用普通工时项实现这个需求呢?”

我想了一下,说:“内部项目和客户项目还有更多的共性,是普通工时项所不具备的,比如说,作为项目,内部项目也有项目经理,也有子项目,也有明确的起止时间,也有项目状态等等。”

你觉得言之有理,于是利用泛化的知识,画出了后面的模型图。

从这个图里,我们看到,作为父类的 项目 实体,反映了 客户项目内部项目 的共性,例如都有项目经理,都可以建立子项目等等;而 客户项目 体现了与 内部项目 的区别或者说个性,例如一个客户项目必须属于某个合同,并且需要分配项目成员,而内部项目则不具有这两个特征。所以,目前这个模型确实反映了需求中的领域知识。

一定要泛化吗?

我仔细看了这个模型,然后又问了一个问题:“我承认这个模型正确地反映了需求,但是泛化似乎让这个模型变得更复杂了,有没有可能画一个不用泛化的模型,看看效果怎么样呢?”

你说:“也可以,我们比较一下吧。”

于是,你又画出了后面这个等价的模型。

可以看到,这个模型也正确地反映了同样的需求。如果把这个模型和没有反映“客户项目和内部项目”需求的模型,也就是这节课的第一张图相对比,就会发现,这个模型只有三个主要变化。

第一,在项目与合同的关联上,合同一端的多重性由“1..1”变成了“0..1”。这是因为,原来,一个项目必须属于某个合同,而现在,内部项目没有合同,也就是说,有的项目可以对应“0”个合同。

第二,在项目实体中增加了一个“是否客户项目”的标志,这样就代替了原来泛化符号起到的区分两种项目的作用。

第三,增加了两个约束,用于说明客户项目和内部项目的区别。而在采用泛化的模型中,并不需要这两个约束,因为模型本身的结构已经说明了相同的知识。

这时候我说:“站在产品经理的角度,不用泛化的模型,比起之前没有这个新需求的模型(“增强灵活性”一节的最后一张图)来说,并没有增加或者减少任何关联,而且由于没有用泛化,更简单,更容易理解,你觉得呢?”

你说:“从架构师的角度,这个模型也更容易实现。既然这样,咱们还是不用泛化了吧。” 于是我们达成了一致。

继续增强灵活性

你又看了一下和“是否客户项目”有关的业务规则,然后问了我另一个问题:“‘是否客户项目’和‘是否要分配项目成员’,是同一个维度的问题吗?比如说,会不会内部项目也要分配成员呢?”

我想了想说:“你说得对。尽管九零后公司自己认为这是一回事,但如果把这个应用卖给其他用户,可能就会出现内部项目也要分配成员的情况。也就是说,现在的模型还缺乏通用性。所以,把这两者当做两个维度,分开设置比较好。”

于是你又修改了模型。

在项目里增加了一个“是否要分配项目成员”的属性,和“是否客户项目”分开。然后,在原来关于“是否客户项目”的注释里,删除了有关分配项目成员的约束,因为“是否要分配项目成员”这个属性本身已经不言自明了。

这时候我问了你今天最后一个问题:“回过头来想一下,如果把‘是否客户项目’和‘是否要分配项目成员’都用泛化的方式表示,模型会变成什么样子呢?”

于是,你画出了后面的模型。

比起前面的图,这个模型更加复杂,理解起来更困难一些。所以,我们决定还是选择前面简单些的模型。

另外,这个模型实际上是按照两个不同的维度进行泛化的,称为“多重泛化”。我们在第三个迭代再来讨论这种复杂的情况。

总结

好,这节课的主要内容就讲到这,我们来总结一下。

今天,为了实现“为内部项目报工时”引发的需求,我们比较了使用泛化和不使用泛化两种方式。最后选择了从业务和技术视角都更加简单的,不使用泛化的方式。这说明,使用或者不使用泛化,是一个需要权衡的决策。在权衡过程中,需要结合业务和技术视角。

另外,通过为两个需求增加灵活性,我们也体会到,相比给特定企业定制的系统,SaaS系统往往需要考虑更多的灵活性。

不过,在使用还是不使用泛化这个问题上,你可能还是觉得,似乎现在是在凭“直觉”进行的判断。下节课,我们继续深入,聊聊权衡背后更多的思路和规律。

思考题

下面我给你留了两道思考题。

1.回顾上个迭代的需求, 开发中心直属部门组织 之间,是否应该用泛化来抽象呢?

2.领域建模里的泛化常常对应着面向对象编程里的继承,事实上,UML的符号都是相同的。而课程里说的多重泛化,在编程时则不能用继承直接实现,你能想到为什么吗?

好,今天的课程结束了,有什么问题欢迎在评论区留言,下节课,我们来讨论怎样权衡是否要使用泛化。