面向对象设计与面向对象编程

我发现,面向对象设计,被忽视的太严重了。使用OOP语言,却不是面向对象来设计。无非就是用类写C程序而已,而且这种情况还很常见。另一种情况是过度重视类,而忽视了对象,但是对象才是根本。不过本文会以类型为主,来介绍面向对象设计。

前言

面向对象是一个完全不同于传统软件设计方式的理念,传统软件设计是以功能为主体,以功能为模块,以功能为目标。而面向对象不同,是以软件运行中的组件为主体,实现某个功能,只是恰好这些组件的交互可以间接做到。这些组件,就是对象。用面向对象来设计软件,实际上就是设计一系列的对象,使这些对象在运行中恰好能间接完成软件要做的事。

一 类型

为什么,面向对象出现的时候,同时也会有类这种东西。因为类就是设计对象的一个方式,将对象划分为不同的类型,使用类来描述各种类型的对象。就像一个建筑蓝图,有个人设计了一个大楼,画成蓝图,于是,他可以随时根据这个蓝图去建大楼。

这是反向运用。

因为我们是先认识到一个个的对象,才开始有类型的概念。四条腿,一个长长的头,和尾巴,非常能跑,我们发现很多个这样的对象,于是建立了一个类型:马。后来又发现了狗、猪、牛、羊等等,我们又发现了它们都有一个共同的特点:哺乳产子。于是我们建立一个了类型:会哺乳的,这个类型包含所有是哺乳产子的对象。然后我们又发现,还有另一些东西:鸡、鸭、鸟、鱼等等,和会哺乳的不同,它们是下蛋产子、下卵产子。它们和哺乳产子的都有一个共同的特点:会动。于是我们建立了一个类型:动物,这个类型包含了所有会自己动的对象。经过慢慢的发现,最终就有了现在的体系。

我们还发现了,任何对象,都有两个东西:行为、属性。鸟常常唱歌,这是鸟的行为,鸟有各种大小、颜色、样子,这是鸟的属性。所以我们的OOP语言,建立类型描述对象时,都会提供方法和成员变量,来对应行为和属性。

你现在用的语言,可能就有一个root类型:object,这个类型其实就是『类型本身』,所以任何类型的对象,都可以视为一个object类型的对象。我们在认识了很多很多个对象,建立了很多很多个类型后,同时也开始了反向认识,就是类。从对象建立类型,是从下往上,重建类型划分对象,是从上到下。

我们用的语言,对于建立类型,有三大要素:封装、细化、多态。比如在C++,我们用class定义类型,就是封装,此类型对象的行为和属性,都在class定义中。你建立一个类型:书,然后再建立几个类型:简书、帛书、线装书、电子书等等,你一定会将这些类型归为书的子类型,这就是细化。你需要画一个形状,这个形状可以是矩形圆形乱七八糟型,那么你只需要针对形状类型来操作,比如在形状类型中加上行为:画。其细化的类型:矩形圆形乱七八糟型都各自定义自己的画行为。你只需要获得一个由形状细化的类型的对象就行了,调用它的行为:画,这就是多态。

在软件设计中运用面向对象,其实就是建立一个类型系统,每一个软件都有一个自己的类型体系。我们用类型来设计软件,软件在运行时,通过各种类型的对象来交互,从而恰好间接的完成要做的事。

二 可变 不可变

我们在建立类型时,需要仔细的考虑细化问题。比如一个类型:human,然后我们再细化出两个类型:man、woman。

class human {};
class man : public human {};
class woman : public human {};

但是,这样的细化,是有很大问题的。因为一个man对象,是会变成woman对象的,反之亦然。比如泰国的poy,你会把她当成男吗?为什么?因为她现在就是女的。这个是会变的。而会变的,是属性,而不是类型。

什么叫属性,一个物体的坐标、一个人的名字、你钱包的充实度。这些就是属性,是可变的。而类型不会变,马属于哺乳动物,钢铁属于金属,这些是不变的。

而性别,其实是属性,比如在动物界,很多类型的动物,会改变性别,有些还没性别。我们更应该,把性别作为属性,而不是类型。

class human
{
public:
    const string& getsex() {return this.sex;}
    void setsex(const string& newsex) {return this.sex;}
private:
    string sex;
};

