Skip to content

Latest commit

 

History

History
executable file
·
328 lines (230 loc) · 22.8 KB

第三章-原型模式.md

File metadata and controls

executable file
·
328 lines (230 loc) · 22.8 KB

第三章-原型模式


Sometimes, we need to create an exact copy of an object. For instance, assume that you want to create an application for storing, sharing, and editing (such as modifying, adding notes, and removing) culinary recipes. User Bob finds a cake recipe and after making a few modifications he thinks that his cake is delicious, and he wants to share it with his friend, Alice. But what does sharing a recipe mean? If Bob wants to do some further experimentation with his recipe after sharing it with Alice, will the new changes also be visible in Alice's recipe? Can Bob keep two copies of the cake recipe? His delicious cake recipe should remain unaffected by any changes made in the experimental cake recipe.

有时候,我们需要创建一个对象的完全拷贝。例如,假设你想要创建一个能够存储、分享、和编辑的(比如,修改,添加笔记、删除笔记)菜谱应用。用户Bod发现了一本糕点食谱,之后对他做出了一些自认为可以让蛋糕美味的轻微修改,而且他还想和好友爱丽丝分享这份食谱。那么分享食谱意味着什么呢?如果鲍勃想要在把食谱分享给爱丽丝之后进行深入实验,那么对食谱的新变更也可以出现在爱丽丝的食谱中吗?鲍勃可以拥有蛋糕食谱的双份拷贝吗?他的美味蛋糕食谱应该不受到任何试验性蛋糕食谱修改的影响。

Such problems can be solved by allowing the users to have more than one independent copy of the same recipe. Each copy is called a clone, because it is an exact copy of the original object at a specific point in time. The time aspect is important, since it affects what the clone contains. For example, if Bob shares the cake recipe with Alice before making his own improvements to achieve perfection, Alice will never be able to bake her own version of the delicious cake that Bob created! She will only be able to bake the original cake recipe found by Bob.

此类问题可以通过让用户使用不止一份独立的相同拷贝来解决。每个拷贝都称作克隆,因为这份拷贝是在一个指定对时间内的原始对象拷贝。时间方面很重要,因为它影响着将要克隆的内容。例如,如果鲍勃在改进食谱已实现完美之前就把蛋糕食谱分享给了爱丽丝,那么爱丽丝就永远不能够烘培鲍勃所创建的美味蛋糕的私有定制版本!她只能够烘培由鲍勃创建的原始版本蛋糕食谱中的蛋糕。

Note the difference between a copy and a reference. If we have two references to the same cake recipe, whatever changes Bob makes to the recipe will be visible to Alice's version of the recipe, and vice versa. What we want is both Bob and Alice to have their own copy, so that they can make independent changes without affecting each other's recipe. Bob actually needs two copies of the cake recipe: the delicious version and the experimental version.

注意拷贝和引用之间的不同。如果对相同的糕点食谱做了两个引用,不论Bob对食谱做了什么更改,Alice的食谱中都可以看到,反之亦然。要是我们想让Bob和Alice都拥有各自的拷贝,那么他们就可以各自变更而不影响到对方的食谱。Bob实际上需要糕点食谱的两份拷贝:美味版和体验版。

The difference between a reference and a copy is shown in the following figure:

引用和复制之间的区别如下图所示:

img

On the left part, we can see two references. Both Alice and Bob refer to the same recipe, which essentially means that they share it and all modifications are visible by both. On the right part, we can see two different copies of the same recipe. In this case, independent modifications are allowed and the changes of Alice do not affect the changes of Bob, and vice versa.

在最后一部分,我们看到了两个引用。Alice和Bod引用了相同的食谱,这基本上就意味着他们共享了所有的变更,而且这些变更彼此课件。在右边部分,我们可以看到相同食谱的两份不同拷贝。这种情况下,单独的修改是被允许的,Alice的修改不会影响到Boo,反之亦然。

The Prototype design pattern helps us with creating object clones. In its simplest version, the Prototype pattern is just a clone() function that accepts an object as an input parameter and returns a clone of it. In Python, this can be done using the copy.deepcopy() function. Let's see an example. In the following code (file clone.py), there are two classes, A and B. A is the parent class and B is the derived class. In the main part, we create an instance of class B b, and use deepcopy() to create a clone of b named c. The result is that all the members of the hierarchy (at the point of time the cloning happens) are copied in the clone c. As an interesting exercise, you can try using deepcopy() with composition instead of inheritance which is shown in the following code:

原型设计模式有助于我们创建一个对象的克隆。在其最简单版本中,原型模式只是一个接受对象作为输入参数并返回这个对象克隆的clone()函数。在Python中,可以使用copy.deepcopy()函数实现此目的。让我们看一个例子。在下面的代码中(文件clone.py),由两个类A和B。A是一个父类而B是一个派生类。在主体部分中,我们创建了一个类B的实例b,并使用了deepcopy()创建了一个叫做c的实例b的克隆。

