您的位置:澳门皇家赌场91资源站 > 澳门皇家赌场91资源站 > java面向对象-进度2

java面向对象-进度2

2019-12-14 09:24

SOLID原则是由著名的“Uncle Bob”(Robert C. Martin)所提出并且由5个软件开发原则组合在一起的。它们是一组面向对象设计(OOD)的指南,特别是关于类设计。这些原则非常受敏捷开发项目程序员的欢迎,但是却甚少被游戏开发者所利用。所以我将通过本篇文章详细介绍这些原则并阐述如何将其运用于游戏开发。

  提起面向对象,大家也许觉得自己已经非常“精通”了,起码也到了“灵活运用”的境界。面向对象设计不就是OOD吗?不就是用C++、Java、Smalltalk等面向对象语言写程序吗?不就是封装+继承+多态吗?

1.面向对象的五个基本原则

图片 1

  很好!大家已经掌握了不少对面向对象设计的基本要素:开发语言、基本概念、机制。Java是一种纯面向对象语言,是不是用Java写程序就等于面向对象了呢?我先列举一下面向对象设计的11个原则,测试一下大家对面向对象设计的理解程度~^_^~

三个基本元素:

solid principles(from doolwind.com)

 

1. 封装: 封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。
2. 继承: 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。 
3. 多态: 多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。

单一责任原则(Single Responsibility Principle)

  • 单一职责原则(The Single Responsibility Principle,简称SRP)
  • 开放-封闭原则(The Open-Close Principle,简称OCP)
  • Liskov替换原则(The Liskov Substitution,简称LSP)
  • 依赖倒置原则(The Dependency Inversion Principle,简称DIP)
  • 接口隔离原则(The Interface Segregation Principle,简称ISP)
  • 重用发布等价原则(The Reuse-Release Equivalence Principle,简称REP)
  • 共同重用原则(The Common Reuse Principle,简称CRP)
  • 共同封闭原则(The Common Close Principle,简称CCP)
  • 无环依赖原则(The No-Annulus Dependency Principle,简称ADP)
  • 稳定依赖原则(The Steady Dependency Principle,简称SDP)
  • 稳定抽象原则(The Steady Abstract Principle,简称SAP)

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

“类的改变总是只存在一个原因。”

 

五个基本原则:
①单一职责原则(Single-Resposibility Principle):一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
②开放封闭原则(Open-Closed principle):软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
③Liskov替换原则(Liskov-Substituion Principle):子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
④依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
⑤接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口。

图片 2

  其中1-5的原则关注所有软件实体(类、模块、函数等)的结构和耦合性,这些原则能够指导我们设计软件实体和确定软件实体的相互关系;6-8的原则关注包的内聚性,这些原则能够指导我们对类组包;9-11的原则关注包的耦合性,这些原则帮助我们确定包之间的相互关系。

single_responsibility_principle(from globalnerdy.com)


第一个原则是设置基础,如果能够正确遵循这一原则的话便能够创造出不错的效果。它指出每个类必须只拥有单一责任以及一个改变原因。确保每个类够小且够集中,从而让开发者清楚该去哪里找自己所需要的内容或者该在游戏中添加哪些特殊功能等。

 

为何就不能拥有多个责任?多个责任也就意味着每个独立的代码间存在着连结。这时候一种责任的改变将降低类的功能而导致它难以满足其它责任的要求,并且最终只能创造出一个糟糕的设计。“为何渲染API的改变会破坏整体游戏状态?”

1 单一职责原则(SRP)

就一个类而言,应该仅有一个引起它变化的原因。

 

  在SRP中,我们把职责定义为“变化的原因”。如果你能够想到多于一个动机去改变一个类,那么这个类就具有多于一个的职责。有时,我们很难注意到这一点,我们习惯于以组的形式去考虑职责。

1.1 Rectangle类

  例如,图2.1-1,Rectangle类具有两个方法,一个方法把矩形绘制在屏幕上,另一个方法计算矩形面积。

图片 3

图2.1-1 多于一个的职责

 

   有两个不同的应用程序使用Rectangle类。一个是有关计算几何学方面的,Rectangle类会在几何形状计算方面为它提供帮助,它从来不会在屏 幕上绘制矩形。另一个应用程序是有关图形绘制方面的,它可能进行一些几何学方面的工作,但是它肯定会在屏幕上绘制矩形。

  这个设计违反了SRP。Rectangle类具有两个职责。第一个职责提供了矩形几何形状数学模型;第二个职责是把矩形在一个图形用户界面上绘制出来。

  对于SRP的违反导致了一些严重的问题。首先,我们必须在计算几何应用程序中包含GUI代码。如果这是一个C++程序,就必须要把GUI代码链接进来,这会浪费链接时间、编译时间以及内存占用。如果是一个JAVA程序,GUI的.class文件必须要部署到目标平台。

   其次,如果Graphical Application的改变由于一些原因导致了Rectangle的改变,那么这个改变会迫使我们重新构建、测试已经部署Computational Geometry Application。如果忘记了这样作,Computational Geometry Application可能会以不可预测的方式失败。

  一个较好的设计是把这两个职责分离到图2.1-2中所示的两个完全不同的类中。这 个设计把Rectangle类中进行计算的部分移到GeometryRectangle类中,现在矩形绘制方式 的改变不会对Computational Geometry Application造成影响。 

 

