对面向对象程序设计(OOP)的认识

Posted on 2013-07-30 19:24  冰天雪域  阅读(603)  评论(0编辑  收藏  举报

前言

本文主要介绍面向对象(OO)程序设计,以维基百科的解释:

面向对象程序设计英语Object-oriented programming,缩写:OOP),指一种程序设计范型,同时也是一种程序开发的方法。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。

简略来说,面向对象程序设计,指采用了面向对象的方法来进行程序设计。设计指一种把计划、规划、设想通过视觉传达出来的活动过程,它是一种创造性,积累性,实践性的工作。提笔写设计的文章是很有压力的,它不像深入一个知识点一样让人容易有的放矢,一千个读者心中有一千个哈姆雷特,同样的项目两个人来做架构肯定不一样。包括我,每几年对设计都会有一些不同的看法,回头来看自己的代码也总会挑出很多不足,世界是不完美的,设计却希望尽求完美,闲话不说,来进入本文的正题,看看从何谈起。

面向过程程序设计

面向过程程序设计不是面向对象程序设计的前提,从面向过程谈起主要是因为自面向对象(OO)程序设计一提出,就有太多的两者对比。在这样的对比中,面向过程被形容成老化,腐朽,僵硬的设计模式,它自上而下,按照功能逐渐细化,实现快速但面对变化时束手无策。相对而言面向对象具有封装性,重用性,扩展性等一系列优点,言而总之:“还面向过程??你out了,来面向对象吧。。。”

C语言是面向过程的代表,它在1972年由贝尔实验室的D.M.Ritchie提出,在Unix系统中大放异彩,直至今天在系统软件,图形动画,嵌入开发等众多领域中还保持着旺盛的生命力。程序设计这个概念,伴随着程序开发被提出,最简略的被描述为 程序设计=数据结构+算法,通俗一点的说程序设计指的是设计、编制、调试程序的方法和过程。人是善于思考总结的,在漫长的面向过程的程序开发中,一些设计原则被提出,用以更好的指导设计:

  1. 模块原则:使用简单的接口拼合简单的部件。
  2. 清晰原则:清晰胜于技巧。
  3. 组合原则:设计时考虑拼接组合。
  4. 分离原则:策略同机制分离,接口同引擎分离。
  5. 简洁原则:设计要简洁,复杂度能低则低。
  6. 透明性原则:设计要可见,以便审查和调试。
  7. 健壮原则:健壮源于透明和简洁。
  8. 优化原则:雕琢之前先要有原型,跑之前先学会走,不要过早优化。
  9. 扩展原则:设计着眼于未来,未来总比预想快。

以上原则参考自《Unix编程艺术》,书中总结了17种原则来指导程序设计,精简为一句话就是Unix哲学“KISS—Keep It Simple, Stupid!”,保持简单。

KISS,Keep Simple,Keep是它的核心词,底层的API设计,可以尽量保持简洁清晰,外部的需求变化,Keep?你Hold得住么?

需求变化,你Hold得住么?

你Hold不住!

变化来源于对事物认识的发展和外部业务的变更,当然实际中变化可能来自于更多方面。你不能拒绝变化,你只能拥抱变化,在长期的程序设计中,两条设计原则被提出:

  1. 简洁,用更长的三个词来说应该是简洁,清晰,透明。程序应该易于读懂,易于维护,最好有文档注释。世界在变,规则在变,今天精巧的设计,可能成为明天修改的沉重包袱。
  2. 重构,在不改变软件现有的功能上,通过调整程序代码而改善软件质量,性能等。面向过程中重构的原则性目标在于提高正交性,所谓正交性,是指任何操作均无副作用,每一个动作只改变一件事,不会影响其它。具体到API设计就是一个API中不应做两件事,一个粗粒度内部做几种事情的接口可以细化为多个细粒度的接口,“只做好一件事”,这个忠告不仅是针对简单性的建议,对正交性也是同等程度的强调。