这样,你的类型系统就正常了。为什么呢,比如你设计一个通讯录啊、人际管理啊之类的软件,你细化出man、woman的话,类型体系臃肿,不说,你光是新建一个人员时都得分析是建立man对象还是woman对象,作为属性就不需要了,你只需要human a("man");就可以了,他变性了,那么a.setsex("woman");就可以了。试想如果你不用属性,而是用类型来做,又会是怎样的场面?

所以,我们在建立类型系统时,在细化时,需要分清楚,到底是属性,还是子类型。

可变的,就是属性。

不可变的,就是类型。

三 可能 不可能

这里,我们分析的是,对象的行为。我们在设计一个类型时,如何合理的设计方法。

我常常看到一些,不正确的方法设计。方法没有放到正确的位置,或者是多余的方法,或者是不应该用方法而应该用函数。

比如,有人要设计一个看书软件,于是建立这书这个类型:

class book
{
protected:
    string name;
    string author;
};

然后,因为书是要翻的,于是他有了这样的设计:

class book
{
public:
    void before_page(){if (this.cur_page > 0) {this.cur_page -= 1;} }
    void after_page(){if (this.cur_page < this.page_num) {this.cur_page += 1;} }
protected:
    ...
    size_t cur_page;
    size_t page_num;
};

看上去,似乎没什么。再一看,好像确实没问题。

但是。

这是一个错误的设计。

面向对象,每一个对象都有着自己的行为、属性。我们好好的分析,往上翻页,往下翻页,是谁的行为。是书自己的行为?就比如现实中我们跟一本书讲:翻到下一页,然后这本书就特么的自己动了?自己翻了?这已经是个神话故事了。

翻书是谁的行为,是看书者的行为,看书者用手翻。而不是这本书自己翻。当然如果你真的看到了一本书自己翻,请一定要告诉我,我要膜拜神迹。

那么怎么样设计翻页这个行为呢?简单,非常简单。作为看书者的方法,就可以了。

class book
{
public:
    ...
    const string& page(size_t n);
private:
    ...
    vector<string> pages;
};

class reader
{
public:
    size_t page() {return this.cur_page;}
    void before_page(){if (this.cur_page > 0) {this.cur_page -= 1;} }
    void after_page() {if (this.cur_page < this.page_num) {this.cur_page += 1;} }
private:
    size_t cur_page;
};

class con
{
public:
    void render () {cout << cur_book.page(cur_reader.page()); << endl;}
private:
    reader cur_reader;
    book   cur_book;
};

// 上面这样的设计还带来了一个额外的好处:降低耦合。

书是什么对象,就是一个信息对象,只需要有属性,书名、作者、每个页的文本等等,就这些。当然我们需要用方法,来包装属性,这属于软件工程中很重要的一个手段。在这样的情况下,book.page(),并不是书的行为,而只是对属性的包装。

翻书,是看书者是行为,所以,放在看书者的类型里,最合适。

但是我为什么不我直接把cur_book放到reader里,作为属性呢?因为看书者和书,是两个不同的东西,我们拿起一本书,只是和这本书建立起一个暂时的联系。就像朋友问你,绝对不会问:“你的当前书是什么啊”,这是一种属性的问法,而只是会问:“你现在在看什么书啊”,这是联系的问法。看书者的属性,都是联系性的,比如书名啊,当前的页码啊等等。我们不需要放cur_book到里面,但是我们可以放一个string book_name到里面,作为reader的属性。



再看另一个例子,这个不同了,这是一个广泛被采用的错误例子!

class a : public object
{
public:
    a* clone() {return new a();}
};

class b : public object
{
public:
    b* clone() {return new b();}
};

...

object* src = ... // new a or b
object* k = src->clone();
...

对,克隆。一个非常广泛的类型方法设计。这是运用多态来克隆对象,我们不需要管被克隆的是什么对象,只要有clone方法,我就让你克隆,克死你。

我们仔细分析一下,问题在哪里,在哪里。

那就是不可能。

这个和上面的不同了,上面的book的page,只是属性的包装。而这个是真正的行为了,是纯动词了,连名词都没了。这就是对象的行为了,是一个类型的方法了。

但是,这不可能,这个对象不能够这样行动。

比如科学家克隆一只羊,如果用这个被广泛采用的方式,那就是这样的场面:”诶,那只羊,快点,克隆一个新的你,别墨迹啊。“

滑稽吗,可笑吗?