图片 4

图2.1-2 分离的职责

1.2 结论

  SRP是所有原则中最简单的原则之一,也是最难正确运用的原则之一。我们会自然地把职责结合在一起。软件设计真正要做到的许多内容,就是发现职责,并把那些职责相互分离。事实上,我们要论述的其余原则都会以这样或那样的方式回到这个问题上。


 

修复代码以打破这一原则的方法便是将每个责任按照各自的类进行区分。第一步便是从每个责任中提取一个界面。从而让其它类能够依赖于这些界面而不是类本身。我们便可以基于不同责任(执行单一界面)将这些类区分为不同的类。

2 开放-封闭原则(OCP)

软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。

 

  遵循OCP设计出的模块具有两个主要的特征:

  1、  对于扩展是开放的(Open for extension)

  这意味着模块的行为是可以扩展的。当应用的需求变化时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。换句话说,我们可以改变模块的功能。

  2、  对于更改是封闭的(Closed for modification)

  对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是共享库、dll或者Java的jar文件,都无需改动。

 

  这两个特征好像是相互矛盾的。扩展模块行为的通常方式就是修改模块的源代码。不允许修改的模块常常都被认为是具有固定的行为。怎样可能在不改动模块源代码的情况下去更改它的行为呢?怎样才能在无需对模块进行改动的情况下就改变它的功能呢?——关键是抽象!

2.1 Shape应用程序

  我们有一个需要在标准GUI上绘制圆和正方形的应用程序。

你何时明确这一原则?——通常情况下打破这种原则的罪魁祸首便是拥有成百上千行的类。也就是我们所熟知的“GameObject”或“Entity”类,即人们总是会在其中盲目地添加各种代码。这种类通常会有500个以上的改变原因,这也就等于它将应对500个责任。所以这里总会不断涌现各种可怕的漏洞。

2.1.1 违反OCP

程序2.2.1.1-1 Square/Circle问题的过程化解决方案

------------------------------shape.h------------------------------

enum ShapeType {circle, square };

struct Shape
{
       ShapeType itsType;
}

------------------------------circle.h------------------------------

#include shape.h

struct Circle
{
       ShapeType itsType;
       double itsRadius;
       Point itsCenter;
};

------------------------------square.h------------------------------

#include shape.h 

struct Aquare
{
       ShapeType itsType;
       double itsSide;
       Point itsTopLeft;
};

------------------------------drawAllShapes.c------------------------------

#include shape.h
#include circle.h
#include square.h

typedef struct Shape* ShapePointer;

Void DrawAllShapes(ShapePointer list[], int n)

{

       int i;

       for (i = 0; i < n; i++)
      {
              struct Shape* s = list[i];

              switch (s->itsType)
              {
                     case square:
                            DrawSquare((struct Square*) s );
                            Break;

                     case circle:
                            DrawCircle((struct Circle*) s );
                            Break;
              }
       }
}

 

  DrawAllShapes函数不符合OCP,因为它对于新的形状类型的添加不是封闭的。如果希望这个函数能够绘制包含有三角形的列表,就必须更改这个函数。事实上每增加一种新的形状类型,都必须要更改这个函数。

  同样,在进行上述改动时,我们必须要在ShapeType enum中添加一个新的成员。由于所有不同种类的形状都依赖于这个enum的声明,所有我们必须要重新编译所有的形状模块。并且也必须要重新编译所有依赖于Shape类的模块。

 

   程序2.2.1.1-1中的解决方案是僵化的,这是因为增加Triangle会导致Shape、Square、Circle以及 DrawAllShapes的重新编译和重新部署。该方法是脆弱的,因为很可能在程序的其他地方也存在类似的既难以查找又难以理解的 switch/case或者if/else语句。该方法是牢固的,因为想在另一个程序中复用DrawAllShapes时,都必须附带上Square和 Circle,即使那个新程序不需要它们。因此该程序展示了许多糟糕设计的臭味。

开闭原则(Open Closed Principle)

2.1.2 遵循OCP

程序2.2.1.2-1 Square/Circle问题的OOD解决方案

class Shape
{
    public:
        virtual void Draw() const = 0;
};