按照正交性的设想,我们应该打造一个纯正交的系统,在这个设计中,任何操作均无副作用,每一个动作只改变一件事不会影响其它,改变一件事情一个具体方面的方法只有一个。想法是完美的,世界是复杂的,软件要能做到拥抱变化,设计希望达到高内聚(模块内元素紧密结合),低耦合(模块间依赖尽可能低),那么如何来拥抱变化呢?

让它“活”起来

程序最有魅力的事情在于创造,程序员使用代码来完成程序的创造。一花一世界,一木一浮生,作为粘土世界的上帝,我们采取了很多的努力来完成我们的设计:

  1. 分清程序中可变和不可变的部分,相对来说,数据是不可变的,算法是可变的,我们设计好相对稳定的数据,再细化算法,应对变更。
  2. 以模块化来划分程序,按功能来划分模块,这样从纵向看,程序是由一个个的模块组成的,从横向看,我们定义好模块间通信的接口,程序就是由模块组织起来的联合体。

程序像流水线一样工作起来了,数据从上到下开始运作,但是变化仍然无处不在,大修小补依然无法避免。如果模块设计的足够独立,程序的正交性足够好,变动还在可控范围之内。一旦变化跨越多个模块,程序经过多次大修,就会有种想把它捏回泥巴的冲动。

外部的变化还在继续,里面依旧是勤恳,冰冷的泥块,怎么来拥抱变化?对,伸出你的金手指,让它“活”起来。

对象

英雄应运而生,对象应责而生。我们点“活”了对象,就是为了让它解决事情承担责任。从:

   1: struct Data
   2: {
   3:     int d;
   4: };
   5: void increase_data(Data* data)
   6: {
   7:     printf("过程调用,数据为: %d", ++data->d);
   8: }
   9: increase_data(&Data());

   1: public class DataWorker
   2: {
   3:     private int data;
   4:     public void Increase()
   5:     {
   6:         Console.WriteLine("对象调用,数据为: {0}", ++data);
   7:     }
   8: }
   9: new DataWorker().Increase();

把传统的数据和处理数据的函数封装起来,用DataWorker对象来表示,数据变成了对象的状态,函数变成了对象的方法(行为)。一个对象被我们点“活”了,它负责处理一件事情,把责任下放是点活对象的出发点--“变化太快了,面面俱到的管理让我疲于奔命,你就负责处理这块事情吧,由你来应对这块事情的变化”。

替换

对象被我们点活了,它们各尽其责来处理自己的事情,程序处理被变成了一个个对象间的相互协作,如果有变化产生我们找到负责的对象,由它来处理变化。想法是完美的,可是具体到对象,它怎么来应对变化?修改自己的方法(行为)?

对象是我们点活的,责任是我们分配的,方法(行为)是我们指定的,发生变化了还要我们来修改它的方法(行为),那绕一圈点活它干嘛?和面向过程中直接修改对应的函数有啥区别?是的,可能方便在于比较容易定位到它,但是你修改了一个对象后,如何保证和它协作的别的对象没有意见,不会造反?

从现实来讲,作为一个老总,指派了一个区域经理来负责一块业务,负责的业务出现了问题,你会对它的业务指手画脚来重新教育他该怎么做么?不会的,不要让自己陷入泥潭,首先信任,不行就炒了他,换一个。

应对变化的关键点在于替换,这样才不会使自己陷入细节。替,顶替,表示新对象可以承担旧对象的职责,对协作的别的对象没有影响。换,表示要能应对变化,更改处理责任的具体方法(行为)。这是一个共性和变性的描述,那么怎么用程序语言来表示?

类是面向对象程序语言中的一个概念,表示具有相同行为对象的模板,类声明了对象的行为,它描述了该类对象能够做什么以及如何做的方法。一个类的不同对象具有相同的成员(属性、方法等),用类来表示对象的共性,那么怎么来表示变性呢?