实际上,科学家是怎么克隆的呢?是科学家用各种手段,来复制这只羊。也就是说,克隆,是科学家的行为,而不是那只羊,被克隆者的行为。哪怕这只羊精通天文地理物理数学,啊,这样的话或许它还真能自己克隆自己,当然前提是它有各种设备可以用,如果你发现了这样的一只羊,请一定告诉我,我将膜拜。

当然不要被上面这段话误导,这实际上一个执行者和目标对象的关系,克隆由科学家执行,执行者是科学家,目标对象是羊。跟这只羊精不精通天文地理物理数学没关系,如果他能自己克隆自己,那么执行者就是羊,目标对象是它本身,行为是克隆。

那么我们应该如何设计,克隆呢?

那就是分析出谁是执行者,放到执行者里面。

class scientist
{
public:
    sheep* clone(sheep* target);
    dog* clone(dog* target);
    pig* clone(pig* target);
    money* clone(money* target); // 其实money是一种动物 :)
};

...

secientist sei;
sheep s;
animal* k = sei.clone(&s);

有些时候,我们分析不出执行者,或者实在懒得分析,那么作为函数就行了

class string : public object {};
class vector : public object {};

// 你可能需要使用friend来连接这些函数
string* clone(string* target);
vector* clone(vector* target);

...

string s();
object* k = clone(&s);


我们有没有在现实中看到能自己克隆自己的东西呢?自己能克隆自己,简直是神迹啊。

但是,宇宙万物不可思议。

还真的有能自己克隆自己的东西:病毒。

这是题外话。

所以,虽然我说被广泛采用的clone方式是个错误的设计,但指的是语义上的错误,在软件设计中,其实那是正确的设计。病毒能自己克隆自己,那么计算机里的字符串、数组能自己克隆自己,有什么好稀奇的。不可思议,计算机本来就不可思议。既然能自己克隆自己,那么clone作为这个对象的行为,并没什么问题。

所以我们建立类型系统,是以『现在』为参照的,对于『现在』是不变的。但到了以后,或许人类不再被归为高级动物了,不是动物了,那么所有将human作为animal的子类型的设计,又要重新设计了。所以我们用面向对象来设计软件时,不要沉迷于类型,而是要沉迷于对象。我就是因为在上面的例子中沉迷于类型,才会有clone不能作为对象行为的看法。什么行为和属性,一个对象能克隆自己,那么给他加个clone方法,不能,就不需要有,根本不需要用类型来定义。为什么JavaScript比C++/Java更面向对象,因为JavaScript不沉迷于类型,而C++/Java沉迷于类型。

面向对象,面向的,是对象。


以上是对类型的介绍,下面,才是真正的对面向对象的介绍


四 面向对象设计

那么什么是面向对象设计?在面向对象的领域中,有几个方面:OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。

首先是面向对象分析,我们需要弄清楚,软件中需要有哪些对象,这些对象是什么关系,要做的是什么。注意,是对象,而不是类型。就比如一个看书软件,有哪些对象?比如我们可以发现,有书、看书者,还有界面、用户设置等等对象。

然后是面向对象设计,我们就是分析出的对象为主体,围绕这些对象,来设计软件。而不是围绕功能,围绕功能去设计软件的,就不是面向对象。面向对象只能是以对象为主体,什么乱七八糟功能,都只是这些对象在交互中,恰好间接的完成了而已。模块的划分,大概就体现出了,你是不是面向对象。是面向对象的话,基本上每个模块都对应着一个对象。当然不是Java那种,Java是以类型划分模块,而不是以对象划分模块。

简单的说,只要你的设计是以对象主的,那么就是面向对象设计。

FILE* f = fopen("a.txt", "rb");
const char* str = "hello world!\n";
fwrite(str, strlen(str), 1, f);
fclose(f);

C语言不是OOP语言,但C语言是OOD语言,C语言的很多地方,都体现出了面向对象,比如fopen系列,数学运算系列,字符串系列等等。

在这里,f就是一个文件对象,fopen建立这个对象,fwrite将数据写到一个文件对象里,fclose关闭一个文件对象。fwrite不需要管你打开是哪个文件,只要能写,它就给你写。fclose不管你打开的是什么文件,反正你传入一个文件对象,就给你关了。fopen系列,围绕着文件对象,也就是FILE*类型的对象,这就是面向对象设计。在这个,你打开a.txt文件,然后写入hello world!,都是通过文件对象间接完成的。fopen/fread/fwrite/fclose/···就是文件对象的方法,FILE*结构里的东西,就是文件对象的属性。

有一个更大更好的例子,就是Windows API的HANDLE,几乎所有的函数,都是围绕HANDLE做事的。一个HANDLE对象,要么是最上层的HANDLE类型,要么是HMENU、HMODULE等更细化的类型。你不用知道这些HANDLE都是什么鬼样子,反正你只要用HANDLE对象来做事就行了,Windows API,就是围绕着HANDLE对象做事的,这就是面向对象设计,而且是最纯粹的。

面向对象设计,和什么语言是无关的。因为这是设计,是体现出现的,而不是具体的样子。我们可以在任何语言中运用面向对象设计。

哪——怕——是——汇——编。

但面向对象编程和语言是有关的。

五 面向过程

区别到底在哪里?区别在于,面向过程以功能为主,以目标为主。比如上面的a.txt例子,用面向过程去做,就是这样。

OpenFile("a.txt");
const char* str = "hello world!\n";
Write(str, strlen(str), 1);
CloseFile();

在这里,就是纯粹的实现功能,打开a.txt文件,然后写那行字,然后关了。没有什么文件对象。在划分模块时也是以功能为主:OpenFile(打开文件)/Write(写东西到文件)/CloseFile(关闭文件)

用面向对象方式,就是以对象为主,这里有文件对象,所以我们建立一个文件对象,围绕这个对象,来间接完成要做的事。看起来是不是区别小?实际上,天壤之别。

六 面向对象编程

光有面向对象设计是不够的,我们还需要有在骨子里就支持面向对象设计的语言,就是我们现在常见的,OOP语言。这些语言,能使你更自然的设计对象,提供封装、细化、多态来让你能更好的运用对象。

面向对象不包括封装、细化、多态。

面向对象编程才有封装、细化、多态。

为什么?因为面向对象,主体是对象,其实是不管类型的。然而我们需要管类型,所以面向对象编程,才会提供封装、细化、多态给我们,就是让我们能有一套手段来管理不同的对象。没有这些,我们就难以实现面向对象。

为什么str.size()对比strlen(str)是一个巨大的进步,因为我们有了强力的手段,来实现面向对象。面向对象早就有了,但是没有提升软件设计领域的水平,而面向对象编程语言的出现,才提升了软件设计领域的水平。

但是,虽然我们有了OOP语言,却依然会不知不觉的偏离面向对象,导致用类写C程序,用对象来直接完成功能的出现。

这是因为没有弄清楚对象,没有设计好对象。

另一个偏离是,过度重视类型,重视class,而忽略了对象。比如有些编程者,差不多把面向对象变成了面向设计模式,代码中全是设计模式,而最重要的,最核心的对象,都被淹没在各种模式中。

所以,整个面向对象领域中,有三大元素:

面向对象分析

面向对象设计

面向对象编程

缺一不可

在C语言,我们都会运用面向对象设计,到了OOP语言,就更应该面向对象设计。不要想着功能,不要想着目标,而是对象的交互恰好间接的实现用户的需求,恰好、间接,这是两个很重要很重要很重要很重要的概念。主体是对象,用类型来设计对象。用这些对象的交互来恰好间接实现功能,不能直接实现功能,不能。



在结尾的最后,我给大家说一个目前大部分OOP语言的一个缺陷,就是过度重视类型。比如在某个对象只需要有一个实体时就出问题了,设计模式中介绍的单例模式是有极其严重的BUG的。为什么,因为在这种情况下,类型都不是必须的,很多语言都着叹为观止的类型设计手段,而没有一个设计单对象的方式。只有一个对象,意味着什么封装、细化、多态都不需要了,一定要建立一个类型,然后用各种诡异、莫名其妙的手段来限制只能建立一个实体对象,是自寻烦恼。面向对象,核心是对象,而不是类型。

时间: 2024-08-01 05:04:15

面向对象设计与面向对象编程的相关文章

第八章、面向对象设计

面向对象程序设计的基本特征有: 1,封装性: 封装性是指将对象相关的信息和行为状态捆绑成一个单元,即将对象封装为一个具体的类.封装隐藏了对象的具体实现,当要操纵 对象时,只需调用其中的方法,而不用管方法的具体实现. 2,继承性: 一个类继承另一个类,继承者可以获得被继承类的所有方法和属性,并且可以根据实际的需要添加新的方法或者对被继承类中的方法 进行覆写,被继承者称为父类或者超类,继承者称为子类或导出类,继承提高了程序代码的可重用性,Java中一个子类只能继承一个 父类,Object类是所有类的