class Square : public Shape
{
    public:
        virtual void Draw() const;
};


class Circle : public Shape
{
    public:
        virtual void Draw() const;
};

void DrawAllShapes(vector<Shape*>& list)
{
    vector<Shape*>::iterator i;
    for (i == list.begin(); i != list.end(); i++)
        (*i)->Draw();
}

 

  可以看到,如果我们要扩展程序2.2.1.2-1中 DrawAllShapes函数的行为,使之能够绘制一种新的形状,我们只需增加一个新的Shape派生类。DrawAllShapes函数并不需要改 动。这样DrawAllShapes就符合了OCP。无需改动自身的代码就可以扩展它的行为。实际上,增加一个Triangle类对于这里展示的任何模块 完全没有影响。很明显,为了能够处理Triangle类,必须改动系统中的某些部分,但是这里展示的所有代码都无需改动。

 

  这个程序是符合OCP的。对它的改动是通过增加新代码进行的,而不是更改现有的代码。因此,它就不会引起像不遵循OCP的程序那样的连锁改动。所需要的改动仅仅是增加新的模块,以及为了能够实例化新类型的对象而进行的围绕main的改动。

“软件实体(游戏邦注:也就是类,模块和函数等内容)应该能够扩展但却不易修改。”

2.1.3 是的,我说谎了

  上面的例子其实并非是100%封闭的!如果我们要求所有的圆必须在正方形之前绘制,那么程序2.2.1.2-1中DrawAllShapes函数无法对这种变化做到封闭。

  这就导致一个麻烦的结果,一般而言,无论模块是多么的封闭,都会存在一些无法对之封闭的变化。没有对于所有的情况都贴切的模型。

  既然不可能完全封闭,那么就必须有策略地对待这个问题。也就是说,设计人员必须对于他设计的模块应该对哪种变化封闭作出选择。他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。

 

   有句古老的谚语说:“愚弄我一次,应感羞愧的是你。再次愚弄我,应感羞愧的是我。”这也是一种有效的对待软件设计的态度。为了防止软件背负着不必要的复 杂性 ,我们会允许自己被愚弄一次。这意味着在我们最初编写代码时,假设变化不会发生。当变化发生时,我们就创建抽象来隔离以后发生的同类变化。简而言之,我们 愿意被第一颗子弹击中,然后我们会确保自己不再被同一支枪发射的其他任何子弹击中。

2.2 结论

   在许多方面,OCP是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处(也就是:灵活性、可重用性以及可维护性)。然而,并 不是说只要使用一种面向对象语言就是遵循了这个原则。对于应用程序中的每个部分都肆意地进行抽象同样不是一个好主意。正确的做法是,开发人员应该仅仅对程 序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。


 

图片 5

3 Liskov替换原则(LSP)

*子类型(subtype)必须能够替换掉它们的基类型(base type)。*

 

  Barbara Liskov首次写下这个原则是在1988年。她说道:

  这里需要如下替换性质:若对每个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,o1替换o2后,程序P行为和功能不变,则S是T的子类型。

 

  想想违反该原则的后果,LSP的重要性就不言而喻了。假设有一个函数f,它的参数为指向某个基类型B的指针或引用。同样假设某个B的派生类D,如果把D的对象作为B类型传递给f,会导致f出现错误行为。那么D就违反了LSP。显然D对f来说是脆弱的。

 

   f的编写者会想去对D进行一些测试,以便于在把D的对象传递给f时,可以使f具有正确的行为。这个测试违反了OCP,因为此f对于B的所有不同的派生类 都不再是封闭的。这样的测试是一种代码的臭味,它是缺乏经验的开发人员(或者,更糟的,匆忙的开发人员)在违反了LSP时所产生的结果。

3.1 正方形和矩形,微妙的违规

程序2.3.1-1 Rectangle类和Square类

class Rectangle
{
    public:
        void SetWidth(double w) {itsWidth = w;}
        void SetHeight(double h) {itsHeight = h;}
        double GetWidth() {return itsWidth;}
        double GetHeight() {return itsHeight;}
    private:
        Point itsTopLeft;
        double itsWidth;
        double itsHeight;
};

class Square : public Rectangle
{
    public:
        void SetWidth(double w) 
        {
             Rectangle::SetWidth(w);
             Rectangle::SetHeight(w);
        }

