如果亚里士多德是一名软件工程师呢?






4.82/5 (11投票s)
基于哲学模式的介绍。
引言
在20世纪70年代末,著名的漫画出版商漫威漫画(《蜘蛛侠》、《钢铁侠》、《美国队长》、《神奇四侠》等)推出了一套新颖独特的漫画系列——“假如…”系列(“假如蜘蛛侠加入了神奇四侠”,“假如美国队长当选总统”等),其宗旨是“探索未曾踏足的道路”。该系列每本书的出发点都是一个超乎漫威漫画通常叙事之外的极端情况。
在本文中,我们将尝试在另一个领域——软件设计领域——探索“假如”这两个词可能为我们打开的未知之路。本文的标题暗示了我们将使用的虚构情境是亚里士多德生活在我们这个时代,并作为一名软件工程师工作。由此引发的第一个问题(至少在我脑海中)是:亚里士多德作为一个哲学家,他对事物的根源感兴趣,或者说,他对事物的哲学基础感兴趣(甚至愿意挑战当前公认的哲学基础并提供替代方案),他将如何处理软件设计过程中出现的常见问题?换句话说,我们在这里试图探索的是一个哲学家处理软件设计问题的思维模式。
那么,首先,我们应该问,软件设计原则根本上是否存在哲学基础?众所周知,当前软件设计原则实际上是面向对象编程(OOP)原则,但我们似乎忽略了OOP原则本身,无论是有意还是无意,都源于一系列相对经典的哲学思想。例如,OOP最基本思想之一就是实例化。实例化意味着任何运行时元素(对象、实例)的本质,它实际上是一段内存,承载着对实际、现实世界实体的详细描述,都存在于一个抽象元素中。这就是类,类是一个不可变的实体(只要软件处于运行时状态),仅存在于一个独立的、抽象的领域,也就是代码本身——一组类似英语的词语。当然,很容易看出这一基本的OOP原则与柏拉图的理念论之间的相似之处。另一个基本的OOP原则是继承的概念,这意味着一个对象(实例)的属性和潜在行为集合不仅来自于其被实例化的实际类,还来自于该类在继承层次结构中的所有父类。在这种情况下,我们也很容易发现这种思想与亚里士多德在其著作《范畴篇》中描述世界的方式之间的相似之处——分层继承的生成、种类和实体,它们之间保持着清晰的继承关系。似乎如果我们进一步检查所有主要的OOP思想,最终都能找到它们的哲学起源。
正如我们前面提到的,一位既是软件工程师又是哲学家的软件工程师可能会挑战这些哲学起源(当然,并非总是如此,只在某些情况下,当很难找到基于当前概念的设计解决方案时),以用其他哲学思想取而代之。这种行为将使他更容易找到解决他所处理问题的适当设计解决方案。
为了说明这种用一种基于哲学的原则替换另一种原则的过程,以应对某些设计问题,我选择使用一种在某种意义上类似于已知的设计模式格式。这种新的格式,描述了一个我称之为基于哲学模式的实体,如下所示:
- 目的:简要描述基于哲学模式应处理的设计问题类型。
- 动机:基于软件设计问题示例的动机定义。
- 哲学背景:简要讨论哲学界对当前问题的见解。
- 当前基于哲学模式:描述目前作为做事模型的功能性哲学思想。
- 建议的基于哲学模式:一种模式,提出一种替代性哲学思想,在某些情况下成为设计解决方案的模型。
- 实现示例:展示建议的基于哲学模式的实现。
以下是一个基于哲学模式的示例,该模式与我们管理对象属性值历史的方式有关。
其他基于哲学模式的模式可在www.philosoftware.com找到。
对象历史 - 一种基于哲学的模式
目的
此设计模式的目的是提供一种管理对象属性随时间变化的方法。
动机
面向对象软件最基本特质之一就是其当代性——对象通常包含反映最新信息的(作为其数据成员值)。例如,CRM应用程序中的特定客户对象将包含最新的联系信息,因此,它不会包含(例如)联系人的前电话号码。当然,这并不意味着面向对象软件应用程序不被设计来处理历史信息——销售互动对象的列表很可能附加到上述客户对象上,毫无疑问,每次销售活动都是过去发生的事情。但即便如此,“历史”对象本身——在本例中是销售活动——将不包含其所记录的销售活动的历史(例如,所售产品的预谈判价格),而只包含最终结果。我们可以将其表述为:通常,对象数据将表示对象在特定时间点(通常是当前日期)的状态。
当然,我们可能会问自己,这是否足够?是否有任何情况值得保留和管理对象历史,而不仅仅是对象在特定时间点的状态?
让我们看一个例子——假设我们要开发一个应用程序,用于管理一家家具制造商产品树的价格列表。该应用程序的对象模型可能如下所示:
这样的对象模型可以轻松支持一个用户界面,使用户能够查看特定家具的当前或过去的价格,以及为该项目添加新价格(从某个日期开始生效)。但是,我们能否有效地使用上述对象模型来显示某件产品价格在过去一年中的变化(比如,在图表中)?当然,可以创建一个指向同一件家具的对象的列表,每个对象代表价格发生变化时家具的状态。这个对象列表可能会为我们构建所需的图表提供足够的信息。
好吧,尽管提出的解决方案可能解决了问题,我们仍然不能完全满意。事实上,由于特定事件(显示项目价格图表)软件必须构建和维护全新的对象集合,这与我们对应用程序的假设不符——它包含一个稳定而简单的对象层次结构,每个对象代表一件家具。换句话说,我们在这里争辩的是,各种需求迫使应用程序创建和维护不止一个对象模型;因此,其管理,在同步这些模型(毕竟它们肯定有重叠)的意义上,可能会变得枯燥乏味。
看来我们应该看得更远,找到一种按照不同方法构建对象模型的方式,一种可能更好地满足对象历史管理需求的方法。
哲学背景
当我们谈论对象历史时,我们实际上是在处理形而学(研究实在真实性原理的分支)中最深刻的问题之一——对象连续性的问题。
对象连续性讨论实际上源于另一个基本主题,即同一性本质——是物质“包含”了对象的同一性吗?如果是这样,一个对象是否可能因其组成部分或属性的变化而改变其同一性?纵观历史,许多哲学家都讨论过这个问题。“忒修斯之船”谜语是这种哲学讨论的一个古老例子。它讲述的是忒修斯在战胜米诺陶洛斯后从克里特返回雅典时乘坐的船。雅典人民长期保存着这艘船,并时常更换腐烂的旧木板,换上新的坚固木板。到后来,船上没有一块原始的木板了。随之而来的问题是,经过所有这些变化后,我们仍然看到的是忒修斯乘坐的那艘船吗?因为如果我们指代不同时间点的船,我们会发现我们指代的是两个由不同“物质”组成的实体。但似乎直观上,我们将这两个不同的实体视为一个——忒修斯之船。那么,我们如何解决我们直觉(单一对象)与事实(两个不同实体)之间的矛盾呢?
讨论同一性问题的良好起点是莱布尼茨的同一无别原理(也称为同一性判据或无别性原理)。17世纪和18世纪的哲学家兼数学家莱布尼茨断言,“同一性导致属性的无别性”。也就是说,如果对象A与对象B相同,那么A的每个属性都与B的相同属性相同,因此,如果两个对象在任何属性上(不)可区分,即使是在它们在空间中的位置属性上,那么它们(不)是不同的。
基于这个原理,我们可以说,目前在这个房间里的桌子与在另一个房间里的桌子不相同(这两张桌子至少在一个属性上不同——它们在空间中的位置)。我们也可以断言我办公室里的桌子与我最喜欢的桌子是相同的(它们是两个具有相同属性的对象,因此它们是同一个)。但这个原理能让我们确定同一对象在不同时间点的两个实例是相同的吗?根据众所周知的杯子例子,似乎并非如此。假设我手里拿着一个带把手的杯子,突然,把手可能已经松了,掉了下来。现在我手里拿着一个没有把手的杯子(看来我没抓住把手的时候它掉了……)。我们说的是同一个杯子吗?失去把手之前的杯子和失去把手之后的杯子是同一个吗?直观上,我们会认为确实如此。它掉把手时我都没有松开它。但根据莱布尼茨的同一性原理,这是两个不同的杯子,因为它们至少在一个属性上不同(把手的存在)。
试图解决莱布尼茨的同一性原理与直观感知之间的矛盾,最终导致了两种截然不同的哲学方法(这两种方法都是大卫·刘易斯在1986年提出的,但此后,每种方法都获得了一小批热情的支持者)。
- 碎化论方法——根据碎化论方法,可以将对象的某些属性视为时间属性,即其值会随时间变化的属性(例如,一个人的地址是一个很可能在他一生中改变的常见属性,与像一个人生物学母亲的身份这样的属性相反,其值将保持稳定)。时间属性值变化会导致创建新的实例,代表对象的新状态,该新实例通过一种特殊同一性与前一个实例(在其时间属性值变化之前)相关联。这种同一性基于我们指出导致该时间属性值变化的过程的能力,而该过程最终触发了新实例的创建。因此,杯子把手存在属性,这可能是一个时间属性,确实会导致两个不同实例的创建(第一个——带把手,第二个——无把手)——正如同一无别原理所暗示的那样。但另一方面,这些实例并不是真正两个独立的同一性(正如我们的直觉所暗示的),它们通过一种同一性关系相互关联,基于一个共同的同一性核心,因为我们可以回忆起杯子把手掉落的事件。
实际上,根据碎化论方法,我们可以将沿着时间轴的单个对象视为一系列实例,这些实例因其时间属性值而彼此不同,但都通过导致这些变化的进程(例如杯子把手掉落的过程)相互关联。
图 4:杯子掉了把手(碎化论) - 固存论方法——固存论方法认为,在任何给定时间,对象都包含其过去和未来。也就是说,对象某个时间属性的特定值本身并不独立存在;相反,它总是带有一个时间戳,因此,我们不能谈论时间属性的单个值,而只能谈论一个集合值。这个集合中的每个值都位于一条连续的时间线上,从对象创建的那一刻开始,到其终止为止。因此,杯子把手存在属性将始终由两个值组成:从杯子创建到把手掉落的时间线上延伸的“带把手”的值,以及第二个值——“无把手”,从把手掉落的时间开始并继续延伸。
根据这种方法,如果我们比较这两个所谓的实例(把手掉落之前和之后),我们会发现它们所谓的无同一性是误导性的,因为在每个时间点,“把手存在”属性的值对于这两个实例来说实际上是相同的,因为,正如固存论方法所暗示的那样,时间属性总是包含其过去和未来的所有值。因此,杯子把手的掉落并没有与同一无别原理相矛盾。
图 5——杯子掉了把手(固存论)
在这两种方法中,固存论方法似乎不容易理解。直观的假设是,如果某个对象的某个可测量属性值发生变化,那么该对象在变化之前将被感知为与变化之后的对象不同。因此,这些对象实例可能具有相同的同一性,但它们肯定不会被感知为相同(正如固存论方法所暗示的那样)。以下示例展示了这种矛盾——假设一个人在 t0 时体重为 80 公斤,在 t1 时体重为 81 公斤。在这两个时间点,我们肯定会以不同的方式感知这个人。然而,对象本身(人)不应该改变(这是固存论方法所声称的)。
固存论方法的支持者试图通过提供一种新的理解方式来解决这个问题,即我们如何感知对象的某个实例。他们声称,感知对象的体验基于这样一个事实:对象在每个给定时间都揭示相关的“时间间隔”–“体重测量”对;因此,实例可能显得不同,但实际上并非如此。
图 6 展示了根据固存论方法,属性及其值如何包含在对象中。如前所述,每个属性实际上是一组“时间间隔”–“测量值”对,这使得在不同时间出现不同实例成为可能。例如,一个在相交间隔 P1 1、P2 1 和 P3 1 期间发生的实例将与一个在相交时期 P1 1、P2 1 和 P3 2 期间发生的实例不同,但这两个实例都指向同一个对象。
当前基于哲学模式
由于上述哲学讨论的目的是寻找一种在对象内部维护其历史的方法,我们现在应该问自己,面向对象软件与讨论的理论有何关系,或者换句话说,在当前软件应用程序的上下文中,我们如何看待对象随时间的同一性?
今天定义对象属性的方式(作为类中的数据成员)似乎很大程度上受到了碎化论方法的影响。为了检验这一假设,让我们实现“掉把手的杯子”的例子,看看代码是如何工作的。
当这段代码运行时,在 t0 时,一个带把手的 Cup
类实例将被实例化到计算机内存中。在 t1 时,调用 RemoveHandle
方法后,该实例(相同的内存位置,这里作为同一性核心)将发生改变,并且该内存将包含一个稍微不同的对象(无把手),而不会留下杯子先前状态(带把手)的痕迹。这正是碎化论方法描述的过程——对象的时间属性在对象同一性的上下文中被改变,而不会留下先前状态或值的痕迹。
建议的基于哲学模式
然而,固存论方法可能更适合我们正在寻找的东西——固存论方法提供了一种在对象内部维护整个对象历史(在时间属性变化方面)的方法,因此,我们可以使用这种方法作为我们建议的基于哲学模式的模型。
根据固存论方法,属性(参数)与其值之间没有直接关系,而是属性(参数)与“时间间隔”–“值”对的列表之间的关系。这意味着,当我们激活使用此类参数的对象方法(根据固存论方法实现)时,或者当我们直接访问此类参数时,我们必须清楚与请求相关的时间间隔。
如UML图所示,历史管理参数(HistoryManagedParam
)的本质是TimePeriodValuePair
对象的列表,其中每个TimePeriodValuePair
实际上是ParamValue
对象(包含值本身)和DateTime
对象(指示其配对项(值)有效的日期)的对。参数对象本身可以附加到任何类型的对象(图中的HistoryManagedObject
)。
当需要为特定参数创建新值时(例如,某人在2010年1月1日体重为70公斤,2010年1月2日为71公斤),我们使用HistoryManagedParam.AddValueByPeriod
方法来添加另一个ParamValue
-DateTime
对到HistoryManagedParam
的对列表中。
当需要执行涉及此类参数的特定任务时,我们通过使用HistoryManagedParam.GetValueByPeriod
方法来获取相关值,该方法返回根据输入的时间相关的ParamValue
。
实现示例
我们使用一个应用程序,该应用程序允许一家家具制造商为其产品生成基于日期的价格列表,以演示建议的参数历史管理解决方案。
该应用程序根据家具的类型(例如桌子、椅子等)及其子类型(例如,桌子子类型将是写字台、餐桌等)来分类家具,并且当然也允许用户添加、编辑或删除家具。
这里有趣的是Furniture对象的price数据成员,它在这里被定义为HistoryManagedParam对象。这使得应用程序能够管理每件家具的价格历史。
因此,建议模型提供的信息足以进行诸如显示基于时间的价目表(点击“价目表”按钮)之类的操作,而无需进一步构建任何数据结构(如果我们没有使用这样的历史管理模型)。
初始化
当应用程序执行时,充当应用程序管理器/协调员的DataManager
对象,根据从存储库(XML文件——Furnitures.xml)获取的数据初始化域对象。
如上所述,每个域对象(家具)都包含一个类型为HistoryManagedParam
的价格参数,此外,每个类还有一个其他参数的集合,这些参数的值在对象历史中是稳定的。这类参数将是家具的目录号或家具的设计师姓名。以下是Furniture
类中定义的、对所有家具项通用的参数。
添加新价格
当新价格值即将添加到某个家具项目的价格历史列表中时(当对象初始化或编辑时发生),我们使用Price
的AddValueByPeriod
成员函数来完成此任务。如前所述,该方法将值和日期对作为输入,相应地构建一个TimePeriodValuePair
,并将其(以排序方式添加,以便于按日期检索)添加到参数的历史列表中。
用法
当查询Furniture类型的对象在特定日期的价格时,它通过激活Price
数据成员的GetValueByPeriod
函数来返回一个答案。
该函数接收所需的日期作为参数,并返回该日期的相应值(如果找不到匹配的日期,则返回null
)。
注释
- 历史机制使我们能够从特定日期开始向对象添加参数,这样在更早的日期,对象实际上缺少该参数。
- 对于作为对象不可分割的一部分的参数,因此其生命周期与对象的生命周期相同,我们应该添加一种机制,以确保参数的生命周期与对象的生命周期相匹配。