类之间支持继承,可以用父类和子类来表示这层关系,用自然语言来形容,父类是子类一种更高程度的抽象,比如动物和哺乳动物。子类可以添加新的行为或者重新定义父类的行为来完成变化。允许子类来重定义父类的行为,在对象间的相互协作中尤为重要,可以把不同的子类对象都当做父类对象来看,这样可以屏蔽不同子类对象间的差异。在需求变化时,通过子类对象的替换来在不改变对象协作关系的情况下完成变化,这种特性也被称为多态。

封装,继承,多态,被称为面向对象技术中的三大机制,那么回到本文的主题,什么叫面向对象呢?

面向对象

所谓面向对象,面向两个字很重要--“我的眼里只有你”,面向对象的哲学在于把软件(世界)看成是由各种各样具有特定职责的对象所组成,不同对象之间的相互作用和通讯构成了整个软件(世界)。以面向对象的角度去进行程序设计,需要至少以下三步:

  1. 发现(设计)对象;
  2. 确定对象的职责;
  3. 确定对象间的相互关系。

按前面所提,类是具有相同行为对象的模板,通过同一个类创建的不同对象具有相同的行为,对象是类的一个具体例子(实例),我们面向对象设计程序时,一般从类的设计开始。

类的设计

类通常情况下是自然世界中一个概念的描述,比方说Person类通常对应人,这种软件和自然世界中的对应关系使我们可以尽可能运用人类的自然思维方式去解决问题。那么如何发现类呢?一个最简单的办法就是把我们熟知的自然概念直接抽象为类,比方说一个图书馆借书的程序,管理员,书,借书者,我们可以很容易想出一系列的概念,这些名词概念来自于我们对生活对该领域的了解。把我们熟悉的名词(概念)直接抽象为类,把动词抽象为该类的行为,这是一种最粗糙的类设计方法。这种设计比较容易下手,但是也会出现一些问题:

  1. 实现性,事物分抽象事物和具体事物两种,程序设计中用类的实例(对象)来表示一个具体事物。对于人来说,管理员,借书者都是人,同一个人可以充当不同角色,一个人可以通过学习具备一些能力并且通过经验积累来优化自己的能力。那么如何来设计这样一个类,让该类可以具备责任并自适应变化?
  2. 目的性,现实世界中,解决一个领域的软件总会遇见各种各样的概念,那么需要把这些概念全部抽象为类么?面向对象就是把所有概念全部类化?

对第一点来说,这是一个理想状态,大多数面向对象语言都是静态语言,如C#/Java/C++等,类作为对象的模板,既确定了该类对象的功能,在编译后又决定了对象的内存模型。静态语言使用继承,接口来完成类的扩展,相比动态语言,静态语言在运行速度,类型安全上有着很大优势,但由于类的扩展性是在编译期决定的,要应对变化就需要在类的设计上多下功夫。

针对第二点,这个前面已经提到了,引入对象的唯一原因是具有责任,应对变化,让它“活”起来,是为了让我们更轻松的生活,面向对象是一种方法观,程序执行后仍然被编译成一条条的过程语句执行。不要舍本逐末,来总结一下面向对象的设计经验。

设计模式

设计模式,这里的全称应该是面向对象设计模式,我们熟知的设计模式,通常指GOF定义的23种设计模式。每种模式都有一个对应的名字,按种类可分为创建型,结构型和行为型三类。

介绍面向对象设计模式的文章也很多,模式,按Alexander的经典定义,指在某一背景下某个问题的一种解决方案。这里的解决方案指细节,某一背景下某个问题才是难点,深刻理解“什么时候,什么场合用”比“如何用”更重要,既然是面向对象设计模式,先从对象创建说起。

面向对象是为了适应变化,变化无处不在:

  1. 对象的创建要能适应变化,它不会自己凭空跳出来,必须有其他对象来负责该对象的创建。通常用工厂对象(Factory)来负责对象的创建,在工厂的基类中定义创建对象的虚方法,由工厂的子类来具体化这个创建。
  2. 对象间的组织结构要能适应变化,如一般大公司具有总经理-部门经理-项目经理-员工等职位,完成一件事情需要各部门各员工间的配合,混乱的组织结构会导致难以应对扩张,对象间职位不清等问题。
  3. 对象的行为要能适应变化,为了完成责任,变化可能来自于各个方面,可能会受对象状态的影响,可能需要其他对象的协助等等。