        void SetHeight(double h) 
        {
             Rectangle::SetWidth(h);
             Rectangle::SetHeight(h);
        }
};

 

       从一般意义上讲一个正方形就是一个矩形。因此,把Square类视为从Rectangle类派生是合乎逻辑的。

       IS-A关系的这种用法有时被认为是面向对象分析(OOA)的基本技术之一。一个正方形是一个矩形,所以Square类就派生自Rectangle类。不 过这个想法会带来一些微妙但极为严重的问题。一般来说,这些问题是难以预见的,直到我们编写代码时才会发现它们。

 

  我们 首先注意到出问题的地方是,Square类并不同时需要成员变量itsHeight和itsWidth。但是Square类仍会在Rectangle类中 继承它们。显然这是个浪费。在许多情况下,这种浪费是无关紧要的。但是,如果我们必须创建成百上千个Square对象,浪费的程度则是巨大的。

 

  假设目前我们并不十分关心内存效率。从Rectangle类派生Square类也会产生其他一些问题。请考虑下面这个函数:

void f (Rectangle& r)
{
    r.SetWidth(32);    // Calls Rectangle::SetWideth()
}

   如果我们向这个函数传递一个指向Square对象的引用,这个Square对象就会被破坏,因为他们的长并不会改变。这显然违反了LSP。以 Rectangle派生类的对象作为参数传入是,函数f不能正确运行。错误的原因是在Rectangle中没有把SetWidth和SetHeight声 明为虚函数,因此它们不是多态的。

 

  这个错误很容易修正。然而,如果派生类的创建会导致我们改变基类,这就常常意味着设 计是有缺陷的。当然也违反了OCP。也许有人会反驳说,真正的设计缺陷是忘记把SetWidth和SetHeight声明为虚函数,而我们已经作了修正。 可是,这很难让人信服,因为设置一个长方形的长和宽是非常基本的操作。如果不是预见到Square类的存在,我们凭什么要把这两个函数声明为虚函数呢?

 

  尽管如此,假设我们接受这个理由并修正这些类。

 

程序2.3.1-2 修正后的Rectangle类

class Rectangle
{
    public:
        virtual void SetWidth(double w) {itsWidth = w;}
        virtual void SetHeight(double h) {itsHeight = h;}
        double GetWidth() {return itsWidth;}
        double GetHeight() {return itsHeight;}
    private:
        Point itsTopLeft;
        Double itsWidth;
        Double itsHeight;
};

 

Open Closed Principle(from doolwind.com)

3.1.1 真正的问题

   现在Square和Rectangle看起来都能够正常工作。无论Square对象进行什么样的操作,它都和数学意义上的正方形保持一致。无论 Rectangle对象进行什么样的操作,它都和数学意义上的长方形保持一致。此外,可以向接受指向Rectangle的指针或引用的函数传递 Square,而Square依然保持正方形的特性,与数学意义上的正方形一致。

 

  这样看来,设计似乎是自相容的、正确的。可是,这个结论是错误的。一个自相容的设计未必就和所有的用户程序自相容。考虑下面的函数g:

 

void g (Rectangle& r)
{
    r.SetWidth(5);
    r.Setheight(4);
    assert(r.Area() == 20);
}

 

   这个函数认为所传递进来的一定是Rectangle,并调用了其成员函数SetWidth和SetHeight。对于Rectangle来说,此函数运 行正确,但如果传递进来的是Square对象就发生断言错误(assertion error)。所以,真正的问题是:函数g的编写者假设改变Rectangle的宽不会导致其长的改变。

 

  很显然,改变 一个长方形的宽不会影响它的长的假设是合理的!然而,并不是所有可以作为Rectangle传递的对象都满足这个假设。如果把一个Square类的实例传 递给g这样做了假设的函数,那么这个函数就会出现错误的行为。函数g对于Square/Rectangle层次结构来说是脆弱的。

 

  函数g的表现说明有一些使用指向Rectangle对象的指针或者引用的函数,不能正确地操作Square对象。对于这些函数来说,Square不能替换Rectangle,因此Square和Rectangle之间的关系是违反LSP的。

这一原则的目标是确保每个类尽可能频繁地发生改变,并能够将被用于多种情况中。尽管这两种要求听起来相互矛盾,但是它们却能够通过互补而创造出强大的设计。类能够进行扩展也就意味着随着类的行为能够根据需求改变而以新方式发生改变。而当这些改变都不需要任何源代码变化时类便不能进行修改。

3.1.2 IS-A是关于行为的

  那么究竟是怎么会使?Square和Rectangle这个显然合理的模型为什么会有问题呢?毕竟,Square应该就是Rectangle。难道他们之间不存在IS-A关系吗?

 

   对于那些不是g的编写这而言,正方形可以是长方形,但是从g的角度来看,Square对象绝对不是Rectangle对象。为什么!?因为Square 对象的行为方式和函数g所期望的Rectangle对象的行为方式不相容。从行为方式的角度来看,Square不是Rectangle,对象的行为方式才 是软件真正所关注的问题。LSP清楚地指出,OOD中IS-A关系是就行为方式而言的,行为方式是可以进行合理假设的,是客户程序所依赖的。

