Skip to content

Latest commit

 

History

History
1668 lines (1336 loc) · 81.2 KB

File metadata and controls

1668 lines (1336 loc) · 81.2 KB

八、创建业务对象

在检查第 7 章hms_sys的逻辑架构*设置项目和流程时,*在整个系统范围内出现了一些常见的业务对象类型:

上图中显示的对象解释如下:

  • 一个Artisan对象表示一个Artisan——创建要销售的产品项目并通过系统将这些产品提供给 HMS 中央办公室的最终用户。Artisan被收集到中央办公室的数据结构中,并且在一定程度上可以由中央办公室工作人员管理,但他们的大部分实际数据需要由单个工匠自己拥有和管理;这样一来,他们就可以尽可能多地控制自己的信息,而且如果中央办公室的员工更改了地址,或者想要添加或更改公司名称,他们就不会为工匠管理数据更改。
  • 产品是一个物理对象的表示,是一个工匠创造的用于销售的东西。
  • 订单是客户通过 HMS 网络商店订购产品的结果。

这三种对象类型还推断出两种之前未调用的其他对象类型:

  • 客户,表示下了订单的实际客户,可以附加到一个或多个订单
  • 一个地址代表一个物理位置,可以将某物运送到或从该物理位置运出,也可以附加到一个或多个订单,它可能是客户的财产,几乎可以肯定是工匠的财产

本章将介绍这些对象作为公共类库的实现,应用程序和服务项目的代码可以利用这些类库,包括将其转化为可部署包的设计、实现、自动测试和构建过程。

本章包括以下内容:

  • 迭代目标
  • 故事和任务的集合
  • 快速复习课程
  • 实现hms_sys中的基本业务对象
  • 测试业务对象
  • 分配和安装注意事项
  • 质量保证和验收
  • 操作/使用、维护和退役注意事项

迭代目标

因此,此迭代的可交付成果是一个类库,可以与实际项目、用户应用程序和服务的包和代码一起安装或合并,以提供这些业务对象的通用表示结构:

  • hms_core包/库
  • 单元测试
  • 能够作为一个独立的组件建造
  • 包括提供以下基线表示形式的基类:

故事和任务的集合

由于 business objects 软件包的组件旨在供系统中的其他软件包使用,因此大多数相关故事仍然侧重于提供开发人员需要的东西:

  • 作为一名开发人员,我需要一个通用的定义和功能结构来表示系统中的地址,以便我可以将它们合并到系统中需要它们的部分:

    • 定义一个BaseAddress抽象基类ABC
    • 实施BaseAddress作业成本法
    • 单元测试BaseAddressABC
  • 作为一名开发人员,我需要一个通用的定义和功能结构来表示系统中的工匠,这样我就可以将他们合并到系统中需要他们的部分中:

    • 定义一个BaseArtisanABC
    • 实施BaseArtisan作业成本法
    • 单元测试BaseArtisanABC
  • 作为一名开发人员,我需要一个通用的定义和功能结构来表示系统中的客户,这样我就可以将他们合并到系统中需要他们的部分中:

    • 定义一个BaseCustomerABC
    • 实施BaseCustomer作业成本法
    • 单元测试BaseCustomerABC
  • 作为一名开发人员,我需要一个通用的定义和功能结构来表示系统中的订单,以便我可以将它们合并到系统中需要它们的部分:

    • 定义一个BaseOrderABC
    • 实施BaseOrder作业成本法
    • 单元测试BaseOrderABC
  • 作为一名开发人员,我需要一个通用的定义和功能结构来表示系统中的产品,以便我可以将它们合并到系统中需要它们的部分:

    • 定义一个BaseProductABC
    • 实施BaseProduct作业成本法
    • 单元测试BaseProductABC
  • 作为一名技工,我需要随我的应用程序一起安装 business objects library,这样应用程序就可以根据需要工作,而无需为其安装相关组件:

    • 确定基于setup.py的打包是否可以包含来自本地项目结构外部的打包,如果可以,则实施
    • 否则,实施基于Makefile的流程,将hms_core包含在其他项目的包装流程中
  • 作为一名 Central Office 用户,我需要将 business objects library 与我的应用程序一起安装,以便应用程序能够根据需要工作,而无需安装依赖于它的组件:

    • 验证Artisan包装/安装流程是否也适用于中央办公室安装
  • 作为系统管理员,我需要使用Artisan网关服务安装 business objects library,这样它就可以根据需要工作,而无需安装依赖于它的组件:

    • 验证Artisan包装/安装过程是否也适用于Artisan网关安装

值得注意的是,虽然此设计从定义许多抽象类开始,但这并不是它可以采用的唯一方法。另一个可行的选择是从其他每个库中的简单具体类开始,然后提取这些库中的公共需求,并创建 ABC 来强制执行这些需求。这种方法将更快地产生具体的功能,同时将结构和数据标准降低到更晚的水平,并且需要将相当多的代码从具体的类移回 ABC,但这仍然是一种可行的选择。

快速复习课程

在任何面向对象的语言中,类都可以被看作是创建对象的蓝图,定义这些对象(作为类的实例)是什么、拥有什么和可以做什么。类通常表示现实世界中的对象,无论是人、地点还是事物,但即使它们不表示,它们也提供了一组简洁的数据和符合逻辑概念单元的功能。

随着hms_sys开发的进展,将设计和实现几个类,包括具体类和抽象类。在大多数情况下,设计将从类图开始——一个一对多类的图形,显示每个类的结构以及它们之间的任何关系:

一个具体类将被实例化,以便根据它提供的蓝图创建对象实例。抽象类为具有特定类成员(具体或抽象)的对象提供基线功能、接口要求和类型标识,这些成员将由派生类继承,或需要在派生类中实现。这些成员的范围,包括属性方法,由**+表示公共成员,-表示私人成员,以及表示受公约保护的成员,尽管如前所述,Python 没有真正的受保护或私有成员。尽管如此,这些至少提供了一些关于成员的预期范围的指示。**

**# 在 hms_ 系统中实现基本业务对象

在开发过程的这一点上,我们只是不知道是否所有业务对象类的完全相同的功能将在将要构建的两个应用程序和服务中发挥作用。确定用户可以在对象中创建、更新或删除哪些数据的数据所有权规则尚未详细到足以做出这些决定。然而,我们确实有足够的信息,仅仅基于这些对象的目的,来开始定义它们代表什么数据,以及这些数据点周围应该存在什么约束。

我们现在可能有足够的信息来了解这些对象类型中的某些功能需要存在,Artisan对象需要能够添加和删除相关的产品对象,例如,即使我们还不知道如何工作,或者是否有关于这些的数据所有权规则。我们还可以对哪些类需要抽象进行一些有根据的猜测(因为它们的实际实现在应用程序和服务之间会有所不同)。

住址

Address类表示一个物理位置——一个可以邮寄或装运某物的地方,或者可以在地图上找到的地方。无论对象在什么上下文中遇到,地址的属性都是一致的,也就是说,地址是一个地址,无论它与工匠客户订单关联-在这一点上,我们可以安全地假设,任何地址的全部都可以被它所属的对象更改,或者任何地址都不能更改。在这一点上,除非有相反的信息,否则不需要在后端数据结构中将地址作为单独的项存储;尽管他们有可能拥有自己有意义的独立存在,但没有理由认为他们会。

考虑到这一点,让地址成为一个抽象类并不觉得有必要,至少现在还没有:

地址是一个哑数据对象,至少到目前为止是这样;它由数据结构组成,但没有方法或功能。类本身的属性非常简单,并且有一些规则:

  • street_address是该地点的街道地址。它应该是单行字符串值,是必需的(不能为空),并且可能不允许使用空格以外的任何空白字符。street_address的示例值为1234 Main Street

  • building_address是地址的可选第二行,用于指示街道地址实际位置的更多细节。示例可能包括公寓号码、套房或办公室位置或号码等。如果它出现在任何给定的地址中,它应该是一个字符串值,具有与street_address相同的约束,但它还是一个可选值。

  • city是必需的字符串值,也限制为一行,与street_address的空格规则相同。

  • region是一个可选的字符串值,具有与postal_codecountry相同的约束,至少目前是这样。

