Python Redux 中的子类化

子类化和组合之间的冲突与面向对象编程一样古老。最新的语言如Go或Rust证明你不需要子类来成功编写代码。但是,具体来说,在 Python 中进行子类化的实用方法是什么?

任何关注我足够长的人都知道,我坚定地站在组合胜于继承的阵营中。但是,Python 的设计方式有时如果没有子类化**就无法编写惯用代码。我写这篇文章的目的是思考有时是什么时候的问题,并解开我对这个话题的直觉.

我意识到这篇博文很长。事实上,这是我自 2006 年发表论文以来写的最长的一篇散文。客观地说,我应该把它分成至少三个部分。参与会更好(搜索引擎优化!社交媒体!点击!),这将使人们更有可能真正读到最后。

但我希望它代表自己。我希望它是我多年来所学知识的精髓。当然,你可以随意休息,只要你喜欢——这篇文章不会去任何地方!

让我们从细微差别开始。许多关于子类化的讨论如此令人沮丧地没有结果的原因之一是不只有一种类型的继承。正如精彩的文章为什么继承从来没有任何意义所解释的那样,有三种类型永远不应该混合——不管你对子类化的感觉如何。

这使得三个人有可能互相争论,每个人都以自己的方式正确,从未找到共同点。我们都看到了这些讨论的展开。

根据我的经验——如果严格单独使用——一种是好的,一种是可选的但有用的,一种是坏的。子类化的大多数问题源于我们试图一次使用不止一种类型的子类化——或者将对象设计集中在坏类型上。

在所有情况下,你都牺牲了阅读的便利性来换取写作的便利。这不一定是坏事,因为软件设计都是关于权衡的,你可以得出结论,在某些情况下它是完全值得的。这就是为什么我希望你同意接下来的一切。但我希望能激发一些想法,帮助你在未来做出决定。

但是现在,事不宜迟,让我们来看看这三种类型。从坏的开始。

类型 1:代码共享

命名空间是一个很棒的主意——让我们做更多的事情吧!

—蒂姆·彼得斯, Python 之禅

大多数对子类化的批评来自代码共享,这是理所当然的。我觉得我没有太多要补充的东西,所以我将链接到比我更聪明、更有说服力的人的卓越的现有艺术:

  • Brandon Rhodes的组合优于继承原则,

  • 对象继承的终结和新模块化的开始 作者: Augie Fackler和Nathaniel Manista在 PyCon US 2013,

  • 在 RailsConf 2015 上,Sandi Metz无事可做。

简而言之,存在三个总体问题:

  1. 超过一个轴的变化。这是 Brandon 的帖子和 Sandi 演讲的后半部分的主要实用内容。这并不容易解释,所以我会参考他们的作品,但本质是:如果你想自定义一个类的多个行为方面,通过子类化的代码共享是行不通的。它导致子类爆炸。

    这不是意见或权衡。这是事实。

  2. 类和实例名称空间变得混乱。如果您访问self.x从一个或多个基类继承的类中的属性,您就是在告诉代码的读者:“类层次结构中的x 某处有一个属性,但我不会告诉您在哪里。祝你好运找到它!” 需要研究和精神能量来找出来自哪里x。阅读代码时如此,调试时也是如此。

    这也意味着同一层次结构中的两个类总是存在危险——它们彼此不知道——试图拥有一个同名的属性。Python 有双下划线前缀 ( __x) 的概念来处理这种情况,但这已被反对并认为更喜欢成年人同意的原则。

    问题是,如果所有各方都不知情,就不可能达成知情同意。这个问题随着多重继承及其极端形式的mixins呈指数级恶化。你依赖于类——你可能无法控制并且彼此一无所知——在一个共享的命名空间中相处。

    另一个问题是您无法控制从基类公开给用户的**方法和属性。它们就在那里,玷污了你的 API 表面。随着基类的发展以及添加或重命名方法和属性,可能会随着时间的推移而发生变化。这就是为什么attrs(最终是dataclasses)选择使用类装饰器而不是子类化的原因之一:您必须仔细考虑附加到类的内容。不可能不小心将某些东西泄漏给所有子类。

  3. 混淆间接。这是上一个问题的一个特例,也是奥吉和纳撒尼尔谈话的重点。如果每个方法都 on self,则在查看调用时不清楚它来自哪里。除非你非常小心,否则每一次理解控制流的尝试都会以徒劳无功的方式结束。一旦多重继承开始发挥作用,您最好阅读MRO和super(). 我认为如果一个问题归结为“什么是super()平衡的?” ,那么可以说有些不对劲。在StackOverflow上获得近 3,000 个赞和超过 1,000 个书签。

    如果您构建的 API 需要子类化以实现或覆盖从其他地方调用的现有方法,所有这些都会带来额外的问题。Twisted和asyncio都犯了这些罪Protocol分别上课,这让我永远伤痕累累。最常见的问题是找出存在哪些方法很复杂(尤其是在像Twisted这样的深层层次结构中),并且如果您将方法命名为轻微错误并且基类没有找到它,那么通常会出现无声的失败。

    “基于子类化的设计也是一个巨大的错误”可能是编程中最常说的一句话。

    ——科里·本菲尔德, 推特