3.2 从派生类中抛出异常

  另一种LSP的违规形式是在派生类的方法中添加了其他基类不会抛出的异常。如果基类的使用者不期望这些异常,那么把它们添加到派生类的方法中就会导致不可替换性。此时要遵循LSP,要么就必须改变使用者的期望,要么派生类就不应该抛出这些异常。

3.3 有效性并非本质属性

  在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案。必须要根据设计的使用者做出的合理假设来审视它。

  有谁知道设计的使用者会做出什么样的合理假设呢?大多数这样的假设都很难预测。事实上,如果试图去预测所有这些假设,我们所得到的系统很可能会充满不必要的复杂性的臭味。因此,像所有其他原则一样,通常最好的方法只预测那些最明显的对于LSP的违反情况而推迟所有其他的预测,知道出现相关的脆弱性的臭味时,才去处理它们。

3.4 结论

  OCP是OOD中很多说法的核心。如果这个原则应用得有效,应用程序就会具有更多的可维护性、可重用性以及健壮性。LSP是使OCP成为可能的主要原则之一。正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。这种可替换性必须使开发人员可以隐式依赖的东西。因此,如果没有显式地强制基类类型的契约,那么代码就必须良好地并且明显地表达出这一点。

 

  俗语“IS-A”的含义过于宽泛以至于不能作为子类型的定义。子类型的正确定义是“可替换性的”,这里的可替换性可以通过显式或隐式的契约来定义。


 

我们可以使用受数据驱动的设计来解释这一原则。通过将所需要的配置数据转到类中,我们便可以轻松地扩展类而不需要对其做出修改。同时我们还应该将任何变量(从数学意义上看)转移到类中从而确保类本身的定义不会只是关于类本身的功能。如果是从基本的OOD原则的数据和操作来看,这应该是最简单的方法吧。类能够定义自身功能的操作以及相关操作数据。而我们则需要尽可能将这些数据转移到类/功能中。这将能够把类本身以外的数据配置转移到调用代码中,从而提高类的改变能力。

4 依赖倒置原则(DIP)

A、 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。

B、  抽象不应该依赖于细节,细节应该依赖于抽象。               

 

   这条原则的名字中使用“倒置”这个词,是由于许多传统的软件开发方法,例如结构化分析和设计,总是倾向于创建一些高层模块依赖于低层模块,策略 (policy)依赖于细节的软件结构。实际上这些方法的目的之一就是要定义子程序层次结构,该层次结构描述了高层模块怎样调用低层模块。第一章中1.2 节的Copy程序的初始设计就是这种层次结构的一个典型示例。一个设计良好的面向对象的程序,其依赖程序结构相对于传统的过程式方法设计的通常结构而言就 是被“倒置”了。

 

  请考虑一下高层模块依赖于低层模块时意味着什么。高层模块包含了一个应用程序的重要的策略选择和业务 模型。正是这些高层模块才使得其所在的应用程序区别于其他。然而,如果这些高层模块依赖于低层模块,那么对低层模块的改动就会直接影响到高层模块,从而迫 使它们依次做出改动。

 

  这种情形是非常荒谬的!本应该是高层的策略设置模块去影响低层的细节实现模块的。包含业务规则的模块应该优先于并独立于包含实现细节的模块。无论如何高层模块都不应该依赖于低层模块。

 

   此外,我们更希望能够重用的是高层的策略设置模块。我们已经非常擅长于通过子程序库的形式来重用低层模块。如果高层模块依赖于低层模块,那么在不同的上 下文中重用高层模块就会变得非常困难。然而,如果高层模块独立于低层模块,那么高层模块就可以非常容易的被重用。该原则是框架(framework)设计 的核心原则。

4.1 层次化

请看图2.4.1-1的层次化方案:

图片 6

 图2.4.1-1 简单的层次化方案

 

   图中,高层的Policy Layer使用了低层的Mechanism Layer,而Mechanism Layer又使用了更细节的层Utility Layer。这看起来似乎是正确的,然而它存在一个隐伏的错误特征,那就是:Policy Layer对于其下一直到Utility Layer的改动都是敏感的。这种依赖关系是传递的。Policy Layer依赖于某些依赖于Utility Layer的层次;因此Policy Layer传递性的依赖于Utility Layer。这是非常糟糕的。

 

  图 2.4.1-2展示了一个更为适合的模型。每个较高层次都为它所需的服务声明一个抽象接口,较低的层次实现了这个抽象接口,每个高层类都通过该抽象接口使 用下一层,这样,高层就不依赖于低层。低层反而依赖于在高层中声明的抽象服务接口。这不仅解除了Policy Layer对于Utility Layer的传递依赖关系,甚至也解除了Policy Layer对Mechanism Layer的依赖关系。

图片 7