如果没有某种特定于国家的背景,这最后三个属性很难生成规则。有些国家的地址可能没有地区或邮政编码,而另一些国家的地址则有完全不同的名称和数据要求,尽管这似乎不太可能。举例来说,考虑到在美国,区域和 PoT T0 代表的是 Ty1 T1 状态,Ty2 T3,Zip Po.T4,Ty5 T5 代码,代码 To6 T6(五个数字,可选的 DASH 和四个数字),而在加拿大,它们将代表一个地区或省份和一个字母数字的邮政编码。在各个国家的基础上,可能会有一个针对要求某些方面的解决方案,这将在考虑初始属性定义后进行检查。

Address的初始实现非常简单;我们首先定义一个具有可用属性的类:

class Address:
    """
Represents a physical mailing-address/location
"""
    ###################################
    # Class attributes/constants      #
    ###################################

# ... removed for brevity

    ###################################
    # Instance property definitions   #
    ###################################

    building_address = property(
        _get_building_address, _set_building_address, 
        _del_building_address, 
        'Gets, sets or deletes the building_address (str|None) '
        'of the instance'
    )
    city = property(
        _get_city, _set_city, _del_city, 
        'Gets, sets or deletes the city (str) of the instance'
    )
    country = property(
        _get_country, _set_country, _del_country, 
        'Gets, sets or deletes the country (str|None) of the '
        'instance'
    )
    region = property(
        _get_region, _set_region, _del_region, 
        'Gets, sets or deletes the region (str|None) of the '
        'instance'
    )
    postal_code = property(
        _get_postal_code, _set_postal_code, _del_postal_code, 
        'Gets, sets or deletes the postal_code (str|None) of '
        'the instance'
    )
    street_address = property(
        _get_street_address, _set_street_address, 
        _del_street_address, 
        'Gets, sets or deletes the street_address (str) of the '
        'instance'
    )

这些property调用中的每一个都指定了一个 getter、setter 和 deleter 方法,然后必须实现这些方法。getter 方法都非常简单,每个方法都返回存储该属性实例数据的关联属性值:

    ###################################
    # Property-getter methods         #
    ###################################

    def _get_building_address(self) -> (str,None):
        return self._building_address

    def _get_city(self) -> str:
        return self._city

    def _get_country(self) -> (str,None):
        return self._country

    def _get_region(self) -> (str,None):
        return self._region

    def _get_postal_code(self) -> (str,None):
        return self._postal_code

    def _get_street_address(self) -> str:
        return self._street_address