设计模式是一种经验的积累,面向对象设计模式的根本是为了应对对象变化,每种设计模式都对应了一类变化点。这就需要在实际运用中识别变化点,因地制宜的分析可否引入对应的设计模式来最佳化设计。

后记

文章存在草稿里很久了,再接已经没有思路了,简略了描述一下后面的想法:

  1. 关于面向对象设计的方法:面向对象程序设计仍在发展之中,关于如何指导用户从面向对象角度分析和设计程序,四色原型分析方法,领域驱动建模思想,DCI架构等都是一些好的经验。当然,这些经验都是帮助我们更好的去设计程序的,要因地制宜,不要本末倒置,不看实际就选定了一种方法,再生搬硬套把需求塞进这些框框之中。
  2. 关于面向对象设计的优点:前面列举了面向过程的一些好的经验(模块原则,组合原则,扩展原则等),是因为在很多介绍面向对象程序设计的文章中,往往夸大了面向对象设计的优点。这些好的思想原则是通用的,不仅仅是面向对象设计所独有的。面向对象程序设计的优点在于它契合了人们分析自然时概念化的思维方式,在解决真实生活问题时往往比较容易下手。
  3. 关于面向对象设计的缺点:成也萧何败也萧何,如果我们正确分析了需求,找准了可能的变化点,设计出的类模型往往具有较高的价值。如果错误的假设了程序的逻辑,过细/忽略了可能的变化点,则设计出的程序可能会导致结构混乱,抽象过多/无法扩展等问题。面向对象程序设计多呼吁尽早的引入领域专家帮助分析建模,来防止错误的捏造对象导致事倍功半。
  4. 关于面向对象设计的实践:变化是不断存在的,很难存在所谓的完美设计,即使有领域专家的帮助,也很难做到类完全对修改封闭,对扩展开放。随着开发的深入,重构是不可避免的,这里重构的作用既要适应变化,又要保证已有功能的正确性。关于这方面的实践,敏捷是比较热的词汇,敏捷的主张在于用简单的初始设计,然后小步的持续改进,通过TDD来确保每一步改进的正确性。要应对变化,就要预知变化,设计抽象来隔离变化,当然,过度的抽象也会增加软件的复杂度,一个简单的指导原则是,在不能预知变化或者变化不是十分剧烈前,不要过多设计,清晰胜于一切。
  5. 关于面向对象设计和编程语言:众所周知,C++/JAVA/C#等是面向对象的语言,在语言层次提供了类的支持,但并不是这些面向对象语言写出来的一定是面向对象的程序,比如所谓的充血/贫血模型---对象的行为是否被封装在对象其中,还是由Service来提供。这里出现了很多的名字,DomainObject,ValueObject等等,抛去这些名字不谈,如果对象的行为由对象来提供,那么这个对象才真正“活”起来,否则它还是过程化的数据封装。但并不是这样做不好,面向对象设计不是万能药,错误点“活”了对象,不仅不会让你省心,还会让你更加操心。反过来C语言等面向过程语言也可以支持面向对象的思想,Windows的WNDCLASS就是一个很好的例子,换一个角度去讲,如果程序中出现了依赖倒置,依赖于抽象而不依赖于具体,这种抽象体现出来的就是面向对象的思想。

题外话

设计方面的话题总是显得空泛,设计能力的提升来自于经验,脱离了实际去谈设计模式无疑纸上谈兵。设计上面对应需求,下面对应编码,实际又为项目服务,受项目资源制约。好的设计来自于坚持和妥协,也许往大了说很多东西也如此,祝朋友们多点积累,少点折腾,谢谢。

Copyright © 2024 冰天雪域
Powered by .NET 9.0 on Kubernetes