import copy


class A:
    def __init__(self):
        self.x = 18
        self.msg = 'Hello'


class B(A):
    def __init__(self):
        A.__init__(self)
        self.y = 34
    def __str__(self):
           return '{}, {}, {}'.format(self.x, self.msg, self.y)


if __name__ == '__main__':
    b = B()
    c = copy.deepcopy(b)
    print([str(i) for i in (b, c)])
    print([i for i in (b, c)])

When executing clone.py on my computer, I get the following:

当你执行clone.py后,输出内容是:

>>> python3 clone.py
['18, Hello, 34', '18, Hello, 34']
[<__main__.B object at 0x7f92dac33240>, <__main__.B object at
0x7f92dac33208>]

Although your output of the second line will most likely not be the same as mine, what's important is to notice that the two objects reside in two different memory addresses (the 0x... part). This means that the two objects are two independent copies.

虽然你的第二行的输出和我的有着很大的不同,重要的在于注意这两个对象位于两个不同的内存地址中(0x...那一部分)。这就意味着两个对象是两份独立的副本。

In the Implementation section, later in this chapter, we will see how to use copy. deepcopy() with some extra boilerplate code wrapped in a class, for keeping a registry of the objects that are cloned.

在本章稍后的实现小节中,我们会看到如何使用copy. deepcopy()

A real-life example

The Prototype design pattern is all about cloning an object. Mitosis, the process in a cell division by which the nucleus divides resulting in two new nuclei, each of which has exactly the same chromosome and DNA content as the original cell, is an example of biological cloning [j.mp/mmitosis].

原型设计模式全都是关于克隆一个对象的。

The following figure, provided by www.sourcemaking.com, shows an example of the mitotic division of a cell [j.mp/pprotpat]:

在下面的由www.sourcemaking.com提供的图表中,

图片:略

Another popular example of (artificial) cloning is Dolly, the sheep [j.mp/wikidolly].

另外一个流行的克隆案例是多莉羊🐑。

A software example

There are many Python applications that make use of the Prototype pattern [j.mp/ pythonprot], but it is almost never referred to as Prototype since cloning objects is a built-in feature of the language.

有很多的Python应用使用了原型模式,但几乎从不提及原型,因为克隆对象是这门语言的一个内置功能。

One application that uses Prototype is the Visualization Toolkit (VTK) [j.mp/pyvto]. VTK is an open source cross-platform system for 3D computer graphics, image processing, and visualization. VTK uses Prototype for creating clones of geometrical elements such as points, lines, hexahedrons, and so forth [j.mp/vtkcell].

Another project that uses Prototype is music21. According to the project's page, "music21 is a set of tools for helping scholars and other active listeners answer questions about music quickly and simply" [j.mp/pmusic21]. The music21 toolkit uses Prototype for copying musical notes and scores [j.mp/py21code].

Use cases

The Prototype pattern is useful when we have an existing object and we want to create an exact copy of it. A copy of an object is usually required when we know that parts of the object will be modified but we want to keep the original object untouched. In such cases, it doesn't make sense to recreate the original object from scratch [j.mp/protpat].

当我们拥有一个现有对象,而且想要创建一个此对象的完全拷贝时原型模式就很有用了。

Another case where Prototype comes in handy is when we want to duplicate a complex object. By duplicating a complex object, we can think of an object that is populated from a database and has references to other objects that are also populated from a database. It is a lot of effort to create an object clone by querying the database(s) multiple times again. Using Prototype for such cases is more convenient.

So far, we have covered only the reference versus copy issue, but a copy can be further divided into a deep copy versus a shallow copy. A deep copy is what we have seen so far: all data of the original object are simply copied in the clone, without making any exceptions. A shallow copy relies on references. We can introduce data sharing, and techniques like copy-on-write to improve the performance (such as clone creation time) and the memory usage. Using shallow copies might be worthwhile if the available resources are limited (such as embedded systems) or performance is critical (such as high-performance computing).

到目前为止,我们只说到了引用与复制的问题,但是复制又被进一步分为深拷贝和浅拷贝。我们目前所见到的深拷贝是:

In Python, we can do shallow copies using the copy.copy() function. Quoting the official Python documentation, the differences between a shallow copy (copy. copy()) and a deep copy (copy.deepcopy()) in Python are [j.mp/py3copy] as follows:

在Python中,我们可以使用函数copy.copy()执行浅拷贝。引用Python官方文档来说,Python中的浅拷贝和深拷贝的区别在于以下两点:

  • "A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.

  • A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original."

  • “”

Can you think of any examples where using shallow copies is better than using deep copies?

Implementation 实现

In programming, it is not uncommon for a book to be available in multiple editions. For example, the classic textbook on C programming The C Programming Language by Kernighan and Ritchie is available in two editions. The first edition was published in 1978. At that time, C was not standardized. The second edition of the book was published 10 years later and covers the standard (ANSI) version of C. What are the differences between the two editions? To mention a few, the price, the length (number of pages), and the publication date. But there are also many similarities: the authors, the publishers, and the tags/keywords that describe the book are exactly the same. This indicates that creating a new book from scratch is not always the best approach. If we know that there are many similarities between two book editions, we can use cloning and modify only the different parts of the new edition.

Let's see how we can use the Prototype pattern for creating an application that shows book information. We begin with the representation of a book. Apart from the usual initialization, the Book class demonstrates an interesting technique. It shows how we can avoid the telescopic constructor problem. In the __init__() method, only three parameters are fixed: name, authors, and price. But clients can pass more parameters in the form of keywords (name=value) using the rest variable-length list. The line self.__dict__.update(rest) adds the contents of rest to the internal dictionary of the Book class to make them part of it.

But there's a catch. Since we don't know all the names of the added parameters, we need to access the internal dict for making use of them in __str__(). And since the contents of a dictionary do not follow any specific order, we use an OrderedDict to force an order; otherwise, every time the program is executed, different outputs will be shown. Of course, you should not take my words for granted. As an exercise, remove the usage of OrderedDict and sorted() and run the example to see if I'm right:

class Book:
    def __init__(self, name, authors, price, **rest):
       '''Examples of rest: publisher, length, tags, publication
       date'''
       self.name = name
       self.authors = authors
       self.price = price      # in US dollars
       self.__dict__.update(rest)
    def __str__(self):
       mylist=[]
       ordered = OrderedDict(sorted(self.__dict__.items()))
       for i in ordered.keys():
           mylist.append('{}: {}'.format(i, ordered[i]))
           if i == 'price':
               mylist.append('$')
           mylist.append('\n')
       return ''.join(mylist)

The Prototype class implements the Prototype design pattern. The heart of the Prototype class is the clone() method, which does the actual cloning using the familiar copy.deepcopy() function. But the Prototype class does a bit more than supporting cloning. It contains the register() and unregister() methods, which can be used to keep track of the objects that are cloned in a dictionary. Note that this is just a convenience, and not a necessity.

Moreover, the clone() method uses the same trick that __str__() uses in the Book class, but this time for a different reason. Using the variable-length list attr, we can pass only the variables that really need to be modified when cloning an object as follows:

class Prototype:
       def __init__(self):
           self.objects = dict()
       def register(self, identifier, obj):
           self.objects[identifier] = obj
       def unregister(self, identifier):
           del self.objects[identifier]
       def clone(self, identifier, **attr):
           found = self.objects.get(identifier)
           if not found:
               raise ValueError('Incorrect object identifier:{}'.format(identifier))
           obj = copy.deepcopy(found)
           obj.__dict__.update(attr)
           return obj

The main() function shows The C Programming Language book cloning example mentioned at the beginning of this section in practice. When cloning the first edition of the book to create the second edition, we only need to pass the modified values of the existing parameters. But we can also pass extra parameters. In this case, edition is a new parameter that was not needed in the first book but is useful information for the clone:

def main():
       b1 = Book('The C Programming Language', ('Brian W. Kernighan',
       'Dennis M.Ritchie'), price=118, publisher='Prentice Hall',
       length=228, publication_date='1978-02-22', tags=('C',
       'programming', 'algorithms', 'data structures'))
       prototype = Prototype()
       cid = 'k&r-first'
       prototype.register(cid, b1)
       b2 = prototype.clone(cid, name='The C Programming Language(ANSI)', price=48.99, length=274,
       publication_date='1988-04-01', edition=2)
       for i in (b1, b2):
           print(i)
       print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2)))

Notice the usage of the id() function which returns the memory address of an object. When we clone an object using a deep copy, the memory addresses of the clone must be different from the memory addresses of the original object.

The prototype.py file is as follows:

import copy
from collections import OrderedDict


   class Book:
       def __init__(self, name, authors, price, **rest):
           '''Examples of rest: publisher, length, tags, publication
           date'''
           self.name = name
           self.authors = authors
           self.price = price      # in US dollars
           self.__dict__.update(rest)
       def __str__(self):
           mylist=[]
           ordered = OrderedDict(sorted(self.__dict__.items()))
           for i in ordered.keys():
               mylist.append('{}: {}'.format(i, ordered[i]))
               if i == 'price':
                   mylist.append('$')
               mylist.append('\n')
           return ''.join(mylist)


   class Prototype:
       def __init__(self):
           self.objects = dict()
       def register(self, identifier, obj):
           self.objects[identifier] = obj
       def unregister(self, identifier):
           del self.objects[identifier]
       def clone(self, identifier, **attr):
           found = self.objects.get(identifier)
           if not found:
               raise ValueError('Incorrect object identifier:{}'.format(identifier))
           obj = copy.deepcopy(found)
           obj.__dict__.update(attr)
            return obj


   def main():
       b1 = Book('The C Programming Language', ('Brian W. Kernighan',
       'Dennis M.Ritchie'), price=118, publisher='Prentice Hall',
       length=228, publication_date='1978-02-22', tags=('C',
       'programming', 'algorithms', 'data structures'))
       prototype = Prototype()
       cid = 'k&r-first'
       prototype.register(cid, b1)
       b2 = prototype.clone(cid, name='The C Programming Language (ANSI)', price=48.99, length=274,
       publication_date='1988-04-01', edition=2)
       for i in (b1, b2):
           print(i)
       print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2)))


   if __name__ == '__main__':
       main()

The output of id() depends on the current memory allocation of the computer and you should expect it to differ on every execution of this program. But no matter what the actual addresses are, they should not be the same in any chance.

A sample output when I execute this program on my machine is as follows:

>>> python3 prototype.py
authors: ('Brian W. Kernighan', 'Dennis M. Ritchie')
length: 228
name: The C Programming Language
price: 118$
publication_date: 1978-02-22
publisher: Prentice Hall
tags: ('C', 'programming', 'algorithms', 'data structures')

authors: ('Brian W. Kernighan', 'Dennis M. Ritchie')
edition: 2
length: 274
name: The C Programming Language (ANSI)
price: 48.99$
publication_date: 1988-04-01
publisher: Prentice Hall
tags: ('C', 'programming', 'algorithms', 'data structures')
ID b1 : 140004970829304 != ID b2 : 140004970829472

Indeed, Prototype works as expected. The second edition of The C Programming Language book reuses all the information that was set in the first edition, and all the differences that we defined are only applied to the second edition. The first edition remains unaffected. Our confidence can be increased by looking at the output of the id() function: the two addresses are different.

As an exercise, you can come up with your own example of Prototype. A few ideas are as follows:

  • The recipe example that was mentioned in this chapter
  • The database-populated object that was mentioned in this chapter
  • Copying an image so that you can add your own modifications without touching the original

Summary 总结

In this chapter, we have seen how to use the Prototype design pattern. Prototype is used for creating exact copies of objects. Creating a copy of an object can actually mean two things:

本章,我们见到了如何如何使用原型设计模式。原型用来创建精确地对象副本。创建一个对象的副本实际上意味着两件事情:

  • Relying on references, which happens when a shallow copy is created

  • 当执行浅拷贝时,视所引用的原始内容而定。

  • Duplicating everything, which happens when a deep copy is created

  • 在执行深拷贝时,重复所有内容。

In the first case, we want to focus on improving the performance and the memory usage of our application by introducing data sharing between objects. But we need to be careful about modifying data, because all modifications are visible to all copies. Shallow copies were not introduced in this chapter, but you might want to experiment with them.

在第一个例子中,我们希望将注意力放在通过引入对象之间的数据共享对以提高性能以及内存利用上面。但是修改数据时我们需要特别小心,因为这些修改对于所有的复制都是可见的。本章我们不会介绍浅拷贝的,不过你可以自己了解了解。

In the second case, we want to be able to make modifications to one copy without affecting the rest. That is useful for cases like the cake-recipe example that we have seen. Here, no data sharing is done and so we need to be careful about the resource consumption and the overhead that is introduced by our clones.

在第二个例子中,我们徐昂能够修改一个副本而不会影响到余下的副本。

We showed a simple example of a deep copying which in Python is done using the copy.deepcopy() function. We also mentioned examples of cloning found in real life, focusing on mitosis.

我们演示了在Python中使用copy.deepcopy()函数的深拷贝的简单例子。我们也谈到了真实生活中存在的一个关于克隆寻找的例子,即细胞的有丝分裂。

Many software projects use Prototype, but in Python it is not mentioned as such because it is a built-in feature. Among them are the VTK, which uses Prototype for creating clones of geometrical elements, and music21, which uses it for duplicating musical scores and notes.

很多的软件想都用到了原型,但是在Python中它却并未被提及,因为它是一个内建的功能。

Finally, we discussed the use cases of Prototype and implemented a program that supports cloning books so that all information that does not change in a new edition can be reused, but at the same time modified information can be updated and new information can be added.

最后,我们讨论了原型的使用案例,实现了一个支持克隆登记的例子,这样在一个能够重复使用的新版本中所有的信息都不会被改变,但是在同一时间修改的信息能够被更新而且新信息也可以增加。

Prototype is the last creational design pattern covered in this book. The next chapter begins with Adapter, a structural design pattern that can be used to make two incompatible software interfaces compatible.

原型