setter 方法也相对简单,不过为了执行前面提到的类型和值规则,必须实现一些逻辑。到目前为止,Address 的属性分为两类:

  • 必填、非空、单行字符串(如street_address

  • 可选(None或非空、单行字符串值(building_address

所需值的实现都将遵循相同的模式,以street_address为例:

    def _set_street_address(self, value:str) -> None:
        # - Type-check: This is a required str value
        if type(value) != str:
            raise TypeError(
                '%s.street_address expects a single-line, '
                'non-empty str value, with no whitespace '
                'other than spaces, but was passed '
                '"%s" (%s)' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        # - Value-check: no whitespace other than " "
        bad_chars = ('\n', '\r', '\t')
        is_valid = True
        for bad_char in bad_chars:
            if bad_char in value:
                is_valid = False
                break
        # - If it's empty or otherwise not valid, raise error
        if not value.strip() or not is_valid:
            raise ValueError(
                '%s.street_address expects a single-line, '
                'non-empty str value, with no whitespace '
                'other than spaces, but was passed '
                '"%s" (%s)' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        # - Everything checks out, so set the attribute
        self._street_address = value

然后,setter 方法从开始到结束的过程如下:

  1. 确保提交的valuestr类型,如果不是,则引发TypeError

  2. 创建一个禁止字符列表,包括换行符、回车符和制表符('\n''\r''\t')——该值中不允许使用这些字符

  3. 假设该值在另行确定之前有效(is_valid = True

  4. 检查值中是否存在这些错误字符,如果存在,则将该值标记为无效

  5. 检查值是否仅为空白(value.strip())或是否发现任何无效字符,如果是,则引发ValueError

  6. 如果未引发错误,请将属性的内部存储属性设置为现在已验证的值(self._street_address = value

此代码将street_address更改为city,负责城市属性的 setter 实现。在这个迭代和随后的迭代中,这个属性设置程序流程/流将重复出现。从现在开始使用时,它将被称为标准的必需文本行属性设置器。

可选属性使用非常相似的结构,但首先检查(并允许)一个None值,因为将其值设置为None在技术上是有效的/允许的。building_address属性设置器作为此过程的一个示例:

    def _set_building_address(self, value:(str,None)) -> None:
        if value != None:
            # - Type-check: If the value isn't None, then it has to 
            #   be a non-empty, single-line string without tabs
            if type(value) != str:
                raise TypeError(
                    '%s.building_address expects a single-line, '
                    'non-empty str value, with no whitespace '
                    'other than spaces or None, but was passed '
                    '"%s" (%s)' % 
                    (
                        self.__class__.__name__, value, 
                        type(value).__name__
                    )
                )
            # - Value-check: no whitespace other than " "
            bad_chars = ('\n', '\r', '\t')
            is_valid = True
            for bad_char in bad_chars:
                if bad_char in value:
                    is_valid = False
                    break
            # - If it's empty or otherwise not valid, raise error
            if not value.strip() or not is_valid:
                raise ValueError(
                    '%s.building_address expects a single-line, '
                    'non-empty str value, with no whitespace '
                    'other than spaces or None, but was passed '
                    '"%s" (%s)' % 
                    (
                        self.__class__.__name__, value, 
                        type(value).__name__
                    )
                )
            # - If this point is reached without error, then the 
            #   string-value is valid, so we can just exit the if
        self._building_address = value

此 setter 方法过程与前面的标准 required 文本行属性一样,将以一定频率出现,并将被称为标准可选文本行属性 setter。

deleter 方法也将非常简单所有这些属性,如果被删除,可以设置为None的值,这样它们仍然有一个值(从而避免AttributeError的实例,如果它们在别处被引用),但是一个可以用来指示没有值的属性:

    def _del_building_address(self) -> None:
        self._building_address = None

    def _del_city(self) -> None:
        self._city = None

    def _del_country(self) -> None:
        self._country = None

    def _del_region(self) -> None:
        self._region = None

    def _del_postal_code(self) -> None:
        self._postal_code = None

    def _del_street_address(self) -> None:
        self._street_address = None

在定义了属性定义及其底层方法之后,使类可用的剩下的就是它的__init__方法的定义,这样Address实例的创建就可以实际接受并存储相关属性。

坚持简单的结构是很有诱惑力的,不同的地址元素按照通常使用的顺序被接受和要求,比如:

    def __init__(self, 
        street_address,                  # 1234 Main Street
        building_address,                # Apartment 3.14
        city, region, postal_code,       # Some Town, ST, 00000
        country                          # Country. Maybe.
        ):

另一种同样有效的方法是允许将转换为所创建实例的可选属性的参数使用默认值:

    def __init__(self, 
        street_address,                  # 1234 Main Street
        city,                            # Some Town
        building_address=None,           # Apartment 3.14
        region=None, postal_code=None,   # ST, 00000
        country=None                     # Country
        ):

从函数的角度来看,这两种方法都是完全有效的。使用其中一种方法可以创建一个Address实例,但第一种方法可能更容易理解,而第二种方法可以创建一个最小实例,而无需每次都指定每个参数值。决定使用哪种论证结构可能需要认真考虑各种因素,包括:

  • 谁将创建新的Address实例?
  • 那些Address创建过程是什么样子的?
  • 何时何地需要新的Address实例?
  • 如何创建它们?也就是说,流程周围是否会有某种具有一致性的 UI?

世卫组织的问题有一个非常简单的答案,其中一个主要回答了其他问题:几乎任何用户都可能需要能够创建一个新地址。中央办公室工作人员可能正在建立新的Artisan账户。工匠有时可能需要更改地址。客户虽然只是间接地,但在下第一批订单时需要创建发货地址,并且可能需要创建独立于其默认/账单地址的发货地址。即使是Artisan网关服务也可能需要创建Address实例,作为处理来回数据移动过程的一部分。

不过,在大多数情况下,都会涉及到某种类型的 UI:用于客户订单相关项目的 web 商店表单,以及Artisan和中央办公室应用程序中的任何 GUI。由于 UI 位于地址创建过程的顶部,将参数从该 UI 传递到__init__的责任只对开发人员来说是重要的或关注的。因此,这些问题,虽然它们阐明了函数需要是什么,但对于在两种论证形式的可能性之间做出选择,确实没有多大帮助。

也就是说,__init__没有理由不能以一种方式定义,而为 Address 创建的另一种方法可能允许另一种结构 astandard_address

    @classmethod
    def standard_address(cls, 
            street_address:(str,), building_address:(str,None), 
            city:(str,), region:(str,None), postal_code:(str,None), 
            country:(str,None)
        ):
        return cls(
            street_address, city, building_address, 
            region, postal_code, country
        )

然后允许__init__使用利用各种默认参数值的结构:

def __init__(self, 
    street_address:(str,), city:(str,), 
    building_address:(str,None)=None, region:(str,None)=None, 
    postal_code:(str,None)=None, country:(str,None)=None
    ):
    """
Object initialization.

self .............. (Address instance, required) The instance to 
                    execute against
street_address .... (str, required) The base street-address of the 
                    location the instance represents
city .............. (str, required) The city portion of the street-
                    address that the instance represents
building_address .. (str, optional, defaults to None) The second 
                    line of the street address the instance represents, 
                    if applicable
region ............ (str, optional, defaults to None) The region 
                    (state, territory, etc.) portion of the street-
                    address that the instance represents
postal_code ....... (str, optional, defaults to None) The postal-code 
                    portion of the street-address that the instance 
                    represents
country ........... (str, optional, defaults to None) The country 
                    portion of the street-address that the instance 
                    represents
"""
    # - Set default instance property-values using _del_... methods
    self._del_building_address()
    self._del_city()
    self._del_country()
    self._del_postal_code()
    self._del_region()
    self._del_street_address()
    # - Set instance property-values from arguments using 
    #   _set_... methods
    self._set_street_address(street_address)
    self._set_city(city)
    if building_address:
        self._set_building_address(building_address)
    if region:
        self._set_region(region)
    if postal_code:
        self._set_postal_code(postal_code)
    if country:
        self._set_country(country)

这使得Address在功能上是完整的,至少就本次迭代中有关它的故事而言。

由于任何类都在进行开发,很可能会围绕开发人员设想的用例出现问题,或者只是在考虑类如何工作的某些方面时出现问题。Address被充实时出现的一些例子如下:

  • 如果在实例中删除非默认属性值,会发生什么情况?如果删除了一个必需的值,那么实例就不再是格式良好的,并且在技术上是无效的,因此是否可以执行这样的删除?

  • 有一个 Python 模块pycountry,它收集 ISO 派生的国家和地区信息。是否有必要尝试利用这些数据,以确保国家/地区组合是现实的?

  • Address最终是否需要任何类型的输出功能?例如,标签文本?或者可以在 CSV 文件中生成行?

这样的问题可能值得留到某个地方,即使它们从来都不相关。如果没有某种项目系统存储库来存储这些东西,或者开发团队中没有某种过程来保存它们,这样它们就不会丢失,那么它们总是可以作为某种注释添加到代码本身中,比如:

# TODO: Consider whether Address needs some sort of #validation 
#       mechanism that can leverage pycountry to assure #that 
#       county/region combinations are kosher.
#       pycountry.countries—collection of countries
#       pycountry.subdivisions—collection of regions by #country
# TODO: Maybe we need some sort of export-mechanism? Or a 
#       label-ready output?
# TODO: Consider what can/should happen if a non-default #property-
#       value is deleted in an instance. If a required #value is 
#       deleted, the instance is no longer well-formed...
class Address:
    """
#Represents a physical mailing-address/location
"""

棒球手

Artisan类代表参与手工制品市场的工匠,即创建可通过中央办公室网络商店销售的产品的人。知道每个不同用户与最终Artisan类的交互几乎肯定会有不同的功能规则,在hms_core代码库中创建一个抽象类是有意义的,它定义了其他包中任何具体Artisan的通用功能和需求。我们将为该类命名为BaseArtisan

与我们刚刚完成的Address类一样,BaseArtisan的设计和实现从类图开始:

It's not unusual for abstract classes to have a naming convention that indicates that they are abstract. In this case, the prefix of Base is that indicator, and will be used for other abstract classes as development progresses.

BaseArtisan旨在为系统任何部分中与任何工匠相关的所有属性提供一套通用的状态数据规则和功能。那么,属性本身就是具体的实现。BaseArtisan还旨在以add_productremove_product方法的形式提供一些(最低)功能需求。由于工匠和产品相互关联,一个具体的Artisan对象需要能够添加和删除Product对象,这是一个已知的事实,但是关于这些过程如何工作的细节在使用该功能的两个应用程序和服务之间可能会有所不同,因此,它们是抽象的,需要在从BaseArtisan派生的任何类中重写/实现。

这个类图还包括前面创建的Address类,两个类之间有一个菱形端连接器。该连接表示Address类被用作BaseArtisan的聚合属性,即BaseArtisan的 address 属性是Address的实例。这也在 address 属性本身中指示,并指定一个<Address>作为 address 属性的类型。简单来说,一个BaseArtisan有一个Address

也可以将BaseArtisan定义为继承自Address。除连接器外,该关系的类图几乎相同,如下所示:

在这种关系中,BaseArtisan是一个Address——它将具有Address的所有属性,以及可能添加的任何方法成员。这两种关系都是完全合法的,但使用聚合(或组合)方法优于依赖继承,这在继续实现BaseArtisan之前值得注意。

OO 原则——组合而非继承

这些优点中最明显的可能是结构易于理解。一个Artisan实例将有一个地址属性,它是另一个对象,并且该对象有它自己的相关属性。在Artisan级别,只有一个重要的地址,这似乎并不重要。然而,其他对象,如CustomerOrder可能有多个关联地址(例如账单和发货地址),甚至多个:Customer可能有多个需要保留并可用的发货地址。

随着系统的对象库变得越来越大、越来越复杂,使用纯粹基于继承的设计方法将不可避免地产生大量的类树,其中许多类可能仅仅是为了被继承而提供功能。基于组合的设计将降低这种复杂性,在更大、更复杂的库中可能更为明显,因为功能将封装在单个类中,这些类的实例本身就是属性。

尽管如此,这种组合也有一些潜在的缺点:深度嵌套的对象、属性的属性和令人厌恶的属性,可能导致数据结构的长链。例如,如果hms_sys上下文中的order有一个customer,而该customer又有一个shipping_address,那么从顺序中查找该地址的postal_code看起来类似于order.customer.shipping_address.postal_code。这不是一个非常深入或复杂的获取数据的路径,而且因为属性名称很容易理解,所以理解整个路径并不困难。与此同时,不难想象这种嵌套会失去控制,或者依赖于不易理解的名称。

还可能(可能)需要类提供某些组合属性类方法的本地实现,这增加了父对象类的复杂性。举例来说,假设刚才提到的shipping_address的 address 类有一个方法,该方法检查各种装运 API,并返回从最低成本到最高成本排序的 API 列表。调用find_best_shipping。如果需要order对象能够使用该功能,那么最终可能会在 order 类级别定义一个find_best_shipping方法,该方法调用 address 级别的方法并返回相关数据。

然而,这两个都不是显著的缺点。如果在确保设计符合逻辑、易于理解、成员名称有意义方面有一定的原则,那么它们可能只会单调乏味。

从更纯粹、面向对象的角度来看,一个更重要的问题是钻石问题。考虑下面的代码:

class Root:
    def method(self, arg, *args, **kwargs):
        print('Root.method(%s, %s, %s)' % (arg, str(args), kwargs))

class Left(Root):
    def method(self, arg, *args, **kwargs):
        print('Left.method(%s, %s, %s)' % (arg, str(args), kwargs))

class Right(Root):
    def method(self, arg, *args, **kwargs):
        print('Right.method(%s, %s, %s)' % (arg, str(args), kwargs))

class Bottom(Left, Right):
    pass

b = Bottom()

如图所示,这些类形成钻石形状,因此钻石问题的名称为:

执行以下操作时会发生什么情况:

b.method('arg', 'args1', 'args2', keyword='value')

将调用哪个方法?除非语言本身定义了如何解决歧义,否则唯一可以安全地假设的是不会调用Root的方法,因为LeftRight类都会覆盖它。

Python 通过使用类定义中指定的继承顺序作为方法解析顺序MRO)来解决这种性质的歧义。在这种情况下,因为Bottom被定义为从LeftRight-class Bottom(Left, Right)继承,这是用于确定实际执行几个可用的method中的哪一个的顺序:

# Outputs "Left.method(arg, ('args1', 'args2'), {'keyword': 'value'})"

尽管任何可安装的hms_sys组件都不太可能达到继承问题将成为一个重大问题的复杂程度,但无法保证它永远不会发生。考虑到这一点,并且从基于继承的结构到基于组合的结构的重构工作可能既痛苦又容易引入破坏性的更改,基于组合的方法,即使有一些固有的缺点,在这一点上仍然是一种更好的设计。

实现 BaseArtisan 的属性

为了将一名工匠作为一个人(也可能有一个公司名称)代表,并提供一个地点和产品,BaseArtisan提供了六名财产成员:

  • contact_name工匠的联系人姓名。如前所述,它应该是标准的必需文本行属性。
  • contact_emailcontact_name中所述人员的电子邮件地址。它应该是一个格式良好的电子邮件地址,并将是必需的。
  • company_name是标准的可选文本行属性(可选,因为并非所有工匠都有公司名称)。
  • address将是必需的,并且将是Address的实例。
  • website工匠的可选网站地址。如果它存在,它将需要一个格式良好的 URL。
  • products将是BaseProduct对象的集合,与address是单个Address实例的方式大致相同。有关产品的一些实施细节将推迟到BaseProduct完全定义之后。

与前面一样,该过程从创建类开始,然后定义属性,这些属性的实现将在下一步得到充实:

class BaseArtisan(metaclass=abc.ABCMeta):
    """
Provides baseline functionality, interface requirements, and 
type-identity for objects that can represent an Artisan in 
the context of the HMS system.
"""

metaclass=abc.ABCMeta的加入使用abc模块的ABCMeta功能,将BaseArtisan定义为一个抽象基类:

    ###################################
    # Instance property definitions   #
    ###################################

    address = property(
        _get_address, _set_address, _del_address, 
        'Gets, sets or deletes the physical address (Address) '
        'associated with the Artisan that the instance represents'
    )
    company_name = property(
        _get_company_name, _set_company_name, _del_company_name, 
        'Gets, sets or deletes the company name (str) associated '
        'with the Artisan that the instance represents'
    )
    contact_email = property(
        _get_contact_email, _set_contact_email, _del_contact_email, 
        'Gets, sets or deletes the email address (str) of the '
        'named contact associated with the Artisan that the '
        'instance represents'
    )
    contact_name = property(
        _get_contact_name, _set_contact_name, _del_contact_name, 
        'Gets, sets or deletes the name of the contact (str) '
        'associated with the Artisan that the instance represents'
    )
    products = property(
        _get_products, None, None, 
        'Gets the collection of products (BaseProduct) associated '
        'with the Artisan that the instance represents'
    )
    website = property(
        _get_website, _set_website, _del_website, 
        'Gets, sets or deletes the URL of the website (str) '
        'associated with the Artisan that the instance represents'
    )

由于company_namecontact_name是标准的可选和必需的文本行实现,正如在创建Address类中所描述的,它们的实现将遵循此处建立的模式,并且不会进行任何详细的检查。这两种方法的过程与Address.building_addressAddress.street_address的过程相同,分别只会更改 getter、setter 和 deleter 方法的名称以及存储属性值的状态数据属性。

类似地,与除产品外的所有属性相关联的_get__del_方法将遵循已经建立的相同基本模式:

  • Getter 方法将只返回存储在相应的 state 存储属性中的值
  • Deleter 方法将相应的状态存储属性的值设置为None

例如,addresscompany_namecontact_email的 getter 和 deleter 方法实现可以是与前面所示完全相同的过程,即使address不是简单的值属性,并且contact_email尚未实现:

    def _get_address(self) -> (Address,):
        return self._address

    def _del_address(self) -> None:
        self._address = None

    def _get_company_name(self) -> (str,None):
        return self._company_name

    def _del_company_name(self) -> None:
        self._company_name = None

    def _get_contact_email(self) -> (str,None):
        return self._contact_email

    def _del_contact_email(self) -> None:
        self._contact_email = None

这可能感觉像是很多样板、复制和粘贴代码,但这是能够执行 setter 方法处理的类型和值检查的成本。setter 方法本身就是保持所需的高度数据类型和完整性的神奇之处。

address属性的 setter 可能非常简单,因为真正需要强制执行的是传递给它的任何值都必须是Address类的实例。没有值检查,因为任何成功创建的Address实例都将在初始化过程中执行自己的类型和值检查:

    def _set_address(self, value:Address) -> None:
        if not isinstance(value, Address):
            raise TypeError(
                '%s.address expects an Address object or an object '
                'derived from Address, but was passed "%s" (%s) '
                'instead, which is not.' %
                (value, type(value).__name__)
            )
        self._address = value

contact_email设置程序的工作原理与Address._set_street_address中定义的标准文本行设置程序非常相似。它关联了一些相同的数据规则,毕竟它是一个必需的值,不能为空,而且因为它是一个电子邮件地址,所以不能是多行或有选项卡。由于它是一个电子邮件地址,但它也不能有空格,还有其他字符限制,是所有电子邮件地址的共同点,而这些电子邮件地址在原始结构中没有考虑。由于属性的要求包括格式良好的电子邮件地址,因此可能有其他更好的方法来验证传递给 setter 的值。

Ideally, an application will want to assure that an email address is both well formed and valid. There's really only one way to do either, though, and it's out of scope for hms_sys, even if it makes sense to try and implement it: send a confirmation email, and don't store the value until/unless a confirmation response is received.

有许多方法可以帮助我们验证格式良好的电子邮件地址。一个可能是最好的开始,就是使用正则表达式来匹配该值,或者删除所有格式良好的电子邮件地址,并且不允许设置该值,除非在执行替换后没有剩余内容。使用正则表达式可能无法保证值的格式正确,尽管它会捕获许多无效值。将其与email.utils模块中的一些标准 Python 功能结合起来,至少可以使代码达到某种程度,在这种程度上可以构建测试,以查找失败的格式良好的地址,并允许修改检查过程。

首先,我们需要导入一些项,即来自email.utilsparseaddr函数和re模块,以便创建我们将用于测试的正则表达式对象。这些导入应发生在模块顶部:

#######################################
# Standard library imports needed     #
#######################################

import abc # This was already present
import re

from email.utils import parseaddr

接下来,我们将创建一个模块级常量正则表达式对象,用于检查电子邮件地址值:

EMAIL_CHECK = re.compile(
    r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)'
)

这将匹配以一个或多个字符AZ(大写或小写)、任何数字 0-9 或下划线、句点、加号或破折号开头的整个字符串,然后是@,然后是大多数域名。这个结构是通过快速搜索在互联网上找到的,可能不完整,但它看起来应该适用于大多数电子邮件地址。setter 方法实现现在需要做的就是检查值是否为字符串,从字符串中解析出一个可识别的地址,检查解析后的值,如果所有内容都已签出,则设置数据存储属性的值:

    def _set_contact_email(self, value:str) -> None:
        # - Type-check: This is a required str value
        if type(value) != str:
            raise TypeError(
                '%s.contact_email expects a str value that is a '
                'well-formed email address, but was passed '
                '"%s" (%s)' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        # - Since we know it's a string, we can start by parsing value 
        #   with email.utils.parseaddr, and using the second item of 
        #   that result to check for well-formed-ness
        check_value = parseaddr(value)[1]
        # - If value is not empty, then there was *something* that was
        #   recognized as being an email address
        valid = (check_value != '')
        if valid:
            # - Try removing an entire well-formed email address, as 
            #   defined by EMAIL_CHECK, from the value. If it works, 
            #   there will either be a remnant or not. If there is 
            #   a remnant, it's considered badly-formed.
            remnant = EMAIL_CHECK.sub('', check_value)
            if remnant != '' or not value:
                valid = False
        if not check_value or not valid:
            raise TypeError(
                '%s.contact_email expects a str value that is a '
                'well-formed email address, but was passed '
                '"%s" (%s)' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        self._contact_email = value

类似的方法应该是 Web setter 方法的良好起点,使用以下作为正则表达式进行测试:

URL_CHECK = re.compile(
    r'(^https?://[A-Za-z0-9][-_A-Za-z0-9]*\.[A-Za-z0-9][-_A-Za-z0-9\.]*$)'
)

它以在Address._set_building_address中建立的相同可选值检查开始,但使用URL_CHECK正则表达式对象来检查传递的值,方法与_set_contact_email基本相同:

    def _set_website(self, value:(str,None)) -> None:
        # - Type-check: This is an optional required str value
        if value != None:
            if type(value) != str:
                raise TypeError(
                    '%s.website expects a str value that is a '
                    'well-formed URL, but was passed '
                    '"%s" (%s)' % 
                    (
                        self.__class__.__name__, value, 
                        type(value).__name__
                    )
                )
            remnant = URL_CHECK.sub('', value)
            if remnant != '' or not value:
                raise TypeError(
                    '%s.website expects a str value that is a '
                    'well-formed URL, but was passed '
                    '"%s" (%s)' % 
                    (
                        self.__class__.__name__, value, 
                        type(value).__name__
                    )
                )
        self._website = value

这只剩下一个属性需要实现:products。products 属性的某些方面在一开始可能并不明显,但对于如何实现它具有潜在的重要影响。首先也是最重要的,它是其他对象的集合,无论是列表、字典还是其他尚未确定的对象,但无论如何,它不是一个单一的对象。此外,它被定义为只读属性:

    products = property(
        _get_products, None, None, 
        'Gets the collection of products (BaseProduct) associated '
        'with the Artisan that the instance represents'
    )

property定义中只提供了 getter 方法。这是有意的,但需要一些解释。

由于产品旨在处理产品对象的集合,因此,products属性本身不能更改为其他属性是非常重要的。例如,如果产品是可设置的,则可以执行如下操作:

# Given artisan = Artisan(...whatever initialization…)
artisan.products = 'Not a product collection anymore!'

现在,当然可以使用类型和值检查代码来防止这种赋值,尽管没有与属性本身相关联的 setter 方法,但我们几乎肯定希望以后有一个可用的 setter 方法,并且无论如何它都应该实现类型和值检查。但是,它的使用可能仅限于在创建 artisan 实例期间填充实例的产品。

另一个潜在的担忧是,可能会以容易出现错误且难以监管的方式更改集合的成员资格。例如,使用相同的artisan实例,并假设产品的底层数据存储是一个列表,没有什么可以阻止代码执行以下任何操作:

artisan.products.append('This is not a product!')
artisan.products[0] = 'This is also not a product!'

类似地,允许任意删除工匠的产品(del artisan.products可能不是一个好主意。

那么,我们至少要保证以下几点:

  • 不允许操纵products的成员资格,也不能影响真实的基础数据
  • 仍然允许访问(或者操纵)单个products成员的成员,也就是说,给定一个产品实例列表,从它们读取数据和向它们写入数据不受它们所在集合的限制

即使不开发某种自定义集合类型,也有几个选项。由于products属性使用 getter 方法获取并返回值,因此可以更改返回的数据,以便:

  • 返回实际数据的直接副本,在这种情况下,更改返回集合的成员身份不会影响原始集合
  • 返回不同收集类型的数据副本;例如,如果真实数据存储在列表中,则返回该列表的元组将提供与原始列表相同的所有 iterable 序列功能,但不允许更改该副本本身的成员资格

Python 通过对象引用跟踪对象,也就是说,它通过与指定给对象的名称关联来关注对象在内存中的实际位置,因此当从已经存在的对象列表创建对象列表或对象元组时,新集合的成员与原始列表中的对象相同,例如:

# - Create a class to demonstrate with
class Example:
    pass

# -  Create a list of instances of the class
example_list = [
    Example(), Example(), Example(), Example()
]

print('Items in the original list (at %s):' % hex(id(example_list)))
for item in example_list:
    print(item)

# Items in the original list (at 0x7f9cd9ed6a48):
# <__main__.Example object at 0x7f9cd9eed550>
# <__main__.Example object at 0x7f9cd9eed5c0>
# <__main__.Example object at 0x7f9cd9eed5f8>
# <__main__.Example object at 0x7f9cd9eed630>

创建原始列表的副本将创建一个新的、独特的集合,其中仍包含相同的成员:

new_list = list(example_list)
print('Items in the new list (at %s):' % hex(id(new_list)))
for item in new_list:
    print(item)

# Items in the new list (at 0x7f9cd89dca88):
# <__main__.Example object at 0x7f9cd9eed550>
# <__main__.Example object at 0x7f9cd9eed5c0>
# <__main__.Example object at 0x7f9cd9eed5f8>
# <__main__.Example object at 0x7f9cd9eed630>

以类似的方式创建元组也是如此:

new_tuple = tuple(example_list)
print('Items in the new tuple (at %s):' % hex(id(new_tuple)))
for item in new_tuple:
    print(item)

# Items in the new tuple (at 0x7f9cd9edd4a8):
# <__main__.Example object at 0x7f9cd9eed550>
# <__main__.Example object at 0x7f9cd9eed5c0>
# <__main__.Example object at 0x7f9cd9eed5f8>
# <__main__.Example object at 0x7f9cd9eed630>

然后,返回从原始状态数据值创建的新列表或元组,可以防止对属性值所做的更改影响实际的基础数据。目前,tuple returning 选项感觉是更好的选择,因为它更具限制性,在这种情况下,_get_products将按如下方式实现:

def _get_products(self) -> (tuple,):
  return tuple(self._products)

deleter 方法_del_products无法将None用作当前已就位的 getter 的默认值。必须将其更改为其他内容,因为尝试返回None默认值的tuple将引发错误。目前,删除的值将更改为空列表:

def _del_products(self) -> None:
  self._products = []

最后,这里是 setter 方法_set_products

    def _set_products(self, value:(list, tuple)) -> None:
        # - Check first that the value is an iterable - list or 
        #   tuple, it doesn't really matter which, just so long 
        #   as it's a sequence-type collection of some kind.
        if type(value) not in (list, tuple):
            raise TypeError(
                '%s.products expects a list or tuple of BaseProduct '
                'objects, but was passed a %s instead' % 
                (self.__class__.__name__, type(value).__name__)
            )
        # - Start with a new, empty list
        new_items = []
        # - Iterate over the items in value, check each one, and 
        #   append them if they're OK
        bad_items = []
        for item in value:
            # - We're going to assume that all products will derive 
            #   from BaseProduct - that's why it's defined, after all
            if isinstance(item, BaseProduct):
                new_items.append(item)
            else:
                bad_items.append(item)
        # - If there are any bad items, then do NOT commit the 
        #   changes -- raise an error instead!
        if bad_items:
            raise TypeError(
                '%s.products expects a list or tuple of BaseProduct '
                'objects, but the value passed included %d items '
                'that are not of the right type: (%s)' % 
                (
                    self.__class__.__name__, len(bad_items), 
                    ', '.join([str(bi) for bi in bad_items])
                )
            )
        self._products = value

综上所述,这些变化极大地限制了产品属性的更改:

  • 属性本身是只读的,不允许设置或删除该值
  • 从 getter 方法返回的值与从中获取的对象的状态数据中实际存储的值相同,但不同,尽管它仍然允许访问原始集合的成员,但不允许更改原始集合的成员身份
  • setter 方法对整个集合强制执行类型检查,确保集合的成员资格仅由适当的对象类型组成

尚未说明的是对集合成员进行更改的实际过程,该功能在方法成员中。

实现 BaseArtisan 的方法

目前设计的BaseArtisan预计提供两种抽象方法:

  • add_product,需要一种机制将products添加到要在派生的具体类上实现的实例的产品集合中
  • remove_product,这同样需要一种从派生实例的products集合中移除项的机制

这些被指定为抽象方法是因为,尽管在hms_sys的应用程序和服务可安装程序中几乎肯定会涉及到它们中的每一个的一些共同功能,但在这些相同的组件中,也几乎肯定会存在显著的实现差异,例如,很可能是唯一能够真正从products收藏中删除项目的用户。

通常,在大多数支持定义抽象方法的编程语言中,这些方法不需要提供任何实际实现。事实上,很可能将方法定义为抽象的行为实际上禁止了任何实现。Python 并没有对抽象方法实施这种限制,但仍然不希望有任何实现。因此,我们的抽象方法不需要比这更复杂:

 @abc.abstractmethod
 def add_product(self, product:BaseProduct):
    pass

 @abc.abstractmethod
 def remove_product(self, product:BaseProduct):
    pass

但是,由于我们可以将具体的实现放在抽象方法中,所以在有一些有用的东西需要保留在一个地方的情况下,可以利用它来提供基线功能。这两种方法add_productremove_product属于这一类:

  • 添加产品始终需要执行类型检查,在显示无效类型时引发错误,并将新项附加到实例集合
  • 删除产品始终涉及从实例的产品集合中删除指定的产品

考虑到这些因素,将这些公共过程放入抽象方法中,就好像它们是具体的实现一样,实际上是有益的。然后,可以从派生类实例调用这些进程,在基线本身执行之前或之后使用或不使用附加逻辑。考虑一个基本的实现,在 Ty1 T1。

    @abc.abstractmethod
    def add_product(self, product:BaseProduct):
        """
Adds a product to the instance's collection of products.

Returns the product added.

self ....... (BaseArtisan instance, required) The instance to 
             execute against
product ...  (BaseProduct, required) The product to add to the 
             instance's collection of products

Raises TypeError if the product specified is not a BaseProduct-
  derived instance

May be implemented in derived classes by simply calling
    return BaseArtisan.add_product(self, product)
"""
        # - Make sure the product passed in is a BaseProduct
        if not isinstance(product, BaseProduct):
            raise TypeError(
                '%s.add_product expects an instance of '
                'BaseProduct to be passed in its product '
                'argument, but "%s" (%s) was passed instead' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        # - Append it to the internal _products list
        self._products.append(product)
        # - Return it
        return product

例如,派生类是存在于中央办公室应用程序中的Artisan类,需要实现add_product,但可以按如下方式实现:

    def add_product(self, product:BaseProduct):
        # - Add any additional checking or processing that might 
        #   need to happen BEFORE adding the product here

        # - Call the parent add_product to perform the actual 
        #   addition
        result = BaseArtisan.add_product(self, product)

        # - Add any additional checking or processing that might 
        #   need to happen AFTER adding the product here

        # - Return the product
        return result

不过,这种方法有一个折衷之处:派生类可以实现一个全新的add_product流程,跳过现成的验证/业务规则。另一种方法是定义一个抽象的验证方法(_check_products,可能),该方法处理验证过程,并由add_product的具体实现直接调用。

remove_product方法可以类似地定义,并且可以在派生类实例中以类似的方式实现:

    @abc.abstractmethod
    def remove_product(self, product:BaseProduct):
        """
Removes a product from the instance's collection of products.

Returns the product removed.

self ....... (BaseArtisan instance, required) The instance to 
             execute against
product ...  (BaseProduct, required) The product to remove from 
             the instance's collection of products

Raises TypeError if the product specified is not a BaseProduct-
  derived instance
Raises ValueError if the product specified is not a member of the 
  instance's products collection

May be implemented in derived classes by simply calling
    return BaseArtisan.remove_product(self, product)
"""
        # - Make sure the product passed in is a BaseProduct.
        #   Technically this may not be necessary, since type 
        #   is enforced in add_product, but it does no harm to 
        #   re-check here...
        if not isinstance(product, BaseProduct):
            raise TypeError(
                '%s.add_product expects an instance of '
                'BaseProduct to be passed in its product '
                'argument, but "%s" (%s) was passed instead' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        try:
            self._products.remove(product)
            return product
        except ValueError:
            raise ValueError(
                '%s.remove_product could not remove %s from its '
                'products collection because it was not a member '
                'of that collection' % 
                (self.__class__.__name__, product)
            )

可能还有其他方法可以添加到BaseArtisan中,但如果有,它们可能会随着具体Artisan类的实现而出现。现在,在定义了__init__方法后,我们可以调用BaseArtisandone:

    def __init__(self, 
        contact_name:str, contact_email:str, 
        address:Address, company_name:str=None, 
        **products
        ):
        """
Object initialization.

self .............. (BaseArtisan instance, required) The instance to 
                    execute against
contact_name ...... (str, required) The name of the primary contact 
                    for the Artisan that the instance represents
contact_email ..... (str [email address], required) The email address 
                    of the primary contact for the Artisan that the 
                    instance represents
address ........... (Address, required) The mailing/shipping address 
                    for the Artisan that the instance represents
company_name ...... (str, optional, defaults to None) The company-
                    name for the Artisan that the instance represents
products .......... (BaseProduct collection) The products associated 
                    with the Artisan that the instance represents
"""
        # - Call parent initializers if needed
        # - Set default instance property-values using _del_... methods
        self._del_address()
        self._del_company_name()
        self._del_contact_email()
        self._del_contact_name()
        self._del_products()
        # - Set instance property-values from arguments using 
        #   _set_... methods
        self._set_contact_name(contact_name)
        self._set_contact_email(contact_email)
        self._set_address(address)
        if company_name:
            self._set_company_name(company_name)
        if products:
            self._set_products(products)
        # - Perform any other initialization needed

基本客户

定义客户数据结构外观的类非常简单,它使用已经在AddressBaseArtisan中为其所有属性建立的代码结构。就像BaseArtisan与具体Artisan实例之间的关系一样,Customer对象在其可以做的事情上,以及在系统的不同组件之间允许的数据访问方式上,预期会有很大的差异。我们将再次从定义 ABC-BaseCustomer而不是具体的Customer类开始:

BaseCustomer的性质为:

  • name,标准要求的文本行。

  • billing_addressshipping_address,除名称外,与BaseArtisan中定义的地址属性相同。shipping_address将是可选的,因为客户很可能只有一个地址同时用于这两个地址。

BaseCustomer唯一值得一提的新方面是shipping_address在初始化期间是如何注释的。BaseCustomer.__init__主要采用与之前的类定义相同的结构/方法:

    def __init__(self, 
        name:str, billing_address:Address, 
        shipping_address(Address,None)=None
    ):
        """
Object initialization.

self .............. (BaseCustomer instance, required) The instance to 
                    execute against
name .............. (str, required) The name of the customer.
billing_address ... (Address, required) The billing address of the 
                    customer
shipping_address .. (Address, optional, defaults to None) The shipping 
                    address of the customer.
"""
        # - Call parent initializers if needed
        # - Set default instance property-values using _del_... methods
        self._del_billing_address()
        self._del_name()
        self._del_shipping_address()
        # - Set instance property-values from arguments using 
        #   _set_... methods
        self._set_name(name)
        self._set_billing_address(billing_address)
        if shipping_address:
            self._set_shipping_address(shipping_address)
        # - Perform any other initialization needed

shipping_address参数的注释(Address,None)是新的,有点过时了。我们以前使用过内置类型作为注释类型,以前使用过内置的非None类型和None作为可选参数规范。Address.__init__多次使用此符号。尽管此代码使用了我们定义的类,但其工作方式是相同的:Address类也是一种类型,就像前面示例中的str一样。这只是一个在这个项目中定义的类型。

基本秩序

创建几乎任何哑数据对象类(甚至大部分哑数据对象类)的过程都非常相似,无论这些类代表什么,至少只要在这些工作的整个范围内都适用任何数据结构规则。随着创建更多此类面向数据的类,需要针对特定需求的新方法将越来越少,直到最终有一套简洁的方法来实现所需的各种类型和值约束的各种属性。

BaseOrder类(此处以BaseProduct显示)就是这种效果的一个很好的例子,至少乍一看:

BaseOrder属性列表非常简短,因为订单真正代表的是客户与一系列产品的关系:

  • customerBaseCustomer的实例,而BaseCustomer又具有该客户billing_addressshipping_address属性;除了属性值的类型将是BaseCustomer实例之外,合理地假设它的行为方式与BaseCustomerAddress类型属性的行为方式相同 *** productsBaseProduct实例的集合,其行为可能与BaseArtisanproducts属性完全相同——毕竟,它将做同样的事情,存储产品实例并防止这些实例的变异,因此它的初始实现将直接从BaseArtisan复制**

简言之,除了在客户**属性的情况下更改名称外,这两个属性都已经建立了实现模式,因此在BaseOrder中没有实质性的新内容。

Copying code directly from one class to another is a contentious topic at times; even if everything works perfectly, it is, by definition, duplicating code, which means that there are now multiple copies of that code to be maintained if something goes awry later on.

基础产品

BaseProductABC 也有很多近似样板的属性代码,尽管到目前为止只有三个属性属于已建立的实现模式:

  • name是标准要求的文本行属性。
  • summary是标准要求的文本行属性。
  • description是可选的字符串值。
  • dimensions是标准的可选文本行属性。
  • shipping_weight是一个必需的数值,它可能仅用于确定运输成本,但也可能出现在网上商店的产品展示中。
  • metadata是元数据键(字符串)和值(也可能是字符串)的字典。这是一个新的数据结构,因此我们将在稍后详细研究它。
  • available是一个必需的布尔值,允许技工指示产品可在 HMS web store 上销售,尽管中央办公室工作人员可以看到。
  • AuthT0T 也是一个所需的布尔值,这表明 HMS Web 存储应该考虑到 OLE T1 产品 Type T2 可用。它是由中央办公室的工作人员控制的,尽管它可能对技工可见。

BaseProduct目前只有两种关联方法,都用于管理与产品实例关联的元数据值:

  • set_metadata将在实例上设置元数据键/值
  • remove_metadata将从实例中删除元数据键和值

namesummarydimensions属性作为标准必需和可选文本行,将遵循这些模式。description几乎是可选的文本行实现;所有需要更改的是删除空白字符检查,这很好:

# These lines aren't needed for description
# - Value-check: no whitespace other than " "
bad_chars = ('\n', '\r', '\t')
for bad_char in bad_chars:
    if bad_char in value:
       is_valid = False
       break

shipping_weight属性的实现在 setter 方法_set_shipping_weight中变化最大,但(希望)在正常的 getter/setter/deleter 方法结构(这是项目中属性的典型方法)的情况下,它的预期是什么:

def _set_shipping_weight(self, value:(int,)):
  if type(value) != int:
    raise TypeError(
      '%s.shipping_weight expects a positive integer '
      'value, but was passed "%s" (%s)' % 
      (
         self.__class__.__name__, 
         value, type(value).__name__
       )
    )
   if value <= 0:
    raise ValueError(
      '%s.shipping_weight expects a positive integer '
       'value, but was passed "%s" (%s)' % 
       (
          self.__class__.__name__, 
          value, type(value).__name__
       )
    )
   self._shipping_weight = value

对于两个available属性的实现也可以这样说,尽管允许形式布尔值(TrueFalse值以及等价整数值(10值)作为有效的 setter 值参数是有意义的。在对象的状态数据可能无法存储为真正的布尔值的情况下,这给了一点回旋余地。虽然这是一种不太可能的情况,但也不是不可能的:

def _set_available(self, value:(bool,int)):
   if value not in (True, False, 1, 0):
      raise ValueError(
        '%s.available expects either a boolean value '
         '(True|False) or a direct int-value equivalent '
         '(1|0), but was passed "%s" (%s)' % 
          (self.__class__.__name__, value, type(value).__name__)
          )
   if value:
      self._available = True
        else:
          self._available = False

这只剩下metadata属性实现。元数据可能最好被认为是关于其他数据的数据,在本例中,是关于类所表示的基本产品的数据。在这种特殊情况下,metadata属性旨在提供高度灵活的数据,这些数据在不同的产品(或产品类型)之间可能会有很大的差异,同时在更严格定义的类/对象结构中仍然以相对简单的方式可用。这在手工制品的需求背景下是很重要的,因为工匠通过其网络商店创建和销售的产品几乎可以是任何东西:珠子、木材或金属家具、服装、珠宝等等。尽管有一些描述可能适用于任何产品,例如,它是由什么制成的,也许一些基本项目,如颜色,还有一些基本项目,使得几乎不可能在现有产品类别结构中不需要更多数据结构的情况下,对整个可用范围内的产品进行分类,或者许多产品类型几乎肯定彼此之间存在着令人望而却步的复杂关系。

最初的实现和设计将围绕着为每个对象维护一个基于dict的元数据结构。如果以后出现更严格的要求(例如,要求木制物品必须指定木材类型),则可能需要进行相应的重构调整,但目前看来,简单的dict是合理的。

BaseArtisanBaseOrder的产品属性一样,一个BaseProductmetadata需要很难随意或意外地改变,它需要有意识地做出改变。考虑到metadata结构预计将提供用于对产品进行分类的数据,这些键至少会对可使用的内容有一些限制。元数据名称应该是有意义的且合理的短。metadata值也应该如此,尽管它们可能比相应的键受到更少的约束。

将所有这些项放在一起,getter 和 deleter 方法与其他属性的等效方法没有太大区别通常的名称更改和不同的已删除默认值就是它们的全部:

    ###################################
    # Property-getter methods         #
    ###################################

    # ... 

    def _get_metadata(self) -> (dict,):
        return self._metadata

    # ... 

    ###################################
    # Property-deleter methods        #
    ###################################

    # ... 

    def _del_metadata(self) -> None:
        self._metadata = {}

setter 方法与大多数情况一样,存在显著差异;在本例中,当调用它时,期望的是清除任何现有元数据,并用一组新的、经过验证的键和值替换它。这将更改属性中的整个集合,而不仅仅是其部分或全部成员。由于该类还将提供一个专用方法,以允许添加新的metadata或更改metadata中的现有项,并且该方法需要对键和值执行所需的任何验证,_set_metadata属性设置器方法将使用类似名称的set_metadata方法以确保所有元数据都符合相同的标准。

第一步是确保传入值是字典:

    ###################################
    # Property-setter methods         #
    ###################################
# ... 

def _set_metadata(self, value:(dict,)):
 if type(value) != dict:
  raise TypeError(
   '%s.metadata expects a dictionary of metadata keys '
    '(strings) and values (also strings), but was passed '
         '"%s" (%s)' % 
    (self.__class__.__name__, value, type(value).__name__)
         )

我们将设置一个变量来跟踪遇到的任何无效值,并使用初始化期间用于清除当前元数据的相同机制清除当前元数据,_del_metadata

badvalues = []
self._del_metadata()

完成这些操作后,我们可以遍历值的键和值,为每一对调用set_metadata,直到它们都被解释清楚,并捕获所引发的任何错误,以便在需要时提供更有用的错误消息:

if value: # Checking because value could be an empty dict: {}
  for name in value:
     try:
       # - Since set_metadata will do all the type- and 
       #   value-checking we need, we'll just call that 
       #   for each item handed off to us here...
           self.set_metadata(name, value[name])
     except Exception:
       # - If an error was raised,then we want to capture 
       #   the key/value pair that caused it...
             badvalues.append((name, value[name]))

如果检测到任何错误值,那么我们将要引发一个错误并记录它们。如果未发生错误,则已重新填充属性:

if badvalues:
   # - Oops... Something's not right...
    raise ValueError(
      '%s.metadata expects a dictionary of metadata keys '
      '(strings) and values, but was passed a dict with '
      'values that aren\'t allowed: %s' % 
         (self.__class__.__name__, str(badvalues))
       )

set_metadata方法看起来很像我们的各种属性设置器方法键,元数据中的值(目前)都像标准的必需文本行属性,因此对每种方法执行的类型和值检查看起来非常熟悉:

def set_metadata(self, key:(str,), value:(str,)):
   """
Sets the value of a specified metadata-key associated with the product 
that the instance represents.

self .............. (BaseProduct instance, required) The instance to 
                    execute against
key ............... (str, required) The metadata key to associate a 
                    value with
value ............. (str, required) The value to associate with the 
                    metadata key
"""

下面是对key参数值的类型和值检查:

if type(key) != str:
  raise TypeError(
    '%s.metadata expects a single-line, '
     'non-empty str key, with no whitespace '
     'other than spaces, but was passed "%s" (%s)' % 
     (
        self.__class__.__name__, key, 
        type(key).__name__
      )
    )
   # - Value-check of key: no whitespace other than " "
        bad_chars = ('\n', '\r', '\t')
        is_valid = True
        for bad_char in bad_chars:
            if bad_char in key:
                is_valid = False
                break
   # - If it's empty or otherwise not valid, raise error
    if not key.strip() or not is_valid:
       raise ValueError(
         '%s.metadata expects a single-line, '
         'non-empty str key, with no whitespace '
         'other than spaces, but was passed "%s" (%s)' % 
          (
            self.__class__.__name__, key, 
            type(key).__name__
          )
       )

下面是对value参数值的类型和值检查:

if type(value) != str:
  raise TypeError(
    '%s.metadata expects a single-line, '
    'non-empty str value, with no whitespace '
    'other than spaces, but was passed "%s" (%s)' % 
    (
       self.__class__.__name__, value, 
       type(value).__name__
    )
  )
  # - Value-check of value: no whitespace other than " "
     bad_chars = ('\n', '\r', '\t')
     is_valid = True
     for bad_char in bad_chars:
        if bad_char in value:
          is_valid = False
          break
  # - If it's empty or otherwise not valid, raise error
      if not value.strip() or not is_valid:
        raise ValueError(
          '%s.metadata expects a single-line, '
          'non-empty str value, with no whitespace '
          'other than spaces, but was passed "%s" (%s)' % 
            (
               self.__class__.__name__, value, 
               type(value).__name__
            )
         )
     self._metadata[key] = value

删除metadata需要相当短和简单的代码,尽管它也假设如果试图删除不存在的元数据,则无需引发错误。可能需要允许出现此类错误,但目前的假设是不需要:

def remove_metadata(self, key):
        """
Removes the specified metadata associated with the product that the 
instance represents, identified by the key

self .............. (BaseProduct instance, required) The instance to 
                    execute against
key ............... (str, required) The key that identifies the 
                    metadata value to remove
"""
        try:
            del self._metadata[key]
        except KeyError:
            pass

BaseProduct完成后,满足hms_core类库的要求范围。单元测试仍然需要编写,因此会出现任何问题。

处理重复代码–HASSProducts

BaseArtisanBaseOrder具有行为相同的products属性,这些属性的原始实现本质上涉及将代码从一个复制和粘贴到另一个。虽然在这种特殊情况下(因为hms_core类库很小,成员很少,而且只有两个地方需要维护重复的代码),但在较大的库中,或者如果该代码有大量重复,那么可能很快就会出现问题。由于 Python 允许类从多个父类继承,我们可以利用该功能定义一个新的 ABC-HasProducts-将所有与产品属性相关的代码保存在一个地方:

This approach is a variation of an object oriented principle that's usually referred to as a mixin—a class that contains concrete implementations of functionality for use in other classes.

HasProducts的实现实质上只是BaseArtisanBaseOrder产品属性代码的收集或重新包装:

class HasProducts(metaclass=abc.ABCMeta):
    """
Provides baseline functionality, interface requirements, and 
type-identity for objects that can have a common products 
property whose membership is stored and handled in the same 
way.
"""

getter、setter 和 deleter 方法:

###################################
# Property-getter methods         #
###################################

def _get_products(self) -> (tuple,):
   return tuple(self._products)

###################################
# Property-setter methods         #
###################################

def _set_products(self, value:(list, tuple)) -> None:
# - Check first that the value is an iterable - list or 
#   tuple, it doesn't really matter which, just so long 
#   as it's a sequence-type collection of some kind.

 if type(value) not in (list, tuple):
   raise TypeError(
     '%s.products expects a list or tuple of BaseProduct '
     'objects, but was passed a %s instead' % 
     (self.__class__.__name__, type(value).__name__)
            )
  # - Start with a new, empty list
  new_items = []
  # - Iterate over the items in value, check each one, and 
  #   append them if they're OK
 bad_items = []
for item in value:
 # - We're going to assume that all products will derive 
 #   from BaseProduct - That's why it's defined, after all
      if isinstance(item, BaseProduct):
         new_items.append(item)
      else:
         bad_items.append(item)
 # - If there are any bad items, then do NOT commit the 
 #   changes -- raise an error instead!
     if bad_items:
      raise TypeError(
      '%s.products expects a list or tuple of BaseProduct'
      'objects, but the value passed included %d items '
      'that are not of the right type: (%s)' % 
      (
         self.__class__.__name__, len(bad_items), 
         ', '.join([str(bi) for bi in bad_items])
      )
   )
   self._products = value

###################################
# Property-deleter methods        #
###################################

  def _del_products(self) -> None:
    self._products = []

products属性定义:

###################################
# Instance property definitions   #
###################################

products = property(
_get_products, None, None,
'Gets the products (BaseProduct) of the instance'
)

对象初始化:

###################################
# Object initialization           #
###################################

def __init__(self, *products):
        """
Object initialization.

self .............. (HasProducts instance, required) The instance to 
                    execute against
products .......... (list or tuple of BaseProduct instances) The 
                    products that were ordered
"""
        # - Call parent initializers if needed
        # - Set default instance property-values using _del_... methods
        self._del_products()
        # - Set instance property-values from arguments using 
        #   _set_... methods
        if products:
            self._set_products(products)
        # - Perform any other initialization needed

###################################
# Abstract methods                #
###################################

添加和删除产品的抽象方法:

    @abc.abstractmethod
    def add_product(self, product:BaseProduct) -> BaseProduct:
        """
Adds a product to the instance's collection of products.

Returns the product added.

self ....... (HasProducts instance, required) The instance to 
             execute against
product ...  (BaseProduct, required) The product to add to the 
             instance's collection of products

Raises TypeError if the product specified is not a BaseProduct-
  derived instance

May be implemented in derived classes by simply calling
    return HasProducts.add_product(self, product)
"""
        # - Make sure the product passed in is a BaseProduct
        if not isinstance(product, BaseProduct):
            raise TypeError(
                '%s.add_product expects an instance of '
                'BaseProduct to be passed in its product '
                'argument, but "%s" (%s) was passed instead' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        # - Append it to the internal _products list
        self._products.append(product)
        # - Return it
        return product

    @abc.abstractmethod
    def remove_product(self, product:BaseProduct):
        """
Removes a product from the instance's collection of products.

Returns the product removed.

self ....... (HasProducts instance, required) The instance to 
             execute against
product ...  (BaseProduct, required) The product to remove from 
             the instance's collection of products

Raises TypeError if the product specified is not a BaseProduct-
  derived instance
Raises ValueError if the product specified is not a member of the 
  instance's products collection

May be implemented in derived classes by simply calling
    return HasProducts.remove_product(self, product)
"""
        # - Make sure the product passed in is a BaseProduct.
        #   Technically this may not be necessary, since type 
        #   is enforced in add_product, but it does no harm to 
        #   re-check here...
        if not isinstance(product, BaseProduct):
            raise TypeError(
                '%s.add_product expects an instance of '
                'BaseProduct to be passed in its product '
                'argument, but "%s" (%s) was passed instead' % 
                (
                    self.__class__.__name__, value, 
                    type(value).__name__
                )
            )
        try:
            self._products.remove(product)
            return product
        except ValueError:
            raise ValueError(
                '%s.remove_product could not remove %s from its '
                'products collection because it was not a member '
                'of that collection' % 
                (self.__class__.__name__, product)
            )

BaseArtisanBaseOrder中使用HasProducts并不困难,尽管它涉及重构它们以删除已经存在的代码,这些代码将覆盖HasProducts中的公共代码。首先确保使用HasProducts的类继承自该类:

class BaseArtisan(HasProducts, metaclass=abc.ABCMeta):
    """
Provides baseline functionality, interface requirements, and 
type-identity for objects that can represent an Artisan in 
the context of the HMS system.
"""

派生类的__init__方法也必须更改为调用HasProducts__init__,以确保它执行所有相关的初始化任务:

def __init__(self, 
  contact_name:str, contact_email:str, 
  address:Address, company_name:str=None, 
  **products
  ):
    """
Object initialization.
"""
   # - Call parent initializers if needed
# This is all that's needed to perform the initialization defined 
# in HasProducts
        HasProducts.__init__(self, *products)

为新类设置默认值和实例值的过程不再需要担心如何处理products属性设置,因为这是由HasProducts.__init__处理的:

        # - Set default instance property-values using _del_... methods
        self._del_address()
        self._del_company_name()
        self._del_contact_email()
        self._del_contact_name()
# This can be deleted, or just commented out.
#        self._del_products()
     # - Set instance property-values from arguments using 
        #   _set_... methods
        self._set_contact_name(contact_name)
        self._set_contact_email(contact_email)
        self._set_address(address)
        if company_name:
            self._set_company_name(company_name)
# This also can be deleted, or just commented out.
#        if products:
#            self._set_products(products)

最后,每个类中的products属性及其关联的 getter、setter 和 deleter 方法可以从派生类中删除:

# This also can be deleted, or just commented out.
#    products = property(
#         _get_products, None, None,
#         'Gets the products (BaseProduct) of the instance'
#    )

随着HasProducts的实现,hms_core包的完整结构和功能暂时完成,因为它还没有经过单元测试。整个软件包的类图显示了所有活动部件以及它们之间的关系:

总结

总的来说,这些类提供了可以描述为哑数据对象的定义。它们提供的功能很少或没有,这些功能在某种程度上与特定数据结构的定义和规范没有直接关系。甚至HasProducts及其派生的类也属于这一类,因为这里提供的功能与提供数据结构和控制该结构的操作方式密切相关。随着从这些类派生的其他类的创建,这些类将开始变得更智能,从单个对象的数据持久化开始。

不过,首先需要编写这些类的单元测试,以确保它们已经过测试,并且可以根据需要重新测试。由于这代表了编码目标的重大转变,并且将涉及对测试目标以及如何实现这些目标的一些深入检查,因此第一个单元测试通过证明了它自己的一章。****