Python条件替换多态

Jac*_*ack 4 python oop polymorphism conditional-statements

我最近阅读了一篇文章/代码片段,其中显示了使用多态替换条件的示例.这是代码:

之前:

def log_msg(log_type):
    msg = 'Operation successful'
    if  log_type == 'file':
        log_file.write(msg)
    elif log_type == 'database':
        cursor.execute('INSERT INTO log_table (MSG) VALUES ('?')', msg)
Run Code Online (Sandbox Code Playgroud)

后:

class FileLogger(object):
    def log(self, msg):
        log_file.write(msg)

class DbLogger(object):
    def log(self, msg):
        cursor.execute('INSERT INTO log_table (MSG) VALUES ('?')', msg)

def log_msg(obj):
    msg = 'Operation successful'
    obj.log(msg)
Run Code Online (Sandbox Code Playgroud)

是我从中得到它的地方.

现在我的问题是,第二种方法比第一种方法更好吗?据我所知,如果我想使用第二种方法,每次我想记录某些内容时,我都必须做这样的事情:

if log_type == 'file':
    log_msg(FileLogger())
elif: log_type == 'database':
    log_msg(DbLogger())
Run Code Online (Sandbox Code Playgroud)

我错过了这个或非常明显的观点吗?

Ben*_*son 5

更换条件有多态性,当你看到重构是最有效的相同条件遍布你的代码.当您需要添加新类型的行为时,您必须查找并更改每个条件以适应新选项.相反,我们将条件逻辑集中在一个地方 - 创建多态对象的代码 - 并且让OO的语义为我们处理剩下的事情.


这是一个更令人震惊的稻草人形式的日志记录示例.

if log_type == "file":
    log_file.write("DEBUG: beginning script")
elif log_type == "database":
    cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('DEBUG', 'beginning script')")

try:
    file = open("/path/to/file")
    lines = file.readlines()

    if log_type == "file":
        log_file.write("INFO: read {} lines".format(len(lines)))
    elif log_type == "database":
        cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('INFO', 'read {} lines')".format(len(lines)))
except:
    if log_type == "file":
        log_file.write("ERROR: failed to read file")
    elif log_type == "database":
        cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('ERROR', 'failed to read file')")

    raise
finally:
    if log_type == "file":
        log_file.write("INFO: closing file")
    elif log_type == "database":
        cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('INFO', 'closing file')")
    file.close()
Run Code Online (Sandbox Code Playgroud)

您可以看到检查日志类型的条件逻辑执行了三次,每次都略有不同.如果我们需要添加一种新类型的日志记录,比如通过电子邮件记录错误,我们必须遍历整个脚本并elif在每个日志语句中添加另一个日志语句,这容易出错并且很麻烦.

也很难一眼看出脚本实际上在做什么,因为它在实际进行日志记录的细节中淹没了.


因此,这是替换条件多态性的一个很好的候选者.以下是重构后的记录器类:

class AbstractLogger:
    def debug(self, msg):
        self.log("DEBUG", msg)

    def info(self, msg):
        self.log("INFO", msg)

    def error(self, msg):
        self.log("ERROR", msg)

    def log(self, level, msg):
        raise NotImplementedError()

class FileLogger(AbstractLogger):
    def __init__(self, file):
        self.file = file

    def log(self, level, msg):
        self.file.write("{}: {}".format(level, msg))

class DatabaseLogger(AbstractLogger):
    def __init__(self, cursor):
        self.cursor = cursor

    def log(self, level, msg):
        self.cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('{}', '{}')".format(level, msg))
Run Code Online (Sandbox Code Playgroud)

我已经使用了继承来避免在FileLogger和DatabaseLogger类之间重复过多的代码.

这是脚本:

# create the logger once at the start
if log_type == "file":
    logger = FileLogger(log_file)
elif log_type == "database":
    logger = DatabaseLogger(cursor)

logger.debug("beginning script")

try:
    file = open("/path/to/file")
    lines = file.readlines()

    logger.info("read {} lines".format(len(lines)))
except:
    logger.error("failed to read file")
    raise
finally:
    logger.info("closing file")
    file.close()
Run Code Online (Sandbox Code Playgroud)

现在添加新类型的日志记录要容易得多:只需编写EmailLogger并修改创建它的单个条件.代码也更清晰:记录器类隐藏了它们如何在一组简单的方法后面工作的详细信息,这些方法具有面向日志的名称.


Bre*_*arn 1

要点是,您通常会在程序的早期某个时刻仅创建一个记录器对象。因此,您只需执行log_msg(myLogger),它就会自动执行正确的操作,无论您最初决定使用基于文件的日志记录还是基于数据库的日志记录。

换句话说,你的代码看起来像这样

# beginning of file
from logmodule import FileLogger, DBLogger, log_msg
myLogger = FileLogger()

# lots of other code here. . .
# later if you want to log something:

log_msg(myLogger)
Run Code Online (Sandbox Code Playgroud)

稍后,您可以返回并将开头更改为myLogger = DBLogger(),一切仍然有效。这个想法是在程序开始时创建记录器,一旦创建它,​​您就不需要担心您创建的是哪种类型,您可以同样使用它。

请注意,这个示例(包括您最初发布的代码)只是一个骨架;它不是您可以实际直接使用的代码。一方面,此代码不提供任何指定日志文件名的方法。我在这里描述的只是为什么要这样重构代码的想法。