`
cloverprince
  • 浏览: 127393 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

对象的责任:自己万能?还是把责任仍给别的对象?

阅读更多
大家好。
好长时间没联系了。
假期好。

/*************
摘要:
  1: 讲个笑话
  2: 浅谈面向对象的一些东西
  3: 引出一个我没有解决的问题
*****************************************************/


<part 1>

咳咳。本大人来讨论一些很有趣的话题。

先来热身:

Q: 看到如下代码,面向对象的程序员会如何反应?

int calculate(int a, int b, char op)
{
 int c;
 switch(op) {
 case '+':
   c=a+b; break;
 case '-':
   c=a-b; break;
 case '*':
   c=a*b; break;
 case '/':
   if (b==0) {
     errno=1;
     return -1;
   } else {
     c=a/b; break;
   }
 }
 return c;
}


A: 会说:阿,多么难看的代码。

Java程序员的回答:
abstract class Calculator {
 int calculate(int a, int b);
}

class Adder {
 int calculate(int a, int b) { return a+b; }
}

class Subtracter {
 int calculate(int a, int b) { return a-b; }
}

class Multiplier {
 int calculate(int a, int b) { return a*b; }
}

class Divider {
 int calculate(int a, int b) { if(b==0) throw new
DivideByZeroException(); else return a+b; }
}

然后,等待文迪学长补充一个C#的吧,记得有一个用Delegate很巧妙地实现的,这里就不敢班门弄斧了。

附注:
然后一位Scheme程序员这样回答:
(define calculate (lambda (a b op) (op a b)))  ; 你没看错,就1行代码。感觉脑残的话就忽略吧。


<part 2>

以上我们得出一个结论:
在面向对象的程序设计里面,对象知道自己应该做什么事。而且,不同的派生类的对象,可以对基类规定的共同操作,呈现出不同的反应。这被成为"多态"。

我们来举一个例子:
这是一个C++程序:

// 这是一个图形软件。
typedef pair<int,int> Point;
typedef pair<Point,Point> Rect;

// 我们处理的是一个个的形状
class Shape {
public:
   virtual Point getCenter() = 0; // 取得图形的重心。不要忘了,加了"virtual"和"=0"的成员函数成为抽象虚函数。
   virtual Rect getBound() = 0; // 取得边框。同样。只要拥有一个抽象函数,这个类就是"纯虚类"。
};

然后,创造两个派生类:
// 第一个是矩形
class Rectangular : public Shape {
private:
   int x1,y1,x2,y2; //矩形的左上角和右下角。
public:
   virtual Point getCenter() {  // 不要忘了,虚函数可以被覆盖。
      return Point((x1+x2)/2,(y1+y2)/2);
   }
   virtual Rect getBound() {  // Java里面所有的函数都是虚函数,除非被final。
     return Rect(Point(x1,y1),Point(x2,y2));
   }
};

// 第二个图形是圆
class Circle : public Shape {
   int cx, cy, radius;  // 嗯,没说public或者protected,就都是private。
public:
   virtual Point getCenter() {  // 同样实现
      return Point(cx,cy);
   }
   virtual Rect getBound() {
     return Rect(Point(cx-radius/2,cy-radius/2),Point(cx+radius/2,cy+radius/2));
   }
};



这样呢,我们有了一个简单的类结构:一个Shape类,还有两个子类,分别是Rectangle和Circle。他们都有getCenter()和getBound()两个方法。

然后呢,给我们一堆Shape,我们逐一对它们进行getCenter()操作即可。不需要知道它们究竟是哪个类的实例。

void printAllCenter(vector<Shape*> vsp) {
 for(vector<Shape*>::iterator it = vsp.begin; it!=vsp.end(); ++it) {
// 我记得谁吧这个叫做"Iterator"模式。
   Point p = (*it)->getCenter();
   cout<<p.first<<" "<<p.second<<endl;
 }
}


然后呢?




<part 2.5>

为了在屏幕上显示,我需要给每个类增加"屏幕画图"的方法。
为了兼容以往其它的图形对象,我设立另一个基类:

// Drawable类,表示所有可以在屏幕上画的对象的类。
class Drawable {
public:
   virtual void draw(CDC* pDC)=0; //
细心的同学会发现这CDC是MFC里的东西。不过,我好久不用MFC,都已经不会了。现在在研究GTK+。很欢乐的。
   // P.S. GTK+的C++绑定叫做GTKmm。
};


然后呢,修改一下原来的类:

class Shape : public Drawable {
 ....
};


然后,给每个类添加draw()函数呗。

class Rectangle : public Shape {
   // 这里有原来的代码.......
   virtual void draw(CDC* pDC) {
       pDC->rect(x1,y1,x2,y2); //不保证正确。暂且看着理解吧。
   }
};

class Circle : public Shape {
   // 这里有原来的代码.......
   virtual void draw(CDC* pDC) {
       pDC->arc(cx,cy,radius, 0.00, 6.28); //暂且当伪代码吧。
   }
};


好了。现在所有的图形对象都可以draw()了。我们的面向对象的程序在不断演进,终于到了。。。


<part 2.9>

然后,我又增加了
class Savable {
  virtual void save(ostream& ost)=0;
};

这样,图形对象就可以保存到文件里去了。

然后,我又添加了
class UserInteracter {
  virtual void onMouseLeftButtonDown()=0;
  virtual void onMouseMiddleButtonDown()=0;
  virtual void onMouseRightButtonDown()=0;
};

这样,所有的图形对象都可以响应鼠标事件了。

然后,我又添加了
class NetworkClient {
   virtual Response request(Request req)=0;
};

以便通过网络与其他程序互动。

然后,我又添加了很多很多的功能。
不管什么功能,都增加给了基类。然后,每个派生类有自己的实现。

结果。。



<part 2.99>

class Circle : public Shape {
   virtual Point getPoint() {
      /* .....  */
   }
   virtual Rect getBound() {
      /* .....  */
   }
   virtual void draw(CDC* pDC) {
      /* .....  */
   }
   virtual void save(ostream ost) {
      /* .....  */
   }
   virtual void onMouseLeftButtonClicked() {
      /* .....  */
   }
   virtual void onMouseMiddleButtonClicked() {
      /* .....  */
   }
   virtual void onMouseRightButtonClicked() {
      /* .....  */
   }
   virtual Response request(Request req) {
      /* .....  */
   }
   virtual void aMethod() {
      /* .....  */
   }
   virtual void anotherMethod() {
      /* .....  */
   }
   virtual void yetAnotherMethod() {
      /* .....  */
   }
   virtual void yetYetAnotherMethod() {
      /* .....  */
   }
   virtual void moreMethodHere() {
      /* .....  */
   }
   virtual void yeahTheLastmethod() {
      /* .....  */
   }
};


到了最后,我拥有了一个巨大的类:Rectangle。这个类里面,实现了从图形计算,到屏幕画图,文件操作,网络操作,GUI操作,数据库操作,分布式操作,内存操作,反病毒操作,软件注册操作,经济操作,金融操作,这个操作,那个操作,渐渐俄,Rectangle成了一个万能的对象。它永远知道自己应该干什么。

但是,我可怜的Rectangle,你只是一个矩形阿。你怎么胜任这么多任务。

我们为你聘请了美术家,数据库分析师,网络工程师,经济人才,社会精英,组成了各个小组。每人负责Rectangle的一部分操作。为了合理利用人才,每个小组横向地负责每一个对象的同一个方法。比如美工小组负责所有的类的draw()方法。

但是,不久引发了灾难



<part 2.999>

每个人都在修改我的源代码。这样我的项目组里,同一个源代码出现了100多个不同版本。我已经无法跟踪,更无从将它们合并在一起了。

我终于忍无可忍,采取了最终的决策:



<part 3.0>

class Shape { // 不实现任何接口(也就是没有任何基类)
public:
   virtual Point getCenter() = 0;   // Shape专心处理几何运算
   virtual Rect getBound() = 0;   // 只保留最基本的功能。
};

class Rectangular : public Shape {
private:
   int x1,y1,x2,y2;
public:
   virtual Point getCenter() {  // 嗯
      return Point((x1+x2)/2,(y1+y2)/2);
   }
   virtual Rect getBound() {  // 做好本职工作就好了。
   }
};

class Circle : public Shape {
   int cx, cy, radius;
public:
   virtual Point getCenter() {  // 乖,
      return Point(cx,cy);
   }
   virtual Rect getBound() {   // 你也是一样。
     return Rect(Point(cx-radius/2,cy-radius/2),Point(cx+radius/2,cy+radius/2));
   }
};


/************** 在另一个文件 drawing.cpp里面 ***************/

#include<typeinfo> // 大家听说过typeinfo这个库吗?没有吧
// 这是在运行时知道对象的继承关系的库。

Rectangle my_rectangle;
Circle my_circle;

void draw(Shape* pShape, CDC* pDC) {
   if(typeid(*pShape)==typeid(my_rectangle)) {
       Rectangle *pRect = (Rectangle*)pShape;
       pDC->drawRect( pRect->x1, pRect->y1, pRect->x2, pRect->y2);
   } else if (typeid(*pShape)==typeid(my_circle)) {
       Circle *pCircle = (Circle*)pShape;
       pDC->arc( ..........);
   }
}


所以,最后又成了老式的C语言风格。



终结:

所以,问题来了。
怎么才能既保留面向对象的方法,
又能把功能分散到各自的模块里面呢?

大家讨论一下吧。


我目前的了解:
C#的Partial Class是不是能解决这个问题?
ruby支持"打开一个已有的类,向里面添加一些方法"

如果我是Haskell程序员,我回这样做:
type Point = (Int,Int)
type Rect = (Point, Point)
data Shape | Rect Point Point | Circle Point Int

center :: Shape -> Point
center (Rect (x1,y1) (x2,y2)) = ((x1+x2)/2, (y1+y2)/2)
center (Circle (cx,cy) r) = (cx,cy)


然后增加类(不是面向对象里面的类,是函数式编程的类)
这一切发生在另一个文件里

class Drawable a where
   draw :: CDC -> a -> IO ()

instance (Shape a) => Drawable a where
   draw dc (Rect (x1,y1) (x2,y2)) = do { DCDrawRect dc x1 y1 x2 y2 }
   draw dc (Circle (cx,cy) r) = do { DCDrawArc dc cx cy r 0 6.28 }



======================

一种可行的解法:

感谢Jerry Tian同学。

让save()不再是Shape的方法,而是创建另一套Saver类。
class BaseSaver {
public:
    void save(Shape* pShape)=0;
};
BaseSaver::_theOnlyBaseSaverInTheWorld=NULL;

class RectSaver {
public:
    void save(Shape* pShape) { ... }
} theOnlyRectSaverInTheWorld; // 哈哈,这就是我的所谓Singleton模式吧。不过没有安全保证。


然后,给Shape类加一个指向Saver类的引用。
class Shape {
    BaseSaver* pSaver;
public:
    void save() {
        pSaver->save();
    }
};


对于具体的Shape派生类,指向相应的Saver类:
class Rectangle {
    Rectangle() { pSaver = &theOnlyRectSaverInTheWorld } // 确定自己的Saver。
};


做法就是把自己的类的责任转移给别的类。这样,自己和那个类就可以分别编辑了,不用担心多个人一起修改会导致的麻烦。
这是“桥接模式”的一种形式。
分享到:
评论
4 楼 cloverprince 2010-01-07  
garfileo 写道
garfileo 写道
我认为应当利用消息处理的方式来解除这些类直接的耦合。例如,这个受虐的矩形本来就不应该知道如何绘制自己,而应当请求美术家来绘制自己。


嗯,这个问题又引发了另外一个问题,那就是利用当前正在使用的语言去模拟消息处理。

不知道这个世界上有没有“原生”支持既能面向对象又支持消息处理的语言。

C#吧。不过我真不确定消息处理有没有用。
3 楼 garfileo 2010-01-06  
garfileo 写道
我认为应当利用消息处理的方式来解除这些类直接的耦合。例如,这个受虐的矩形本来就不应该知道如何绘制自己,而应当请求美术家来绘制自己。


嗯,这个问题又引发了另外一个问题,那就是利用当前正在使用的语言去模拟消息处理。

不知道这个世界上有没有“原生”支持既能面向对象又支持消息处理的语言。
2 楼 garfileo 2010-01-06  
我认为应当利用消息处理的方式来解除这些类直接的耦合。例如,这个受虐的矩形本来就不应该知道如何绘制自己,而应当请求美术家来绘制自己。
1 楼 ileile 2009-12-12  
你既然会用Haskell或者Lisp,难道自己看不出关键之处吗?

你对面向对象的理解也是错的。对象仅仅是为了提供一个事后查询事先声明的机构,既不是必须和现实中的某个概念相对应,也不是说所有的宾语应该加在一个主角身上。

你这种方法,不叫做抽象,叫做人为的具体化,自然得不到抽象的好处;反而承担了一堆你自己设计的坏处。

当然,即便能够熟练掌握面向对象比较正确的用法(面向对象比较反人类,经常误导程序员做出错误的设计,虽然这不代表永远不能在面向对象下作出对的),用面向对象设计在一些具体场景里也是相当蹩脚。

相关推荐

Global site tag (gtag.js) - Google Analytics