仅当我需要改变我无法控制的类的行为时,我才使用子类化进行代码共享。我认为这是一种不那么令人震惊的猴子补丁。通常最好编写一个Adapter、Facade、Proxy或Decorator,但在某些情况下,如果您只想更改一个小细节,您需要委托的方法数量会变得很麻烦.

无论如何,不要让它成为你设计的核心部分。

类型 2:抽象数据类型,即接口

抽象数据类型(ADT)主要用于收紧接口契约。您希望能够说您想要一个具有某些属性(属性、方法)的对象,而不关心其余部分。在许多语言中,它们被称为接口,这听起来不那么自命不凡,这就是我从现在开始使用该术语的原因。

由于 Python 是动态类型的,并且类型注释是严格可选的,因此您不需要正式的接口。但是,有一种方法可以显式定义一段代码运行所需的接口,这是非常有用的。自从像Mypy这样的类型检查器出现以来,它们已经成为经过验证的API 文档,我觉得这很棒。

例如,如果你想编写一个接收带有read()方法的对象的函数,你会以某种方式定义一个Reader具有该方法的接口(稍后将解释如何)并像这样使用它:

def printer(r: Reader) -> None:
    print(r.read())
​
printer(FooReader())

你的printer()函数并不关心read()它在做什么,只要它返回一个它可以打印的字符串。它可以返回预定义的字符串、读取文件或进行 Web API 调用。printer()不在乎,如果您尝试调用任何其他方法而不是read()它,您的类型检查器会对您大喊大叫。


Python 的标准库提供了两种定义接口的方法:

  1. 抽象基类(ABC) 是zope.interface的一个功能较弱的版本,使用名义子类型工作。它们从 Python 2.6 开始就已经存在,标准库中充满了它们。

    请注意,并非每个抽象基类也是抽象数据类型。有时它只是一个不完整的类,你应该通过继承它并实现它的抽象方法来完成它——而不是一个接口。但是,区别并不总是 100% 清楚。

  2. 协议通过使用结构子类型来避免子类化。它们已在 Python 3.8 中添加,但键入扩展使它们早在 Python 3.5 时就可用。

名词子类型和结构子类型是大词,但幸运的是它们很容易解释。

名义子类型

名义子类型意味着您必须告诉类型系统您的类是接口定义的子类型。ABC 通常通过子类化来做到这一点,但您也可以使用该register()方法。

这就是您如何Reader从介绍和标记中定义接口FooReader以及BarReader作为它的实现:

