《 Python 工匠》中多态的使用有一些疑惑

288 天前
 shinonome

文中给了一个案例

class FancyLogger:
    """日志类:支持向文件、Redis 、ES 等服务输出日志"""

    _redis_max_length = 1024

    def __init__(self, output_type=OutputType.FILE):
        self.output_type = output_type
        ...

    def log(self, message):
        """打印日志"""
        if self.output_type == OutputType.FILE:
            ...
        elif self.output_type == OutputType.REDIS:
            ...
        elif self.output_type == OutputType.ES:
            ...
        else:
            raise TypeError('output type invalid')

    def pre_process(self, message):
        """预处理日志"""
        # Redis 对日志最大长度有限制,需要进行裁剪
        if self.output_type == OutputType.REDIS:
            return message[:self._redis_max_length]

FancyLogger 类在日志输出类型不同时,需要有不同的行为。因此,我们完全可以为“输出日志”行为建模一个新的类型:LogWriter ,然后把每个类型的不同逻辑封装到各自的 Writer 类中。

class FileWriter:
    def write(self, message):
        ...
        

class RedisWriter:
    max_length = 1024

    def write(self, message):
        message = self._pre_process(message)
        ...

    def _pre_process(self, message):
        """Redis 对日志最大长度有限制,需要进行裁剪"""
        return message[:self.max_length]


class EsWriter:
    def write(self, message):
        ...

基于这些不同的 Writer 类,FancyLogger 可以简化成下面这样:

class FancyLogger:
    """日志类:支持向文件、Redis 、ES 等服务输出日志"""

    def __init__(self, output_writer=None):
        self._writer = output_writer or FileWriter()
        ...

    def log(self, message):
        self._writer.write(message)

文中对这样的写法好处解释为 代码利用多态特性,完全消除了原来的条件判断语句。另外你会发现,新代码的扩展性也远比旧代码好。

但是在我看来, 你要传什么 output_writer 不还是要通过 if 来选择吗, 只是把一个地方的 if 换到了另外一个地方,

扩展性 这个模块看起来确实好了, 但是总感觉和上面一样, 这里提高了, 但是其他地方就要更多 if, TVT, 面向对象还是没有入门

2543 次点击
所在节点    Python
10 条回复
shinonome
288 天前
@piglei 作者大佬能看看我的问题吗
zepc007
288 天前
emmm, 你可以理解为设计模式
yooomu
288 天前
抽离出 writer 之后,添加新的 writer 类型不再需要修改原来的类了,只需要编写一个新的 writer 实现就行了。体现了设计模式的开闭原则
zhu327
288 天前
这里如果有个工厂模式来实例化不同的 logger 的话,从上层看的话就不需要理解不同的 if 之间的差异了,可以理解为一种封装形式,隐藏不同 logger 之前的细节,对外提供统一的用户接口
ajunno
288 天前
> 你要传什么 output_writer 不还是要通过 if 来选择吗, 只是把一个地方的 if 换到了另外一个地方

这个成立吗?换到哪里了呢?我的观点是,`FancyLogger`内部解耦了类型判断——针对事物的行为建模,而不是对事物本身建模——分支语句是完全消失了,而不是转移(到外部)了。从外部使用者的角度来说,是完全一样的。
newaccount
288 天前
改之前:FancyLogger 需要知道每一个 Writer 类型,所以必须有 if 判断来根据不同的参数选择不同的具体实现

改之后:FancyLogger 与 Writer 解耦,具体的输出工作由 Writer 来完成,FancyLogger 不知道也不关心具体实现

你所指的 if 判断由 FancyLogger 转移到调用方的这个前提是不存在的,调用方直接实例化一个 Writer 实现类,或者通过工厂方法来获取配置的具体 Writer 对象
dajj
288 天前
实际上取决于谁写,如果两个类都是一个人维护,没意义
如果 Writer 需要其它的人写,那就有意义了
增加了灵活度,也增加了复杂度
两种都是正确的做法,不要厚此薄彼
NoOneNoBody
288 天前
FancyLogger 就是个“通用代码”,参考#7
FancyLogger 相当于一个 switcher

代码创建者 A 完成三个 Writer 类,给下游未知的人使用
如果有 FancyLogger 这段通用代码,不管谁,只需要知道类名就可以了
writer = FancyLogger(classname())
如果没有这段通用代码,使用者 B 可能需要三个 class 都研判一次,写出 if 逻辑才能使用
当然,如果使用者 B 熟悉这三个 class ,他也可以不使用 FancyLogger ,而自己另外写代码

还有一种非常好用的情况
_writer = read_from(...) 反序列化或者从其他途径获得,这时使用者 C 甚至不知道 classname 是哪个,只知道 FancyLogger ,因为不知道具体 class 就无法写 if 逻辑,这时也能使用
writer = FancyLogger(_writer) 导入并继续之前的工作
piglei
286 天前
> 但是在我看来, 你要传什么 output_writer 不还是要通过 if 来选择吗, 只是把一个地方的 if 换到了另外一个地方,

你的理解已经很接近:“多态”确实无法完全消除“if”。这是因为,人们总是需要用代码来表达某种“如果/否则”的逻辑。但和普通的“if/else”比起来,面向对象设计的区别在于,它会努力将这些“如果/否则”逻辑封装在不同的实现(`XWriter`)中,让“if/else”代码只存活于代码的边缘区(工厂函数),从核心区中销声匿迹。

什么是核心区?显而易见,是 FancyLogger 中实际完成日志打印的部分;什么是边缘区?在例子中,用 if/else 分支去创建对应的 Writer 实例的代码,前置于打印日志功能,可以被看作身处边缘区。对比例子中的两种方式:

1. 无 Writer 抽象时:核心区需要理解**全部的“如果/否则”逻辑**,才能完成日志打印
2. 有 Writer 抽象时:在边缘区创建 writer 实例,传递给核心区,后者**一视同仁**调用 writer 完成日志打印

本书中的代码示例,因篇幅原因实现的功能比较简单,不同代码之间所产生的对比可能不够强烈。当业务逻辑变得更复杂后,利用多态特性来提炼并封装“如果/否则”的优势会变得更显著。在我心目中,那是在整个“面向对象”中,最富有魅力的地方之一。
shinonome
285 天前
感谢各位大佬的答疑, 有一点知道面向对象在干什么了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://ex.noerr.eu.org/t/1107575

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX