Python的设计模式编程入门指南


Posted in Python onApril 02, 2015

有没有想过设计模式到底是什么?通过本文可以看到设计模式为什么这么重要,通过几个Python的示例展示为什么需要设计模式,以及如何使用。
设计模式是什么?

设计模式是经过总结、优化的,对我们经常会碰到的一些编程问题的可重用解决方案。一个设计模式并不像一个类或一个库那样能够直接作用于我们的代码。反之,设计模式更为高级,它是一种必须在特定情形下实现的一种方法模板。设计模式不会绑定具体的编程语言。一个好的设计模式应该能够用大部分编程语言实现(如果做不到全部的话,具体取决于语言特性)。最为重要的是,设计模式也是一把双刃剑,如果设计模式被用在不恰当的情形下将会造成灾难,进而带来无穷的麻烦。然而如果设计模式在正确的时间被用在正确地地方,它将是你的救星。

起初,你会认为“模式”就是为了解决一类特定问题而特别想出来的明智之举。说的没错,看起来的确是通过很多人一起工作,从不同的角度看待问题进而形成的一个最通用、最灵活的解决方案。也许这些问题你曾经见过或是曾经解决过,但是你的解决方案很可能没有模式这么完备。

Python的设计模式编程入门指南

虽然被称为“设计模式”,但是它们同“设计“领域并非紧密联系。设计模式同传统意义上的分析、设计与实现不同,事实上设计模式将一个完整的理念根植于程序中,所以它可能出现在分析阶段或是更高层的设计阶段。很有趣的是因为设计模式的具体体现是程序代码,因此可能会让你认为它不会在具体实现阶段之前出现(事实上在进入具体实现阶段之前你都没有意识到正在使用具体的设计模式)。

可以通过程序设计的基本概念来理解模式:增加一个抽象层。抽象一个事物就是隔离任何具体细节,这么做的目的是为了将那些不变的核心部分从其他细节中分离出来。当你发现你程序中的某些部分经常因为某些原因改动,而你不想让这些改动的部分引发其他部分的改动,这时候你就需要思考那些不会变动的设计方法了。这么做不仅会使代码可维护性更高,而且会让代码更易于理解,从而降低开发成本。

设计出一个优雅的、易于维护的程序难点在于发现我所说的“变化的向量”(在这里,“向量”指的是最大的梯度变化方向(maximum gradient),而并非指一个容器类)。意思是找出系统中变化的最重要的部分,或者换句话说,发现影响系统最大的花销在哪里。一旦你发现了变化的向量,你就可以围绕这个重点设计你的程序。

所以设计模式的目的就是分离代码中的可变部分。如果你这么去审视这个问题,你会立刻看到多个设计模式。举个例子,面向对象的继承(inheritance)可以看做一种设计模式(虽然是由编译器实现的)。它允许通过同样的接口(不变的部分)来表现不同的行为(变化的部分)。组合也可以被认为是一种设计模式,因为它允许通过动态或静态的方式改变实现类的对象以及他们的行为。

另一个常见的设计模式例子是迭代器。迭代器自Python出现伊始就已经随for循环的使用而存在了,并且在Python2.2版本的时候被明确成为其一个特性。一个迭代器隐藏了容器内部的具体实现,提供一个依次访问容器对象内每个元素的方式。所以,你能够使用通用的代码对一个序列的每个元素做对应操作而不用去理会此序列是怎么建立的。所以你的代码能够对任何能够产生迭代器的对象有效。

这里列举了三种最基本的设计模式:

  1.     结构化模式,通常用来处理实体之间的关系,使得这些实体能够更好地协同工作。
  2.     创建模式,提供实例化的方法,为适合的状况提供相应的对象创建方法。
  3.     行为模式,用于在不同的实体建进行通信,为实体之间的通信提供更容易,更灵活的通信方法。

我们为什么要使用设计模式?

从理论上来说,设计模式是对程序问题比较好的解决方案。无数的程序员都曾经遇到过这些问题,并且他们使用这些解决方案去处理这些问题。所以当你遇到同样的问题,为什么要去想着创建一个解决方案而不是用现成的并且被证明是有效的呢?
例子

假定现在有一个任务,需要你找到一个有效的方法合并两个做不同事情的类,在已有系统中这两个类在许多不同的地方被大量使用,所以移除这两个类或是改动已有的代码都是异常困难的。不仅如此,更改已有的代码会导致大量的测试工作,因为在这样一种依赖大量不同组件的系统中,这些修改总是会引入一些新的错误。为了避免这些麻烦,你可以实现一个策略模式(Strategy Pattern)和适配器模式(Adapter Pattern)的变体,这两种模式能够很好的处理这种问题。

class StrategyAndAdapterExampleClass():
 
  def __init__(self, context, class_one, class_two):
    self.context = context
    self.class_one = class_one
    self.class_two = class_two
 
  def operation1(self):
    if self.context == "Context_For_Class_One":
      self.class_one.operation1_in_class_one_context()
    else:
      self.class_two.operational_in_class_two_context()

很简单是吧?现在让我们来仔细研究一下策略模式。
策略模式

策略模式是一种与行为相关的设计模式,允许你在运行时根据指定的上下文确定程序的动作。你可以在两个类中封装不同的算法,并且在程序运行时确定到底执行哪种策略。

在上面的例子中,策略是根据实例化时context变量的值来决定的。如果给定context变量的值是“class_one”,将会执行class_one,否则就会执行class_two。
我在那里使用它?

假定你现在正在写一个类能够更新或创建一条新的用户记录,接收同样的输入参数(诸如姓名、地址、手机号等),但是根据不同的情况会调用对应的更新或是创建方法。当然,你可能会用一个if-else判断处理这个问题,但是如果你需要在不同的地方使用这个类呢?那么你就得不停地重写if-else判断。为什么不简单地通过指定上下文来解决这个问题。
 

class User():
  def create_or_update(self, name, address, mobile, userid=None):
    if userid:
      # it means the user doesn't exist yet, create a new record
    else:
      # it means the user already exists, just update based on the given userid

常规的策略模式涉及到将算法封装到另一个类中,但如果这样的话,那个类就太浪费了。切记不要死记模板,把握住核心概念灵活的变通,最重要是解决问题。

 
适配器模式

适配器模式是一个结构性的设计模式,允许通过不同的接口为一个类赋予新的用途,这使得使用不同调用方式的系统都能够使用这个类。

也可以让你改变通过客户端类接收到的输入参数以适应被适配者的相关函数。
怎么使用?

另一个使用适配器类的地方是包装器(wrapper),允许你将一个动作包装成为一个类,然后可以在合适的情形下复用这个类。一个典型的例子是当你为一个table类簇创建一个domain类时,你能够将所有的对应不同表的相同动作封装成为一个适配器类,而不是一个接一个的单独调用这些不同的动作。这不仅使得你能够重用你想要的所有操作,而且当你在不同的地方使用同样的动作时不用重写代码。

比较一下两种实现:
不用适配器的方案

class User(object):
  def create_or_update(self):
    pass
 
class Profile(object):
  def create_or_update(self):
    pass
 
user = User()
user.create_or_update()
 
profile = Profile()
profile.create_or_update()

如果我们需要在不同的地方做同样的事,或是在不同的项目中重用这段代码,那么我们需要重新敲一遍。
使用包装类的解决方案

看看我们怎么反其道而行:
 

account_domain = Account()
account_domain.NewAccount()

在这种情况下,我们通过一个包装类来实现账户domain类:
 

class User(object):
  def create_or_update(self):
    pass
 
class Profile(object):
  def create_or_update(self):
    pass
 
class Account():
  def new_account(self):
    user = User()
    user.create_or_update()
 
    profile = Profile()
    profile.create_or_update()

这样的话,你就能够在你需要的时候使用账户domain了,你也可以将其他的类包装到domain类下。
工厂模式

工厂模式是一种创建型的设计模式,作用如其名称:这是一个就像工厂那样生产对象实例的类。

这个模式的主要目的是将可能涉及到很多类的对象创建过程封装到一个单独的方法中。通过给定的上下文输出指定的对象实例。
什么时候使用?

使用工厂模式的最佳时机就是当你需要使用到单个实体的多个变体时。举个例子,你有一个按钮类,这个按钮类有多种变体,例如图片按钮、输入框按钮或是flash按钮等。那么在不同的场合你会需要创建不同的按钮,这时候就可以通过一个工厂来创建不同的按钮。

让我们先来创建三个类:
 

class Button(object):
  html = ""
  def get_html(self):
    return self.html
 
class Image(Button):
  html = "<img alt="" />"
 
class Input(Button):
  html = "<input type="text" />"
 
class Flash(Button):
  html = ""

然后创建我们的工厂类:
 

class ButtonFactory():
  def create_button(self, typ):
    targetclass = typ.capitalize()
    return globals()[targetclass]()

译注:globals()将以字典的方式返回所有全局变量,因此targetclass = typ.capitalize()将通过传入的typ字符串得到类名(Image、Input或Flash),而globals()[targetclass]将通过类名取到类的类(见元类),而globals()[targetclass]()将创建此类的对象。

我们可以这么使用工厂类:
 

button_obj = ButtonFactory()
button = ['image', 'input', 'flash']
for b in button:
  print button_obj.create_button(b).get_html()

输出将是所有按钮类型的HTML属性。这样骂你就能够根据不同的情况指定不同类型的按钮了,并且很易于重用。
装饰器模式

装饰器模式是一个结构性模式,允许我们根据情况,在运行时为一个对象添加新的或附加的行为。

目的是为给一个特定的对象实例应用扩展的函数方法,并且同时也能够产生没有新方法的原对象。它允许多装饰器结合用于一个实例,所以你就不会出现实例同单个装饰器相捆绑的情况了。这个模式是实现子类继承外的一个可选方式,子类继承是指从父类集成相应的功能。与子类继承必须在编译时添加相应的行为不同,装饰器允许你在运行时根据需要添加新的行为。

可以根据以下步骤实现装饰器模式:

  1.     以原组件类为基类创建装饰器类。
  2.     在装饰器类中添加一个组件类的指针域
  3.     将一个组件传递给装饰器类的构造器以初始化组件类指针
  4.     在装饰器类中,将所有的组件方法指向组件类指针,并且,
  5.     在装饰器类中,重写每个需要修改功能的组件方法。

相关维基百科(http://en.wikipedia.org/wiki/Decorator_pattern)
什么时候使用?

使用装饰器模式的最佳时机是当你有一个根据情况需要添加新的行为的实体时。假设你有一个HTML链接元素,一个登出链接,并且你希望根据当前页面对具体的行为做微小的改动。这种情况下,我们可以使用装饰器模式。

首先,建立我们所需要的装饰模式。

如果我们在主页并且已经登录,那么将登出链接用h2标签标记。

如果我们在不同的页面并且已经登录,那么用下划线标签标记链接

如果已登录,用加粗标记链接。

一旦建立了装饰模式,我们就可以开工了。

class HtmlLinks():
  def set_html(self, html):
    self.html = html
 
  def get_html(self):
    return self.html
 
  def render(self):
    print(self.html)
 
class LogoutLink(HtmlLinks):
  def __init__(self):
    self.html = "<a href="logout.html"> Logout </a>"
 
class LogoutLinkH2Decorator(HtmlLinks):
  def __init__(self, logout_link):
    self.logout_link = logout_link
    self.set_html(" {0} ".format(self.logout_link.get_html()))
 
  def call(self, name, args):
    self.logout_link.name(args[0])
 
class LogoutLinkUnderlineDecorator(HtmlLinks):
  def __init__(self, logout_link):
    self.logout_link = logout_link
    self.set_html(" {0} ".format(self.logout_link.get_html()))
 
  def call(self, name, args):
    self.logout_link.name(args[0])
 
class LogoutLinkStrongDecorator(HtmlLinks):
  def __init__(self, logout_link):
    self.logout_link = logout_link
    self.set_html("<strong> {0} </strong>".format(self.logout_link.get_html()))
 
  def call(self, name, args):
    self.logout_link.name(args[0])
 
logout_link = LogoutLink()
is_logged_in = 0
in_home_page = 0
 
if is_logged_in:
  logout_link = LogoutLinkStrongDecorator(logout_link)
if in_home_page:
  logout_link = LogoutLinkH2Decorator(logout_link)
else:
  logout_link = LogoutLinkUnderlineDecorator(logout_link)
 
logout_link.render()

单例模式

单例模式是一个创建型的设计模式,功能是确保运行时对某个类只存在单个实例对象,并且提供一个全局的访问点来访问这个实例对象。

因为对于调用单例的其他对象而言这个全局唯一的访问点“协调”了对单例对象的访问请求,所以这些调用者看到的单例内变量都将是同一份。
什么时候能够使用?

单例模式可能是最简单的设计模式了,它提供特定类型的唯一对象。为了实现这个目标,你必须控制程序之外的对象生成。一个方便的方法是将一个私有内部类的单个对象作为单例对象。

class OnlyOne:
  class __OnlyOne:
    def __init__(self, arg):
      self.val = arg
    def __str__(self):
      return repr(self) + self.val
  instance = None
  def __init__(self, arg):
    if not OnlyOne.instance:
      OnlyOne.instance = OnlyOne.__OnlyOne(arg)
    else:
      OnlyOne.instance.val = arg
  def __getattr__(self, name):
    return getattr(self.instance, name)
 
x = OnlyOne('sausage')
print(x)
y = OnlyOne('eggs')
print(y)
z = OnlyOne('spam')
print(z)
print(x)
print(y)
print(`x`)
print(`y`)
print(`z`)
output = '''
<__main__.__OnlyOne instance at 0076B7AC>sausage
<__main__.__OnlyOne instance at 0076B7AC>eggs
<__main__.__OnlyOne instance at 0076B7AC>spam
<__main__.__OnlyOne instance at 0076B7AC>spam
<__main__.__OnlyOne instance at 0076B7AC>spam
<__main__.OnlyOne instance at 0076C54C>
<__main__.OnlyOne instance at 0076DAAC>
<__main__.OnlyOne instance at 0076AA3C>
'''

因为内置类是用双下划线开始命名,所以它是私有的,用户无法直接访问。内置类包含了所有你希望放在普通类中的方法,并且通过外层包装类的构造器控制其创建。当第一次你创建OnlyOne时,初始化一个实例对象,后面则会忽略创建新实例的请求。

通过代理的方式进行访问,使用__getattr__()方法将所有调用指向单例。你可以从输出看到虽然看起来好像创建了多个对象(OnlyOne),但 __OnlyOne对象只有一个。虽然OnlyOne实例有多个,但他们都是唯一的 __OnlyOne对象的代理。

请注意上面的方法并没有限制你只能创建一个对象,这也是一个创建有限个对象池的技术。然而在那种情况下,你可能会遇到共享池内对象的问题。如果这真是一个问题,那你可以通过为共享对象设计签入“check-in”和迁出“check-out”机制来解决这个问题。
总结

在本文中,我只列举了几个我再编程中觉得十分重要的设计模式来讲,除此之外还有很多设计模式需要学习。如果你对其他的设计模式感兴趣,维基百科的设计模式部分可以提供很多信息。如果还嫌不够,你可以看看四人帮的《设计模式:可复用面向对象软件的基础》一书,此书是关于设计模式的经典之作。

最后一件事:当使用设计模式时,确保你是用来解决正确地问题。正如我之前提到的,设计模式是把双刃剑:如果使用不当,它们会造成潜在的问题;如果使用得当,它们则将是不可或缺的。

Python 相关文章推荐
在Python中操作文件之read()方法的使用教程
May 24 Python
Python中内建函数的简单用法说明
May 05 Python
Python回文字符串及回文数字判定功能示例
Mar 20 Python
python实现windows下文件备份脚本
May 27 Python
在Python中使用defaultdict初始化字典以及应用方法
Oct 31 Python
关于python之字典的嵌套,递归调用方法
Jan 21 Python
python3下载抖音视频的完整代码
Jun 05 Python
Python使用APScheduler实现定时任务过程解析
Sep 11 Python
Python如何输出百分比
Jul 31 Python
Python生成pdf目录书签的实例方法
Oct 29 Python
python向xls写入数据(包括合并,边框,对齐,列宽)
Feb 02 Python
让文件路径提取变得更简单的Python Path库
May 27 Python
介绍Python中的一些高级编程技巧
Apr 02 #Python
用Python代码来解图片迷宫的方法整理
Apr 02 #Python
在Python3中使用asyncio库进行快速数据抓取的教程
Apr 02 #Python
Python中的Classes和Metaclasses详解
Apr 02 #Python
详解Python中的装饰器、闭包和functools的教程
Apr 02 #Python
详解Python的迭代器、生成器以及相关的itertools包
Apr 02 #Python
用Python实现通过哈希算法检测图片重复的教程
Apr 02 #Python
You might like
Protoss兵种介绍
2020/03/14 星际争霸
发布一个用PHP fsockopen写的HTTP下载的类
2007/02/22 PHP
PHP学习笔记 用户注册模块用户类以及验证码类
2011/09/20 PHP
PHP循环函数使用介绍之PHP基础入门教程
2013/09/21 PHP
PHP中spl_autoload_register()和__autoload()区别分析
2014/05/10 PHP
php apache开启跨域模式过程详解
2019/07/08 PHP
SUN的《AJAX与J2EE》全文译了
2007/02/23 Javascript
Prototype Selector对象学习
2009/07/23 Javascript
再谈javascript图片预加载技术(详细演示)
2011/03/12 Javascript
优化Jquery,提升网页加载速度
2013/11/14 Javascript
莱鸟介绍window.print()方法
2016/01/06 Javascript
浏览器复制插件zeroclipboard使用指南
2016/03/26 Javascript
Node.js 文件夹目录结构创建实例代码
2016/07/08 Javascript
JavaScript代码里的判断小结
2016/08/22 Javascript
微信小程序 wx.request(OBJECT)发起请求详解
2016/10/13 Javascript
详解如何将 Vue-cli 改造成支持多页面的 history 模式
2017/11/20 Javascript
Vue组件的使用教程详解
2018/01/05 Javascript
微信小程序入口场景的问题集合与相关解决方法
2019/06/26 Javascript
原生js实现拖拽移动与缩放效果
2020/08/24 Javascript
linux系统使用python监测网络接口获取网络的输入输出
2014/01/15 Python
numpy 对矩阵中Nan的处理:采用平均值的方法
2018/10/30 Python
使用 PyTorch 实现 MLP 并在 MNIST 数据集上验证方式
2020/01/08 Python
Python2与Python3的区别详解
2020/02/09 Python
Python 爬取必应壁纸的实例讲解
2020/02/24 Python
解决python便携版无法直接运行py文件的问题
2020/09/01 Python
css3和jquery实现自定义checkbox和radiobox组件
2014/04/22 HTML / CSS
法国发饰品牌:Alexandre De Paris
2018/12/04 全球购物
个人考核材料
2014/05/15 职场文书
竞选大队干部演讲稿
2014/09/11 职场文书
2014年政务公开工作总结
2014/12/09 职场文书
2016年母亲节寄语
2015/12/04 职场文书
入团申请书格式
2019/06/20 职场文书
用Python爬虫破解滑动验证码的案例解析
2021/05/06 Python
redis实现的四种常见限流策略
2021/06/18 Redis
nginx结合openssl实现https的方法
2021/07/25 Servers
nginx容器方式反向代理实战
2022/04/18 Servers