图2.4.1-2 倒置的层次

 

  请注意这里的倒置不仅仅是依赖关系的倒置,它也是接口所有权的倒置。我们通常会认为工具库应该拥有它们自己的接口。但是当应用了DIP时,我们发现,往往是客户端拥有抽象接口,而它们的服务者这从这些抽象接口派生。

这应该是你最害怕签到的文件吧,因为似乎所有人都在使用这一文件。但是不管你致力于创造何种系统,总是不可避免需要用到这些文件。

4.1.1 倒置接口所有权

  这就是著名的Hollywood原则:“Don’t call us, we’ll call you.”(不要调用我们,我们会调用你。)低层模块实现了在高层模块中声明并被高层模块调用的接口。

 

   通过倒置接口所有权,对于Mechanism Layer或者Utility Layer的任何改动都不会在影响到Policy Layer。而且,Policy Layer可以在实现了Policy Service Interface的任何上下文中重用。这样,通过倒置这些依赖关系,我们创建了一个更灵活、更持久、更易改变的结构。

里氏替换原则(Liskov Substitution Principle)

4.1.2 依赖于抽象

  一个稍微简单但仍然非常有效的对于DIP的解释,是这样一个简单的启发式规则:“依赖于抽象 ”。这是一个简单的陈述,该启发式规则建议不应该依赖于具体类——也就是说,程序中所有的依赖关系都应该终止于抽象类或者接口。

 

根据启发式规则:

  • 任何变量都不应该持有一个指向具体类的指针或者引用
  • 任何类都不应该从具体类派生
  • 任何方法都不应该覆写它的任何基类中已经实现了的方法

 

   当然,每个程序都会有违反该规则的情况。有时必须要创建具体类的实例,而创建这些实例的模块将会依赖于它们。此外,该启发规则对于那些虽然是具体但却稳定(nonvolatile)的类来说似乎不太合理。如果一个具体类不太会改变,并且也不会创建其他类似的派生类,那么依赖于它并不会造成损害。

 

  例如,在大多数系统中,描述字符串的类都是具体的(如Java中的String类),而该类有时稳定的,也就是说,它不太会改变。因此,直接依赖于它不会造成损害。

  然而,我们在应用程序中所编写的大多数具体类都是不稳定的。我们不想直接依赖于这些不稳定的具体类。通过把它们隐藏在抽象接口的后面,可以隔离它们的不稳定性。

  这不是一个完美的解决方案。常常,如果不稳定类的接口必须变化时,这个变化一定会影响到该类的抽象接口。这种变化破坏了抽象接口维系的隔离性。

  由此可知,该启发规则对问题的考虑有点简单了。另一方面,如果看得远一点,认为是由客户来声明它需要的服务接口,那么仅当客户需要时才会对接口进行改变。这样,改变实现抽象接口的类就不会影响到客户。

4.2 结论

  使用传统的过程化程序设计所创建出来的依赖关系结构,策略是依赖于细节的。这是糟糕的,因为这样会使策略受到细节改变的影响。面向对象的程序设计倒置了依赖关系结构,使得细节和策略都依赖于抽象,并且常常是客户拥有服务接口。

 

  事实上,这种依赖关系的倒置正好是面向对象设计的标志所在。使用何种语言来编写程序是无关紧要的。如果程序的依赖关系是倒置的,它就是面向对象的设计。否则,它就是过程化的设计。

 

  DIP是实现许多面向对象技术所宣称的好处的基本低层机制。它的正确应用对于实现可重用的框架来说是必须的。同时它对构建在变化面前富有弹性的代码也是非常重要的。由于抽象和细节彼此隔离,所以代码也非常容易维护。


 

“使用指标或参考基本类的函数必须能够使用派生类对象,并且无需了解它。”

5 接口隔离原则(ISP)

不应该强迫客户依赖于它们不要的方法。接口属于客户,不属于它所在的类层次结构。

 

  这个原则用来处理“胖”接口所具有的缺点。如果类的接口不是内聚的(cohesive),

  就表示该类具有“胖”接口。换句话说,类的“胖”接口可以分解成多组方法。每一组方法都服务于一组不同的客户程序。这样,一些客户程序可以使用一组成员函数,而其他客户程序可以使用其他组的成员函数。

 

  ISP承认存在有一些对象,它们确实不需要内聚的接口:但是ISP建议客户程序不应该看到它们作为单一的类存在。相反,客户程序看到的应该是多个具有内聚接口的抽象基类。

 

   如果强迫客户程序依赖于那些它们不使用的方法,那么这些客户程序就面临着由于这些没使用的方法的改变所带来的变更。这无意中导致了所有客户程序之间的耦 合。换种说法,如果一个客户程序依赖于一个含有它不使用的方法的类,但是其他客户程序却要使用该方法,那么当其他客户要求这个类改变时,就会影响到这个客 户程序。我们希望尽可能地避免这种耦合,因此我们希望分离接口。

