面向对象(Object-Oriented,OO)是当下软件开发的主流方法。在OO分析与设计中,我们首先从问题领域中抽象出领域模型,在领域模型中以适当的粒度归纳出相关的类;然后定义各个类之间的关联关系,并给这些类分配相应的职责,同时定义这些类之间的协作方式。将相应的职责分配给具体的类是OO过程中非常重要的一步。GRASP设计模式是职责分配过程中的一套非常重要的设计模式。它给出了在给类分配职责的过程中,设计者们所需要遵从的一些原则或者指导性的建议。
说到设计模式,更为人所知的当然是GoF(Gang of Four)的23种设计模式。与GoF的23种设计模式不同的是,GRASP设计模式描述的是在OO设计中为互相协作的类分配职责的原则或者建议,而GoF的设计模式则是在更高的层次上描述一个OO系统或者其局部系统的行为以及结构上的抽象。
GRASP设计模式的全称是General Responsibility Assignment Software Patterns,即通用职责分配软件模式。它定义了9个基本的OO设计原则或基本的设计构件。这9个设计模式分别是:创建者(Creator)、信息专家(Information Expert)、低耦合(Low Coupling)、控制器(Controller)、高内聚(High Cohesion)、多态性(Polymorphism)、纯虚构(Pure Fabrication)、间接性(Indirection)、防止变异(Protected Variations)。
一、创建者(Creator)
创建者模式关注这样一个问题:假设系统中存在一个类A,那么在这个系统中,谁应该负责创建类A的新实例?
创建类的实例是面向对象的系统中最常见的活动之一。合理分配类的创建职责的设计能够支持低耦合,提高封装性、可复用性、可扩展性。创建者模式为这一活动给出的指导性的建议是,将创建类A的新实例的职责分配给类B,如果以下条件成立:
B“包含”或组成聚集A。
B记录A。
B直接使用A。
B具有A的初始化数据,并且在创建A的实例时会将这些数据传递给A。
这些条件成立得越多越好。如果有一个以上的条件成立,那么通常首选聚集或包含A的类B。
举一个非常简单的例子。假设系统中存在链表类和节点类。一个链表类的对象包含了多个节点的对象,链表类的用户(这里的用户不是指现实世界中的人,而是指使用链表类的代码)可以向一个链表类的对象插入一个新的节点。那么,被插入的新的节点对象应该由谁来创建?显然,链表类的用户本身是一个创建者的候选者,因为用户本身拥有节点对象的初始化数据,由用户创建节点对象是可以实现的。然而,根据创建者模式,链表类却是一个更好的选择。因为链表类包含了节点,并直接使用、操作节点。由链表类本身创建节点可以消除链表类的用户对于节点类的依赖关系,从而消除了用户代码与节点类的耦合,使系统中仅剩下用户代码与链表类的耦合。这形成了一个良好的设计。
使用创建者模式的好处是不会增加系统的耦合度,因为根据创建者模式的建议,类的实例的创建者已经与这个类存在着某种形式的耦合。因此该模式支持低耦合的设计,能产生具有较低的维护依赖性与较高的复用性的系统。
在一个设计灵活的OO系统中,对象的创建方式往往非常复杂。比如,有些系统需要为了更好的性能而集中创建或者使用回收的实例(线程池、连接池、对象池等);有些系统需要根据某些条件来创建一族类的实例;甚至有些框架系统在框架的编写过程中根本不知道需要实例化哪一个类,等等。在这些情况下,最好的方法是将创建类的实例的职责委派给抽象工厂(Abstract Factory)、具体工厂(ConcreteFactory)、创建器(Builder)等等辅助类,而不是创建者模式所建议的类。
二、信息专家(InformationExpert)
信息专家模式关注这样一个问题:给对象分配职责的基本原则是什么?
在一个OO系统中可能会定义成百上千个软件类,而所有这些类必须履行的职责的数量之和甚至更多。如果能很好地给所有这些类分配好职责,那么这个系统就会易于理解、维护和扩展。信息专家模式关于这个问题给出的答案是:把职责分配给信息专家,它具有实现这个职责所必需的信息。
这一设计模式理解起来非常简单,它并不是某种深度认证的结论,而更像是一种直觉,即对象完成与它所具有的信息相关的那些职责。有许多例子都能说明这一点,其中最常见的包括各种GUI库的绘图函数。比如iOS的UIKit框架中的layoutSubviews方法、wxWidgets库中的OnPaint方法、MFC框架中的OnPaint方法、Swing库中的paint/updata/repaint方法,等等。这些方法都是将GUI界面的绘制功能分配到了每一个具体的视图类中,无论是库/框架中已经存在的具体视图,或者是用户自定义的具体视图。因为只有这些具体的视图类才拥有绘制自身所必需的信息,它们能很好地履行绘制自身的这一职责。这是信息专家模式的一个直接应用。
在信息专家模式给出的建议中,由于对象可以使用自身的信息来完成它的职责,因此信息的封装性得以维护,从而支持了低耦合;同时,由于类的职责都根据自身所拥有的信息来分配,因而该模式也支持高内聚的设计。
当然,在某些特殊的情况下,信息专家模式也许并不适用,这通常是由于内聚与耦合的问题所产生的。比如许多后台系统都有把模型(Model)类的数据存入数据库的功能。这一职责的履行所必需的信息显然是存在于各个模型类中,按照信息专家模式给出的建议,应该让这些模型类来完成把自身的数据保存到数据库中的功能。但是,这样的设计会导致内聚与耦合方面的问题。首先,在这样的设计中,所有的模型类都必须包含与数据库处理相关的逻辑,如与JDBC相关的处理逻辑。这使得模型类由于其他职责的存在而降低了它的内聚。其次,这样的设计也为所有的模型类都引入了与JDBC的耦合关系,使得系统的耦合度上升。甚至,这种设计也会导致大量重复、冗余的数据库逻辑存在于整个系统中的各个角落,这也违反了设计要分离主要的系统关注的基本架构原则。因此,在这种情况下,信息专家模式需要我们结合整个系统的耦合和内聚做出另外的考虑。
三、低耦合(LowCoupling)
低耦合模式关注这样一个问题:怎样降低依赖性,减少变化带来的影响,提高重用性?
低耦合模式关于这个问题给出的答案是:分配职责,使耦合度尽可能低。
耦合是系统设计中最重要的概念之一,也是设计中真正的基本原则之一。所谓耦合,指的是对某元素与其他元素之间的连接、感知和依赖程度的度量。在一个OO系统中,所有的耦合形式可分为5类:
零耦合(nil coupling):两个类丝毫不依赖于对方。
导出耦合(export coupling):一个类依赖于另一个类的公有接口。
授权耦合(overt coupling):一个类经允许,使用另一个类的实现细节。
自行耦合(covert coupling):一个类未经允许,使用另一个类的实现细节。
暗中耦合(surreptitious coupling):一个类通过某种方式知道了另一个类的实现细节。
零耦合当然是耦合度最低的。两个丝毫互不依赖的类,意味着在维护和扩展系统时,可以随意地去掉或者修改其中的一个类而丝毫不会影响到另一个类。但是,只使用零耦合却无法创建出一个有意义的OO系统,因为所有的类都是独立、不相关的,相互之间没有消息的传递,这样最多只能创建出一个类库。导出耦合具有相当低的耦合度,因为在导出耦合中,一个类只依赖另一个类的公有接口。在一个设计良好的系统中,消息的传递只会通过类的公有接口进行,因而导出耦合可以很好地支持系统的可维护性与可扩展性。除此之外,授权耦合、自行耦合、以及暗中耦合都是耦合程度比较高的耦合形式。
有这样一条OO设计的经验原则:类与类之间应该零耦合,或者只有导出耦合关系。也即,一个类要么同另一个类毫无关系,要么就只使用另一个类提供的公有接口。授权耦合、自行耦合、暗中耦合基本上不应该在系统中被使用到。
类A到类B或接口B的常见的导出耦合形式有:
A具有引用B的实例或B自身的属性。
A的实例调用B的实例的服务。
A具有以任何形式引用B的实例或B自身的方法。
A是B的直接或间接子类。
B是接口,而A是此接口的实现。
低耦合模式提倡职责分配要避免产生具有负面影响的高耦合。低耦合模式支持在设计时降低类的依赖性,减少变化所带来的影响。比如在通常情况下,系统往往能以使用关系来替换继承关系,因为继承关系是一种耦合程度非常高的强耦合形式,而使用关系能降低耦合度。
当然,没有绝对的度量标准来衡量耦合程度的高低。使用低耦合模式的目的是为了创建一个可灵活伸缩的、可维护的、可扩展的系统。在这个目的之下,低耦合不能脱离信息专家和高内聚等其他模式孤立地考虑,而是应该同时权衡耦合与内聚。高耦合本身也并不是问题之所在,问题是与某些方面不稳定的元素之间的高耦合,这种高耦合会严重影响系统将来的维护性和扩展性。而比如所有的Java系统都能安全地将自己去Java库(java.lang,java.util等)进行耦合,因为Java库是稳定的,与Java库的耦合不会给系统的灵活性、维护性、扩展性带来什么问题。
四、控制器(Controller)
控制器模式关注这样一个问题:在UI层之上首先接收和协调(控制)系统操作的第一个对象是什么?
系统操作就是系统中的主要输入事件,比如按钮的点击、文字的输入等。控制器模式对于这个问题的回答是:使用控制器作为UI层之上的第一个对象,它负责接收和处理系统操作消息。而控制器是能代表以下选择之一的类:
代表整个“系统”、“根对象”、运行软件的设备或主要子系统,这些是外观控制器的变体。
代表用例场景,在该场景中发生系统事件,通常命名为<UseCaseName>Handler、<UseCaseName>Coordinator或<UseCaseName>Session。
在系统中,诸如“窗口”(Windows)、“视图”(View)或“文档”(Document)之类的类主要负责显示的功能,并不属于控制器。这些类不应该完成与系统事件相关的任务。通常情况下,它们接收这些事件,并将其委派给控制器,即委派模式。
例如,在iPhone应用程序的开发框架UIKit中,许多继承自UIView类的具体视图类都具有一个叫做delegate的属性。其中包括UIScrollView类,它表示一个滚动视图,添加到其中的子视图的尺寸可以超过滚动视图自身的尺寸,并在滚动视图中进行滚动显示。而所有UIScrollView类的实例都可以指定一个委派delegate,其类型是UIScrollViewDelegate(接口)。用户可以自行实现UIScrollViewDelegate这个接口,定义好处理各类事件的方法,比如滚动开始事件、滚动结束事件等等。当UIScrollView类的实例接收到系统操作之后,该实例会将这些操作委派给用户指定的delegate进行处理,即该delegate中相应的处理方法会得到调用。
控制器设计中的常见缺陷是分配的职责过多。这时,控制器会具有不好的低内聚。因而存在这样一条准则:正常情况下,控制器应当把需要完成的工作委派给其他的对象。控制器只是协调或控制这些活动,本身并不完成大量工作。
五、高内聚(HighCohesion)
高内聚模式关注这样一个问题:怎样保持对象是有重点的、可理解的、可管理的,并且能够支持低耦合?
高内聚模式关于这个问题给出的答案是:分配职责,使其可保持较高的内聚性。
同耦合一样,内聚也是系统设计中最重要的概念之一,也是设计中真正的基本原则之一。所谓内聚(内聚有多种类型,包括偶然内聚、逻辑内聚、时间内聚、通信内聚、顺序内聚、功能内聚、信息内聚等,这里主要指的是功能内聚),是对元素(包括类、子系统等)职责的相关性和集中度的度量。如果元素具有高度相关的职责,而且没有过多工作,那么该元素具有高内聚性。
高内聚的类的方法数量较少,在功能性上有非常强的关联,而且不需要做太多的工作。如果任务的规模比较大的话,应该将任务所涉及到的各项职责按照关联的强度分配到各个类中,然后让各个类的对象进行相互协作,共同完成这项任务。高内聚的类优势明显,它易于理解、维护和复用。高度相关的功能性与少量操作相结合,也可以简化维护和改进的工作。细粒度的、高度相关的功能性也可以提高复用的潜力。
以信息专家模式一节中最后一段中所举的例子来说,如果按照信息专家给出的建议,把与数据库进行交互的职责都放进模型类中,那么这样的模型类显然做了过多的工作。这是一种不好的低内聚设计。从数据的角度来讲,内聚程度低的模型类由于加入了数据库操作职责,它需要在一个对象中同时维护两大块数据,一块是模型本身的数据,另一块则是数据库操作需要用到的数据。用于进行数据库操作的数据显然会在各个模型类的对象中形成冗余。从功能的角度来讲,数据库操作功能的加入直接导致了模型类中包含了两种截然不同的功能和逻辑,这使得模型类更加难以理解、复用和维护。这样的设计是十分脆弱的,会经常容易受到变化的影响。而一个高内聚的设计则是将数据库相关逻辑从模型类中移除,放入专门的对象或者子系统中。这样,模型类自身便能拥有更高的内聚度,它们所包含的功能容易被人理解;同时,在面向接口的良好设计中,模型类也更加容易被维护以及扩展。
在实践中,高内聚模式也不能脱离其他模式(如信息专家和低耦合)单独地考虑。高内聚与低耦合是在整个系统的设计过程中,需要不断地去考虑和评估的基本原则。
在少数情况下,较低的内聚也是被接受的。比如,为了方便专门的数据库逻辑人员统一管理SQL语句,系统中往往可以将与SQL语句相关的操作都放在一个独立的全能类中;另外,出于性能的考虑,在RPC中使用一个粗粒度的RPC服务器类,既可以减少服务器上对象的数目,可以减少网络请求和连接的数目,从而提高系统的性能。这些,都需要从系统的全局出发,结合多种设计的原则进行权衡考虑。
六、多态性(Polymorphism)
多态性模式关注这样一个问题:如何处理基于类型的选择?如何创建可插拔的软件构件?
关于这个问题,多态性模式给出的答案是:当相关选择或行为随类型(类)有所不同时,使用多态操作为变化的行为类型分配职责。不要测试对象的类型,也不要使用条件逻辑来执行基于类型的不同选择。
例如,在一个电子商务的系统中,有一个Order类用来表示用户提交的一份订单,其中包含了订单所需要的数据,如商品ID列表、每种商品对应的数量等等;以及相关的一些行为,如计算商品的总价格等等。根据信息专家模式给出的建议,计算该订单给消费者产生了多少消费税的这一功能,也应该由Order类来完成(假设方法名叫calculateTax)。然而,根据消费者所在国家的不同,消费税的计算方式明显是不一样的。有一种解决方案是,将用户所在国家的名称以字符串或枚举的形式,连同消费总值一起,作为参数传递给calculateTax方法。然后在calculateTax方法中,通过一连串的if else语句来判断参数传入的是哪个国家,并给出相应的计算代码。
这种方法的确能在某种程度上解决问题。但是这种方案极不利于系统的维护和扩展。首先,设想一下该商务系统支持100多个国家的业务,那么程序中的100多个elseif语句会是怎样一种壮观的景象。这是极不利于系统的维护者去阅读甚至查找某一个国家对应的elseif语句。其次,一旦将来停止了某些国家的业务,或者需要增加新国家的业务,或者需要修改已有的一些国家的税率,除了需要在一大堆的elseif中做一些很繁琐的工作之外,Order这个类本身的代码是需要被重新编译才能继续使用的。这无疑给维护和扩展带来了麻烦。
问题的另一种解决方案则是利用多态性模式。系统首先定义出一个接口,比如叫做TaxCalculator。该接口定义了唯一一个方法,calculateTax,该方法根据消费总值计算消费税。然后,对于每一个国家的不同的消费税计算方法,系统再分别定义专门的类去实现这个接口。比如,计算英国消费者的消费税的类为UKTaxCalculator,计算美国消费者的消费税的类为USTaxCalculator,等等。所有这些类都根据自身所代表的国家的具体消费税计算方法来实现了calculateTax方法。也即,前一种解决方案中的100多个elseif语句中的内容被分拆到了100多个类中。现在,当Order类需要计算消费税的时候,传递给它的calculateTax方法的参数就只需要一个消费总值和一个TaxCalculator的引用了,并且它本身不需要做任何具体的计算,它只需要将这一操作下派给这个引用就可以了。
上面的这个例子其实就是GoF设计模式中的策略模式,这的确是多态性模式的一个经典的应用场景。通过多态性模式,系统分离了接口与具体的实现,从而在保持接口不变的情况下,可以非常容易地对实现进行维护和扩展。并且,在上面的例子中,对具体策略类的各种修改都不需要在对Order类进行重编译。
多态性模式是OO设计的一个基本原则。
七、纯虚构(PureFabrication)
纯虚构模式关注这样一个问题:当你并不想违背高内聚和低耦合或其他目标,但是基于专家模式所提供的方案又不合适时,哪些对象应该承担这一职责?
OO设计中的领域模型是对领域内的概念内或现实世界中的对象的模型化表示。创建领域模型的关键思想是减小软件人员的思维与软件模式之间的表示差异。因此,在OO设计时,系统内的大多数类都是来源于现实世界中的真实类。然而,在给这些类分配职责时,有可能会遇到一些很难满足低耦合高内聚的设计原则。纯虚构模式对这一问题给出的方案是:给人为制造的类分配一组高内聚的职责,该类并不代表问题领域的概念,而代表虚构出来的事物,用以支持高内聚、低耦合和复用。
纯虚构模式强调的是职责应该置于何处。一般来说,纯虚构模式会通过表示解析或者行为解析来确定出一些纯虚构类,用于放置某一类职责。理想状况下,分配给这种虚构物的职责是要支持高内聚低耦合的,从而使整个系统处于一种良好的设计之中。
例如,在信息专家模式的最后一段所举的例子中提到,许多后台系统都需要对数据库进行操作,将系统中的一些对象进行持久化。信息专家模式给出的建议是将持久化的职责分配给具体的每一个模型类。但是这种建议已经被证明是不符合高内聚低耦合原则的,因为不会被采纳。于是,设计者往往会在系统中加入类似于DAO或者PersistentStorage这样的类。这些类在领域模型中是并不存在的,它们完全由设计者根据系统的行为而虚构得到。然而,这些类的引入,使得操作数据库进行持久化这种高内聚的职责可以顺理成章地分配给它们。从而在整个系统中实现了比较好的内聚和耦合。
在使用纯虚构模式时,不能毫无限制地对系统中的各种行为进行解析并纯虚构。如此往往会导致系统中大量的行为对象的存在,这样会对耦合产生不良的影响。
八、间接性(Indirection)
间接性模式关注这样一个问题:为了避免两个或多个事务之间直接耦合,应该如何分配职责?如何使对象解耦合,以支持低耦合并提高复用性潜力?
间接性模式对此的回答是:将职责分配给中介对象,使其作为其他构件或服务之间的媒介,以避免它们之间的直接耦合。中介则实现了其他构件之间的间接性。
间接性模式的思想比较简单,即通过一个中介就能消除许多的耦合。在GoF的23种设计模式中,有许多模式都利用到了间接性的思想。比如桥接模式中,设计将抽象部分与其实现部分相分离,利用的就是在客户与实现之间增加了一个抽象层次。外观模式则是在整个子系统与客户之间增加了一个便于用户使用的外观类作为中介。而中介者模式中的中介者则更是典型的例子。
九、防止变异(ProtectedVariations)
防止变异模式关注这样一个问题:如何设计对象、子系统和系统,使其内部的变化或不稳定性不会对其他元素产生不良影响?
防止变异模式的回答是:识别预计变化或不稳定之处,分配职责用以在这些变化之外创建稳定的接口。
防止变异(PV)是非常重要和基本的软件设计原则,几乎所有的软件或架构设计技巧都是防止变异的特例。PV是一个根本原则,它促成了大部分编程和设计的模式和机制,用来提供灵活性和防止变化。在软件设计中,除了数据封装、接口、多态、间接性等机制是PV的核心机制之外,没有一种固定的或者是通用的办法能够防止一切变化的产生。因此PV的实现依赖的是一系列的OO设计方面的经验性原则,用以产生一个设计良好的高内聚、低耦合的系统,从而支持PV。这里简要讨论6个软件设计方面的原则:
单一职责原则(SingleResponsibility Principle, SRP)
一个类最好只负责一项单一的职责,只有一个引起它变化的原因。
这里的单一职责,或者单一的变化原因,并非指严格意义上的唯一的一个职责或者原因,而应该是指一类联系紧密的职责,或者说单一的变化动机。SRP其实是高内聚的一种形式,它要求一个类紧密围绕着一项职责进行工作,是一种内聚程度较高的设计。
里氏替换原则(LiskovSubstitution Principle, LSP)
如果对于类型S的每个对象o1存在类型T的对象o2,那么对于所有定义了T的程序P来说,当用o1替换o2并且S是T的子类型时,P的行为不会改变。
将上面的形式化定义换一种简单的说法就是:在一个系统中,任何类的对象都可以由该类的任何一个子类的任何对象给替换掉,而整个系统的行为不变。LSP是一种简单的思想,然而严格按照LSP去设计系统会使得所有基类的接口语义是完全稳定的,这非常有利于系统的扩展性。
依赖倒置原则(DependenceInversion Principle, DIP)
抽象不应该依赖于细节,细节应该依赖于抽象。
DIP的核心思想是面向接口编程。一个依赖接口实现的类要比一个依赖细节实现的类更容易维护和扩展。它来源于这样一个事实:相对于细节的多变性,抽象的东西要稳定得多。比如,在一个系统中,类A依赖于类B实现。那么,当类A需要改变对类B的依赖,转而依赖类C时,类A必须修改源代码才能实现。而如果类A依赖于一个接口X实现,而类B和类C都实现这个接口,那么之后无论是类B和类C之间的替换,抑或是让类A去依赖新的类D,都是非常易于实现的。DIP的本质也是降低了耦合,将一个类与细节的耦合降低到了与接口的耦合,而与一个稳定的接口之间耦合是良好的。
接口隔离原则(InterfaceSegregation Principle, ISP)
类不应该依赖它不需要的接口,一个类对另一个类的依赖应建立在最小的接口上。
ISP也是高内聚低耦合的一种表现形式。因为类对公有接口的依赖也是一种导出耦合的关系。如果一个类依赖了它不需要的接口,那么在系统中便存在了这样一种没有意义的耦合,不利于耦合度的降低。反过来,在建立接口时也不要建立臃肿的、包含一切的接口。这样的接口反而失去了高内聚性。
迪米特法则(Law ofDemeter, LOD)
不要历经远距离的对象结构路径去向远距离的间接对象发送消息。
假设在一个系统中,存在A、B、C、D四个类。其中,类A包含了类B,类B包含了类C,类C包含了类D。那么,一个类A的实例a中一定会存在着一个类D的实例d,这是通过类B和类C间接包含进来的。那么,LOD禁止a向d直接发送任何消息,因为这直接引入了类A同类D之间毫无道理的耦合;同时这也是一种极为脆弱的设计,它无法应对将来可能出现的任何变化,对象路径越长,其稳定性就越差。LOD要求一个类在其方法里只给有限的对象发送消息,包括:自身、方法的参数、自身的属性、作为自身属性的集合中的元素、在方法中创建的对象。
开放-封闭原则(Open-ClosedPrinciple, OCP)
模块应该同时对扩展、可适应性开放和对影响客户的更改封闭。
也即,模块应该对扩展开放,对修改关闭。OCP其实是PV的另一种描述。
除了这些原则之外,还有许多各种各样的技术可以用来PV。这里就不再赘述。
在一个系统中,值得应用PV的地方是可能会产生变化的地方。其中之一是系统的需求定义的一些变化点,比如包含计算消费税程序的电子商务系统需要支持在新的国家开展业务。那么在计算消费税的程序里使用策略模式便能很好地支持这一点扩展。换句话说,如果需求中明确规定了在这一点上不允许有任何变化(实际上不太可能,这里只是假设),那么在计算消费税的程序里使用if else语句也未尝不可。
另外,设计者预测出来的可能会产生变化的点也可以应用PV。还是说上一个例子。计算消费税的需求规定了只包含三个国家,但是设计者如果预测出这是一个变化点,可能在将来会有各种变化,那么设计者同样可以利用策略模式来完成这一设计。但是,在预测变化点时,一定要小心谨慎,仔细考虑。否则很容易在系统中预测出大量的、实际上变化概率并不大的变化点出来,从而导致在整个系统中大量地出现了灵活的、使用模式的、支持变化的设计。而往往这些设计在整个软件的生命周期中都从未被用到过。这种现象被称为过度设计。这样的情况在软件开发的过程中是极其浪费资源的,应该努力去避免这种情况的发生。
参考资料:
Applying UML and Patterns: An Introduction to Object-OrientedAnalysis and Design and Iterative Development, Third Edition, by Craig Larman.
Object-Oriented Design Heuristics, First Edition, by Arthur J.Riel.
Design Patterns: Elements of Reusable Object-Oriented Software,First Edition, by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides.
原文链接:http://outofmemory.cn/wiki/grasp-pattern-explain
共同学习,写下你的评论
评论加载中...
作者其他优质文章