7种面向对象设计的强大之处

凭什么要用面向对象来编程,不用是否可以?今天我们通过讲这么几个设计原则来说明为什么要用面向对象,它的好处在哪里. 一.单一职责原则: 全称:“Single-Responsibility Principle”面向对象设计 说明:就一个类而言,应该只专注于做一件事和仅有一个引起它变化的原因.所谓职责,我们可以理解他为功能,就是设计的这个类功能应该只有一个,而不是两个或更多.也可以理解为引用变化的原因,当你发现有两个变化会要求我们修改这个类,那么你就要考虑撤分这个类了.因为职责是变化的一个轴线,当需求

Java程序员应该了解的10个面向对象设计原则

面向对象设计原则: 是OOPS(Object-Oriented Programming System,面向对象的程序设计系统)编程的核心,但大多数Java程序员追逐像Singleton.Decorator.Observer这样的设计模式,而不重视面向对象的分析和设计.甚至还有经验丰富的Java程序员没有听说过OOPS和SOLID设计原则,他们根本不知道设计原则的好处,也不知道如何依照这些原则来进行编程. 众所周知,Java编程最基本的原则就是要追求高内聚和低耦合的解决方案和代码模块设计.查看Ap

面向对象设计原则

七大原则:开闭原则.里氏代换原则.依赖倒转原则.合成/聚合复用原则.迪米特法则.接口隔离原则,单一职责原则. 开闭原则是面向对象的可复用的基石.其他六种原则是手段和工具. 各规则详细(本部分为转载) http://kb.cnblogs.com/page/214010/ 正如牛顿三大定律在经典力学中的位置一样,“开-闭”原则(Open-Closed Principle)是面向对象的可复用设计(Object Oriented Design或OOD)的基石.其他设计原则(里氏代换原则.依赖倒转原则.合

面向对象设计的SOLID原则

S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. SRP The Single Responsibility Principle 单一责任原则 OCP The Open Closed Principle  开放封闭原则 LSP The Liskov Substitution Principle 里氏替换原则 DIP The Dependency Inversion Principle 依赖倒置原则 ISP The

【设计模式】#001 面向对象设计的八个原则

1.对于面向对象的软件系统设计来说,在支持可维护性的同事,需要提高系统的可复用性 2.软件的复用可以提高软件的开发效率,提高软件的质量,节约开发成本,恰当的复用还可以改善系统的可维护性 3.面向对象设计简化成三条 3.1 封装变化点 3.2 对接口编程 3.3 多使用组合,少使用继承 点击查看大图:

面向对象设计——“泛型”的起步

泛型是 2.0 版 C# 语言和公共语言运行库 (CLR) 中的一个新功能.泛型将类型参数的概念引入 .NET Framework,类型参数使得设计如下类和方法成为可能:这些类和方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候.例如,通过使用泛型类型参数 T,您可以编写其他客户端代码能够使用的单个类,而不致引入运行时强制转换或装箱操作的成本或风险. --MSDN 我的计算机是以Visual Basic 6.0拉开帷幕的,让我印象比较深的是两个排序:选择排序和冒泡排序.当然本

第二章 【面向对象设计原则】

(一)如何衡量软件设计的质量 内聚度: 表示一个应用程序的单个单元所负责的任务数量和多样性.内聚与单个类或者单个方法单元相关.(好的软件设计应该做到高内聚.) 耦合度: 耦合度表示类之间关系的紧密程度.低耦合是指尽量使用抽象耦合,少用具体耦合. 设计原则名称 设计原则简介 重要性 单一职责原则 的职责要单一,不能将太多的职责放在一个类中. ★★★★☆ 开闭原则 软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能.  ★★★★★ 历史替换原则 在软件系统中,一个可

(转载)Java程序员应当知道的10个面向对象设计原则

面向对象设计原则是OOPS编程的核心, 但我见过的大多数Java程序员热心于像Singleton (单例) . Decorator(装饰器).Observer(观察者) 等设计模式,而没有把足够多的注意力放在学习面向对象的分析和设计上面.学习面向对象编程像"抽象"."封装"."多态"."继承" 等基础知识是重要的,但同时为了创建简洁.模块化的设计,了解这些设计原则也同等重要.我经常看到不同经验水平的java程序员,他们有的不知