5.1 ATM用户界面的例子

   现在我们考虑一下这样一个例子:传统的自动取款机(ATM)问题。ATM需要一个非常灵活的用户界面。它的输出信息需要被转换成许多不同的语言。输出信 息可能被显示在屏幕上,或者布莱叶盲文书写板上,或者通过语音合成器说出来。显然,通过创建一个抽象基类,其中具有用来处理所有不同的、需要被该界面呈现 的消息的抽象方法,就可以实现这种需求。如图2.5.1-1所示:

 

图片 8

图2.5.1-1 ATM界面层次结构

 

   同样,可以把每个ATM可以执行的不同操作封装为类Transaction的派生类。这样,我们可以得到类DepositTransaction、 WithdrawalTransaction以及TransferTransaction。每个类都调用UI的方法。例如,为了要求用户输入希望存储的金 额,DepositTransaction对象会调用UI类中的RequestDepositAmount方法。同样,为了要求用户输入想要转帐的金 额,TransferTransaction对象会调用UI类中的RequestTransferAmount方法。图2.5.1-2为相应的类图。

图片 9

 

图2.5.1-2 ATM操作层次结构

 

   请注意,这正好是ISP告诉我们应该避免的情形。每个操作所使用的UI的方法,其他的操作类都不会使用。这样,对于任何一个Transaction的派 生类的改动都会迫使对UI的相应改动,从而也影响了其他所有Transaction的派生类以及其他所有依赖于UI接口的类。这样的设计就具有了僵化性以 及脆弱性的臭味。

 

  例如,如果要增加一种操作PayGasBillTransaction,为了处理该操作想要显示的特 定消息,就必须在UI中加入新的方法,糟糕的是,由于DepositTransaction、WithdrawalTransaction以及 TransferTransaction全部都依赖于UI接口,所以它们都需要重新编译。更糟糕的是,如果这些操作都作为不同的DLL或者共享库部署的 话,那么这些组件必须得重新部署,即使它们的逻辑没有做过任何改动。你闻到粘滞性的臭味了吗?

 

  通过将UI接口分解成像DepositUI、WithdrawalUI以及TransferUI这样的单独接口,可以避免这种不合适的耦合。最终的UI接口可以去多重继承这些单独的接口。图2.5.1-3展示了这个模型。

图片 10

图2.5.1-3 分离的ATM UI接口

 

   每次创建一个Transaction类的新派生类时,抽象接口UI就需要增加一个相应的基类并且因此UI接口以及所有他的派生类都必须改变。不过,这些 类并没有被广泛使用。事实上,它们可能仅被main或者那些启动系统并创建具体UI实例之类的过程所使用。因此,增加新的UI基类所带来的影响被减至最 小。

5.2 结论

  胖类(fat class)会导致它们的客户程序之间产生不正常的并且有害的耦合关系。当一个客户程序要求该胖类进行一个改动时,会影响到所有其他的客户程序。因此,客 户程序应该仅仅依赖于它们实际调用的方法。通过把胖类的接口分解成多个特定于客户程序的接口,可以实现这个目标。每个特定于客户程序的接口仅仅声明它的特 定客户或者客户组调用的那些函数。接着,该胖类就可以继承所有特定于客户程序的接口,并实现它们。这就解决了客户程序和它们没有调用的方法间的依赖关系, 并使客户程序之间互不依赖。

 

下一章:面向对象软件设计原则(四) —— 包的设计原则

 

CodeProject

图片 11

Liskov Subtitution Principle(from ianfnelson.com)

继承性与多态性是两种非常强大的机制,能够使用一些简单的方法去解决各种复杂的问题。同时它们也有可能创造出漏洞和问题代码。基于这种原则我们需要确保继承体系的合理性,并且不会被代码所滥用而引出各种难以发觉的漏洞。尽管从表面上看来这种原则很简单,但是我们却很难正确去理解它们。

解决这一问题的第一步便是找到实例以核查对象类型——包括其本身及其目标对象。在这个简单的步骤中蕴含着一个基本原则,即“契约式设计”。我们必须确保在调用每个函数前它们都拥有一组真实的条件(前提条件),并且在完成调用后所有函数都将符合自己所对应的条件(后置条件)。所有致力于这项工作的程序员内心都清楚这些条件。而我们的第一步便是将这些条件转换成代码。当我们完成了这一步骤便能够满足以下规则了,即“派生类只会削弱前提条件而加强后置条件。”换句话说,派生类的功能既不应该超过也不该弱于它们的基础类。这一原则非常重要,因为一个被孤立看待的模块总是难以生效。你只有在“Tank”类的根源,同科或其它游戏系统环境下进行它时,你才能清楚它是否真正有效。