import abc
​
class Reader(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def read(self) -> str: ...
​
class FooReader(Reader):
    def read(self) -> str:
        return "foo"
​
class BarReader:
    def read(self) -> str:
        return "bar"
​
Reader.register(BarReader)
​
assert isinstance(FooReader(), Reader)
assert isinstance(BarReader(), Reader)

如果FooReader没有名为 read 的方法,实例化将在运行时失败。如果您将register()路由与 一起使用BarReader,则接口在运行时不会被验证,它会变成(如文档中所说的)“虚拟子类”。这使您可以自由地使用更多动态或神奇的方式来提供所需的界面。由于register()将实现对象作为其参数,因此您可以将其用作类装饰器并为自己节省两个空行。

在名义子类型中,多重继承不仅被接受而且被鼓励,因为理想情况下,没有方法,没有行为,被继承和无可救药地混合——只有类身份被复合。一个类可以实现许多不同的接口,接口越小越好。


使用 ABC 定义接口的一个“好处”是,通过对它们进行子类化,您可以通过向抽象基类添加常规方法来走私代码共享。但正如开头提到的:混合子类化类型是个坏主意。通过子类共享代码是个坏主意。多重继承使它成为一个特别糟糕的主意。

公平地说,我已经看到了这种模式的良好用途,但你必须非常明智地使用你的方法。Python 中的一个惯用案例是当您需要实现一大堆dunder 方法时基于其他明确定义的行为。一个很好的例子是collections.UserDict。出于上述所有原因,这并不是很好,但在 Python 的约束和文化范围内这是一个很好的权衡。但是,在 的示例中UserDict,当您尝试在子类上添加比 dict 预期的更多行为时,**就会出现问题。然后,子类化代码共享部分中的问题可能会重新生效。只给类一个避免这种情况的责任。

结构子类型

结构子类型是类型的鸭子类型:如果您的类满足 a 的约束Protocol,它会自动被视为它的子类型。因此,一个类可以在不知道它们的情况下从各种包中实现许多s! Protocol

默认情况下,这仅适用于类型检查器,但如果您 apply typing.runtime_checkable(),您也可以isinstance()对它们执行检查。

上一节中的示例如下所示:

from typing import Protocol, runtime_checkable
​
@runtime_checkable
class Reader(Protocol):
    def read(self) -> str: ...
​
class FooReader:
    def read(self) -> str:
        return "foo"
​
assert isinstance(FooReader(), Reader)

如您所见:FooReader根本不知道Reader协议存在!


我真正喜欢s 的地方在于它允许我以完全非侵入的Protocol方式定义我需要的接口,并且该定义可以与接口的**使用者一起使用。当您在同一个代码库中对同一个接口有不同的实现时,那就太好了。例如,您可以有一个MailSender在生产环境中发送电子邮件但在开发环境中将其打印到控制台的界面.

或者,如果您只使用第三方类的一小部分子集,并且想明确说明是哪个子集。这是很棒的(经过验证的!)文档,在为您的测试实施假货时会有所帮助。

有关Protocols 和结构子类型的更多详细信息,请查看Glyph的I Want A New Duck。

虽然这种类型的子类化大多是无害的,但由于ABCs 的方法,您不需要在 Python 中对抽象数据类型进行子类化。typing.Protocol``register()

大师兄影视App,支持下载和切换播放源,4K超清画质支持投屏!

类型 3:专业化

所以我们有一种有害的子类类型和一种不必要的子类类型。现在我们已经找到了好的类型。事实上,即使你愿意,你也无法绕过 Python 中的这种继承。除非你想停止使用Exceptions.

有趣的是,专业化经常被误解。直觉上很容易:如果我们说类 B 专门化基类 A,我们说类 B 是具有附加属性的A。狗是一种动物。A350 是客机。它们具有其基类的所有属性,并添加属性、方法或只是层次结构中的一个位置.

尽管有这种诱人的简单性,但它经常被错误地使用。最臭名昭著的错误是说正方形是矩形的特例,因为在几何上,它是一种特殊情况。但是,正方形不是矩形加上更多

您不能在可以使用矩形的任何地方都使用正方形,除非代码知道它也必须使用正方形. 如果你不能与一个对象进行交互,就好像它是它的基类的一个实例一样,你就违反了Liskov 替换原则而且你不能编写多态代码。

如果仔细观察,您会发现上一节中的接口是专门化的一个特例。您总是将通用 API 合约专门化为具体的东西!关键区别在于抽象数据类型是……嗯……抽象的。

当我试图表示严格分层的数据时,我发现专业化很有用。

例如,假设您想将电子邮件帐户表示为类。他们都共享一些数据,比如他们在数据库中的 ID 和地址,但是——根据账户的类型——他们(可以)有额外的属性。重要的是,这些添加的属性和方法几乎没有改变现有的。例如,在服务器上存储电子邮件的邮箱需要密码哈希形式的登录信息。接受电子邮件并仅将其转发到另一个电子邮件地址的帐户不会.

您最终会得到以下四种方法。

方法一:为每个案例创建一个专用类

这些是您最终想要的类:

class Mailbox:
    id: UUID
    addr: str
    pwd: str
​
class Forwarder:
    id: UUID
    addr: str
    targets: list[str]

地址类型在类中编码,每个类只有它使用的字段。如果你的模型这么简单,这绝对是要走的路。只有当您拥有更多字段和更多类型时,任何重复数据删除尝试才有意义。

您添加到任何一个类的任何方法都将完全独立于另一个类——没有混淆的余地。您还可以将这些类与使用Union 类型的类型检查器一起使用:Mailbox | Forwarder。


通常在任何情况下从这种方法开始都是一个好主意,因为重复比错误的抽象要便宜得多。查看您面前所有可能的字段使进一步的设计决策变得容易得多。

方法2:创建一个类,使字段可选

情况总是变得更糟。条件再现。

——桑迪·梅斯, 无所谓

当您尝试不惜一切代价避免继承时,您可能最终会采用这种设计,但仍要避免重复自己:

class AddrType(enum.Enum):
    MAILBOX = "mailbox"
    FORWARDER = "forwarder"
​
class EmailAddr:
    type: AddrType
    id: UUID
    addr: str
​
    # Only useful if type == AddrType.MAILBOX
    pwd: str | None
    # Only useful if type == AddrType.FORWARDER
    target: list[str] | None

从技术上讲,这更DRY,但它使类实例的使用更加尴尬。大多数字段的类型/存在完全取决于字段的值type,它之所以存在是因为所有地址类型共享相同的类类型。

它与我最喜欢的设计原则相矛盾,即使非法状态无法表示,并且不可能使用类型检查器进行明智的检查,这会一直抱怨访问None-able 字段。

事实上,在这个类上工作的所有行为都会集中在一起,这会导致很多条件(if- elif-else语句)显着增加代码的复杂性。多态性的全部意义在于避免这种情况。

具有可选属性 是潜在的危险信号。拥有需要注释来解释何时使用它们的字段是May Day Rally。正如有争议的类型注释一样,在这种情况下,它们清楚地向您指出您的模型存在问题。没有它们,您将不得不注意到您的代码比应有的更复杂,这并不那么简单。


您可以使情况变得不那么痛苦,并将特定于邮箱的数据移动到一个类中,并使该字段成为可选的。它更好,但仍然不必要地笨重。

方法 3:组合

这种方法颠倒了最后一种方法,并且在我们过于简单的数据模型中看起来很傻,但是让我们假设它EmailAddr有更多的字段,因此值得将其包装到自己的类中:

class EmailAddr:
    id: UUID
    addr: str
​
class Mailbox:
    email: EmailAddr
    pwd: str
​
class Forwarder:
    email: EmailAddr
    targets: list[str]

这种方法还不错!我们没有可选字段,所有数据关系都很清楚。随着可读性和清晰度的提高,没有什么可抱怨的。

除了它也很笨重而且你不需要咨询 Guido 就可以意识到它除了 Pythonic 之外的一切。那么为什么它看起来如此做作,尽管组合应该比继承更好?EmailAddr和Mailbox/Forwarder关系太密切——命名字段来存储它甚至很尴尬。组合并没有让我们失望,但在这种情况下,强迫一种有关系的关系感觉就像违背了规律。

但是向我们展示一些关于我们的模型的信息是很有用的:它们都有共同的基础信息并且密切相关。因此,让我们进行最后一步,使用 Python 共享公共基础的方式,并通过子类化来使用专业化。当我在本文后面的部分改进基于子类的设计时,我们将回到组合。

方法 4:创建一个公共基类,然后专门化

最后,我认为最符合人体工程学、干燥、明显和可行的类型检查方法:

class EmailAddr:
    id: UUID
    addr: str
​
class Mailbox(EmailAddr):
    pwd: str
​
class Forwarder(EmailAddr):
    targets: list[str]

只要你有一个Mailbox,你就知道你有一个pwd字段——你的类型检查器也是如此。类型在类中编码,因此您不必在字段中重复它。AMailbox严格来说是一个EmailAddr 加号。

至于代码,您现在必须了解负责任的子类化规则,例如前面提到的Liskov 替换原则。这是额外的复杂性和精神开销,但边界和责任更加清晰。

子类化需要您的知识和纪律。作曲会机械**地强制你自律——即使它会导致笨拙。

这可能是在构图方面犯错的最简单的原因:它为你的错误留出了更少的空间

各种子类化都会影响阅读清晰度,因为您必须在头脑中组装最终类才能知道存在哪些字段。但实际上你得到了与第一种方法相同的类。只要您不过度使用它并理想地保持定义在物理上彼此接近,这是在这种情况下的最佳权衡。

它非常有用,我已经在我的 PEM 文件解析库中使用它并且还没有后悔。


从本节中得出的一般建议是始终首先关注数据的形状,**然后才是如何处理它。

一旦你确定了形状,行为就会自然得多。一个很好的例子是明确的数据优先的Sans I/O运动,因为这种行为应该可以通过设计替换。

只要在专业化时避免方法之间的跨层次交互,就可以了。但是总是问自己一个函数是否不够——尤其是如果你正在协调两个或多个类之间的工作并且没有多态性可以利用。如果你不能确定一个方法属于哪个类,答案通常不是。

最后一定要了解@singledispatch;如果你还没有,它会感觉很神奇。

作为奖励,遵循这些准则可以为您提供具有出色可测试性的对象。

超越蛇鼻

最后一种方法非常有用,以至于它以嵌入的绰号潜入了 we-don't-subclass Go:

type EmailAddr struct {
    addr string
}
​
type Mailbox struct {
    EmailAddr
    pwd string
}

现在的实例Mailbox有一个属性addr,就好像它是在其中定义的一样: https: //play.golang.org/p/WSjJA6MYUDb。但是在初始化时您仍然必须明确并且没有实际的层次结构。没有super()。您只能侧向呼叫。务实的妥协!

回过头来看,它是我们方法 3的语法,但在许多方面,您从方法 1获得类。

在Go中看到这一点对我来说有点启示,因为我自己的基于直觉的子类启发式符合这种模式,但我不知道如何制定它们。现在我可以说我会尽可能地使用子类化——而且会!– 在Go中使用嵌入。

这强调了学习其他编程语言和交叉授粉的想法是多么值得。但是,当您尝试应用它们时,永远不要忘记通过 Python 的独特视角来检查这些想法。

从这往哪儿走

在阅读清晰度方面,正确完成的组合优于继承。由于代码的阅读次数多于编写次数,因此通常避免子类化,尤其是不要混合各种类型的继承,也不要使用子类化进行代码共享。不要忘记,通常你只需要一个函数。

重要的是要记住,您不能采用基于继承的设计并停止子类化。基于组合的设计从根本上是不同的,因此您可能必须更换一些信念和技术。

尽管不是基于 Python 的,但我所知道的 OOP 设计的最佳介绍是99 Bottles of OOP,如果您还没有阅读它,那么您应该阅读它。它不仅具有令人难以置信的指导意义,而且读起来也很有趣。

为了不完全逃避,我将用一个具体的例子来结束它。

案例分析

我将使用我帮助审查的精彩 的 Python 架构模式中的清晰代码这绝对值得你花时间和金钱. 我在这里使用它是因为 Harry——他是它的作者之一——在我抱怨它之后告诉我写一篇博客文章。


目标是存储库模式的实现:允许您在数据存储中添加和检索对象的类。由于对这篇博文不感兴趣的原因,它还必须记住它在名为seen.

一个重要的设计目标是保持实际存储可插拔,因此它可以——例如——在生产中使用像Postgres这样的数据库,在单元测试中使用字典。但是用于记住所见对象的代码对于所有实现都是相同的,因此您希望共享代码。

专业化在这里不起作用,因为它走错了方向:跟踪存储库是“常规”存储库的专业化。因此,我们想要共享的代码最终会出现在子类中。那没用。

因此,本书使用了我最不喜欢的使用子类化的代码共享类型:模板方法模式。这意味着基类提供了一个整体控制流,而您的子类填写了一些细节:

  1. 用户实例化一个子类,

  2. 然后调用基类上的方法,

  3. 依次调用子类上的方法。

在这种情况下,子类必须实现的方法是add_product和get_by_sku:

class AbstractRepository(abc.ABC):
    seen: set[Product]

    def __init__(self) -> None:
        self.seen = set()

    def add_product(self, product: Product) -> None:
        self._add_product(product)
        self.seen.add(product)

    def get_by_sku(self, sku: str) -> Product | None:
        product = self._get_by_sku(sku)
        if product:
            self.seen.add(product)

		return product

    @abc.abstractmethod
    def _add_product(self, product: Product):
        raise NotImplementedError

    @abc.abstractmethod
    def _get_by_sku(self, sku: str) -> Product | None:
        raise NotImplementedError

因此,每个子类都必须定义add_product()and_get_by_sku()方法。然后,用户调用AbstractRepository'add_product()和get_by_sku()方法,这些方法又委托给子类的add_product()and _get_by_sku(),同时记住Product它看到的类型的对象.

狂热的读者会立即注意到继承的原罪:它混合了接口的定义并与子类共享代码。如果您想重新了解为什么这是不好的,请查看为什么继承从来没有任何意义(我已在介绍中链接)。

更实际的问题是,在试图理解程序流时,在类层次结构中来回走动是很乏味的。

即使作为用户也是如此,因为公共 API 是由抽象基类定义的——而不是您实际实例化的类!这在文档系统中通常没有得到很好的处理,您必须在阅读时跳来跳去。


当面对这样的代码,你想摆脱子类化的束缚时,你有两个选择:

  1. 包装类。与其将其作为 的一部分self,不如将其存储在实例属性中。根据需要委托给属性上的方法。

  2. 参数化行为。一旦您需要跨多个轴自定义类的行为并且通过子类化的代码共享失败,这就是您要走的路。这听起来很复杂,但 Sandi Metz 在前面提到的Nothing is Something演讲中完美地展示了这一点,只需几行代码就可以自定义排序和格式。

    掌握这个概念通常是当它对大多数人来说“点击”时——至少对我来说是这样。

我们的例子很简单:我们只想做具体存储库正在做的事情以及其他事情. 因此,我们选择第一个选项。如果你眯起眼睛,你会意识到模板子类化在这里完成的方式只不过是包装一个类。除了命名空间混淆并且控制流程混乱之外。

存储库

我们不是用一堆代码来定义一个抽象基类,而是通过定义一个名为的协议来定义我们将包装的接口Repository:

class Repository(typing.Protocol):
    def add_product(self, product: Product) -> None: ...
    def get_by_sku(self, sku: str) -> Product | None: ...

当然,如果你不使用类型注解,你可以省略这一步。


使用字典存储数据的简单实现可能如下所示:

class DictRepository:
    _storage: dict[str, Product]

    def __init__(self):
        self._storage = {}

    def add_product(self, product: Product) -> None:
        self._storage[product.sku] = product

    def get_by_sku(self, sku: str) -> Product | None:
        return self._storage.get(sku)

存储库必须实现两个承诺的公共方法,但整个类都属于它。从来没有任何名称冲突的危险。它只有一项工作:保存和检索Products。它也不必知道一个名为Repositoryeven 的协议是否存在;您的类型检查器会为您确定它是它的实现。

追踪

Repository接下来,让我们通过包装一个实例来实现跟踪:

class TrackingRepository:
    _repo: Repository
    seen: set[Product]

    def __init__(self, repo: Repository) -> None:
        self._repo = repo
        self.seen = set()

    def add_product(self, product: Product) -> None:
        self._repo.add_product(product)
        self.seen.add(product)

    def get_by_sku(self, sku: str) -> Product | None:
        product = self._repo.get_by_sku(sku)
        if product:
            self.seen.add(product)

        return product

这个类由一个你只知道它实现的对象Repository和一组Products 组成。如果您在_repo属性上使用了接口未承诺的任何内容Repository,您的类型检查器将在无需执行代码的情况下对您大喊大叫。

概括

我更喜欢这个版本,因为它有一个非常清晰的程序流程。您无需检查任何基类即可知道方法和属性的来源。

这种清晰的代价是我们必须将存储库存储在我们的类 ( repo) 中并调用self.repo.add_product()而不是self._add_product(). 打字有点多。

另一方面,我们最终得到了两个小的、独立的类,它们唯一的契约是一个紧密的、显式的接口。这不仅易于阅读和理解,而且易于测试。

作为临终的顿悟:如果您一直想知道如何为代码编写测试,而不仅仅是像所有测试教程中那样进行字符串操作或添加两个数字,我希望您现在看到学习更好的 OOP 设计也将方便地帮助您。

最后的话

哇,你成功了!谢谢你一直陪着我!我的最终目标是为讨论添加更多细微差别。我想让你明白,使用Exceptions模板方法模式并不是一个好主意,因为“两者都是子类化”。我希望我已经取得了一些成功。

由于篇幅较长,这篇文章不太可能得到很多“看、读、转推/点赞”的分享。很可能,它也花了一些时间在打开的标签/您的阅读队列中!因此,如果您能以某种方式分享它以帮助它传播,那就太好了。

随时让我知道您对此有何看法或您喝了多少杯咖啡。我不打算在公开讨论上花费太多时间,因为它们往往会变得过于激烈、迂腐和教条。我写这篇文章的原因之一是能够将其他参与者指向它的 URL 并使用弹射座椅。愿它以同样的方式为您服务!

我正在根据此材料进行一次演讲,因此,如果您对在您的会议或公司进行的演示感兴趣,请与我们联系,一旦有可能再次面对面!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pxr007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值