我们很容易明找到违背了这一原则的类。只要去找到使用RTTI的基础类去明确它自己所属类型(或它所面向的对象的类型)即可。当“ GameEntity”类校验它是否属于使用特殊码的“Tank”类,这就说明你打破了这一原则。这种类必须能够在忽视对象类型的前提下多形态地调用功能。

界面分隔原则(Interface Segregation Principle)

“不应该强迫用户依赖于他们未曾使用的界面。”

图片 12

Interface Segregation Principle(from doolwind)

我们应该使用界面去推动两种不同对象间的交流,并创造出整洁,标准的代码。如果我们能够保证自己所使用的界面本身就足够整洁且标准,我们便能够基于这一界面推动理念的进一步发展。界面越大,客户端便会越发依赖于其它对象的功能。而如果我们能够提供一些较小且相互隔离的界面,那么每个对象便能够依赖于它所需要的一些小套的功能。这便减少了对象间连接的复杂性,更重要的是能够让别人在阅读了你的代码后便能立刻知晓每种类所依赖的对象。比起提供一个广大的界面,我们选择将界面分割成具有各种功能的群组,并且每个群组面向于不同客户端。

这一原则能够与单一责任原则有效地联系在一起。在这种情况下每个界面都拥有自己的单一原则,从而让我们能够基于界面的要求清楚地呈现出每个对象的功能要求。

着眼于你的所有界面(抽象类)并确保它们的所有功能列表都具有同质性。如果在界面定义中出现了一些功能分组,便说明你违背了这一原则(这是一种简单的判断方法)。在这里空格便是关键,也就是在函数群组间存在着越多空格便意味着它们彼此间越分散。

依赖倒置原则(Dependency Inversion Principle)

“高层次的模块不应该依赖于低层次的模块,这两种模块必须依赖于抽象体。”

“抽象体不应该依赖于细节内容。而细节内容则应该依赖于抽象体。”

图片 13

Dependency Inversion Principle(from doolwind)

但是关键却在于,直到最近我也从未在任何游戏开发中听到过这一原则。这一原则与众多开发者所坚持的做法截然不同。通常情况下如果一种类基于另外一种类,那么客户端必将会认为这一对象的类亦是如此并依此行动。而依赖倒置(游戏邦注:也被称为控制倒置)则与此大相径庭。比起让客户端负起创造对象的责任,这一原则将根据所所依赖的对象做出选择。这就抵消了客户端的控制权,而将其转移到客户端的所有者身上——通常情况也就是游戏引擎。

渲染系统便是一个典型的例子。比起实例化一个渲染对象或直接调用渲染API的类,渲染系统应该接纳一个具有低层次渲染功能的界面。通过依赖于面向渲染系统的界面,我们便能够在不对客户端渲染系统造成破坏性改变的前提下改变低层次的渲染API了。很明显,如果出现了破坏性改变,那么低层次的渲染API界面也会要求做出改变。

两个系统是如何做到彼此间的对话?如果它们正在使用固体类并不断寻找能够依赖于界面的机遇便有可能进行对话。而最佳方法还是让类能够作为构造函数(这是它所面对的类的界面参考)中的一个参数。这同样也意味着子系统是一种可依赖的特殊类。

游戏邦注:原文发表于2011年2月28日,所涉事件和数据均以当时为准。

via:游戏邦/gamerboom

更多阅读:

  • Rampant Coyote:阐述知识产权对独立游戏开发者的价值
  • CEDEC:2017年日本游戏开发者的生活与工作调查报告
  • 阐述游戏开发者需注意的“便利墙”障碍
  • Damon Brown:进行独立游戏开发所需要掌握的9大原则
  • Indie Megabooth:调查显示美国78%独立游戏开发者表示可回本
  • Patrick Miller:2012年游戏开发者生活质量调查
  • Robert Fearon:针对独立游戏开发者的五项PR建议
  • Agnieszka Andrzejewska:论述对独立游戏开发者“独立性”的定义
  • Joe Kaufman:谈独立游戏开发者如何成功盈利
  • 游戏开发者应注意的N种趋势
  • EEDAR:研究显示女性游戏开发者仍遭受薪资和发展歧视
  • Gamasutra:2010年欧美游戏开发者平均薪酬调查报告
  • IGDA:2014年游戏开发者满意度调查报告 41%的人”为了生活“选择游戏
  • 游戏开发者不应高估数据指标的作用
  • Leigh Alexander:开发者谈易用性文本游戏开发工具的挑战性

本文由澳门皇家赌场91资源站发布于澳门皇家赌场91资源站,转载请注明出处:java面向对象-进度2

关键词: