我眼中的面向对象分析

面向对象

似乎我也没学过其他的编程思维方式了,面向对象是我编程时常用的思维方式,因为我觉得它更加贴近于我们的生活,更加容易地去理解和定义程序想要表达的内容。正是因为如此,每当项目要开启的时候,我都会使用该种思维来分析和设计程序。多年下来发现它确实有着它的魅力,帮助我解决了很多设计中的问题。于是我总结了一下,在下面章节中说说面向对象使用上的一些心得。

给你的程序分“类”

面向对象的基础就是“类”的设计,其设计好坏直接影响到程序的结构。那么,如何才能设计出合理的类型呢?本人觉得应该从实际需求出发,提取需求中各种实体对象,最终形成程序中使用的对象。某些同学喜欢为自己的程序埋点,添加很多为日后作扩展的类型,其实我是不建议过度设计,我觉得可以在需求中提及的实体预留一些功能性的扩展,但是不建议为需求中没有的功能点设计类型,因为,你永远都把握不准产品日后的发展会是什么样子的,最终形态是怎么样,也有可能你埋下的点还没来得及实现,程序就需要进行重构了等等的情况都会导致你的额外工作变成无用功。

假如公司要求你做一个附近的陌生人交友的App。从这个App的需求,可以提取出附近陌生人以及交友三个关键字,附近描述的是一种相对的地理位置所以它应该是一个类的属性,而陌生人也是相对于某个人来说的一种人与人的关系,所以这里会被分解为一个的实体,还有一个与其它实体的关系。而最后一个交友关键字其实隐含了一种建立关系的行为,这里可以是聊天,也可以是其它的交友方式。我们再进一步分析,如果App里面提供了聊天功能,那么需求中还隐含了一个实体,那就是聊天的内容。所以,根据需求这些点,我们可以设计出两个类型

//用户类型
class User
{
    //地理位置
    property location : Location;

    //用户的好友
    property friends : Array<User>;

    //给某个用户发送消息
    function sendMessage(user : User,  message : Message) : void;
}

//聊天内容
class Message
{
}

这就由需求所驱动产生的类型,以这些类型为核心类围绕需求进行补充和扩展来不断满足后续的需求。

让类型也符合数据库实体设计

关系数据库设计中常会提及第三范式,其特点是去除表中直接依赖。这意思是说两个实体对象不应该被设计在同一个表中,如:评论表中有记录是某个用户发表的评论,可能会把用户名称等信息归类到评论表中,那这样是不符合第三范式要求的,正确的做法是把用户信息放入用户表,评论表中可以保留一个用户id来标识用户表中的用户记录,从而取消在评论表中直接依赖用户信息,导致用户信息的冗余。

其实我个人觉得这种范式要求很好地划分了实体之间的界线,对于类的设计也同样适用。譬如:你定义了一个人类,该类型有四肢属性和说话的行为。这样很好理解,因为现实中人就具备这些特性。但是在后续的开发过程中由于需求的变动,出现了人类因为有飞行道具可以进行飞行。而把飞行的行为直接赋予人类,这种图省事的做法其实是存在很多弊端的:

  • 首先是不容易读懂,因为常识性的认为人是不会飞的,人会飞的话那肯定是个鸟人。所以会让人摸不着头脑。
  • 再者就是这样的定义往往会限制其它跟你协作开发的同事。因为大多数情况下我们都会秉持着能尽量不动旧代码就不要去动,以免动出问题来。那么你这样的设计就让人不得不按照现有的思维去进行扩展。直到哪一天是在没有办法了,再进行彻底的重构。

所以更优的做法应该是定义出一个道具类。至于道具实现什么功能,由其子类实现:

// 道具基类
class Item
{
    //道具的行为
     function action(target : Person);
}

//飞行道具
class FlyingItem :Item
{
    function action(target : Person)
    {
        //飞行的实现代码
    }
}

类似这样的实现就可以分离人类和道具类型的耦合,而且也提高了道具类型的扩展。所以,在设计类型时一定要给实体分清界线。

另外一个数据库设计时使用较多的就是实体的关系描述。实体的关系不同,在设计表存储数据时也有所不同。通常有三种实体关系:

  • 一对一,即两个实体之间的关系是一一对应的。如:一个中国公民跟他的身份证是唯一绑定的,知道身份证就能够找到对应的人,同样对应的人也能够找到与其对应的唯一的身份证。类似这种关系,通常是建立两张实体数据表,然后关系可以同时放两个实体表中。
  • 一对多,即两种实体之间,第一种实体可以对应多个第二种实体,但是第二种实体只能对应唯一的第一种实体。如用户评论的例子,用户可以发多条评论,但是一条评论只能够是特定的一个用户发出来。对于这种关系,通常也是建立两张实体数据表,然后会在对应多个实体的数据表中记录关系,拿上面的例子来说,就是用户表和评论表,然后评论表会放入一个userId之类的标识来记录与用户的对应关系。
  • 多对多,即两种实体之间,第一种实体可以对应多个第二种实体,第二种实体也可以对应多个第一种实体。例如作者和书籍的关系就是一种多对多的关系,一个作者可以写多本书,同时,一本书有可能有多个作者共同写成。对于这种关系,则需要建立两种实体数据表以及一张关系表来进行描述。

说了这么多实体关系的东西,其实最想说的一点就是,在进行面向对象分析和设计时,关系的分析也是不可缺少的。但是需要怎么来评估关系的划分和归属,我觉得按照数据库的关系设计标准来进行也是一种不错的方法。

  • 一对一,在该种关系下,其实关系的描述可以同时放在两个类型上,如上面所说的中国公民与身份证的例子,用类可以描述为:
class Chinese
{
    //姓名
    property name:String;

    //对应的身份证
    property idCard:IDCard;
}

class IDCard
{
    //身份证号
    property id:String;

    //对应的人
    property person:Chinese;
}
  • 一对多,该种关系下,稍微与数据库表设计有点不同,由于数据表可以通过查询的方式来找出实体对应的多个另一种实体,这已经超出了类设计阶段的范畴。因此,对应多个类实例的类型需要定义一个集合的属性来保留对应的多个类实例。这样在阅读代码时也能够很好理解,拿用户评论的例子来说:
class User
{
    property comments:Set<Comment>;
}

class Comment
{
   property user:User;
}

类似这样能够很好的描述用户进行过多少的评论,然后哪条评论是哪个用户评论的。

  • 多对多,这是一种很复杂的关系,主要是很多情况下,我们都会忽略这样的一种关系,把它直接给变成了一对多的关系设计。其实对于种情况我们都可以将其关系的处理交由另外一个类型进行处理,与数据库表设计一样,两个实体类型,一个关系类型。拿刚才作者跟书籍的关系为例,可以设计成下面的样子:
//作者书籍关系管理类型
class AuthorBookRelationshipManager
{
   //设置书籍的作者信息
   static function setBookAuthor(book : Book, authors : Array<Author>) : void;

    //获取指定书籍的作者
    static function getAuthors(book : Book) : Array<Author>;

    //获取指定作者的书籍
    static function getBooks(author : Author) : Array<Book>;
}

//作者类型
class Author
{
    //作者名称
    property name : String;

    //获取作者出版过的书籍
    function getBooks() : Array<Book>
    {
        return AuthorBookRelationshipManager.getBooks(this);
    }
}

//书籍类型
class Book
{
    //书籍名称
    property name : String;

    //获取书籍的作者信息
    function getAuthors():Array<Author>
    {
        return AuthorBookRelationshipManager.getAuthors(this);
    }
}

上面的代码可以看出,使用了一个AuthorBookRelationshipManager类型来管理书籍和作者的关系。然后实体类型不再保存之间的关系,要获取相关的实体需要通过AuthorBookRelationshipManager类型来获取。

上面所说的实体划分和关系划分在面向对象中有着非常重要的意义。平时要多加练习才能够在面临实际项目开发时做出合理的分类,让自己开发的架构更加灵活更加强大。

以上对于类型的设计基本上是讲完了,是时候要说说继承方面的事情了,这是面向对象的一个很强大的特性。它可以改善代码结构的问题,节省代码的书写工作量。下面将会围绕它来聊聊我的看法。

万物的始祖

其实不管是写C++、C#还是写Java等等这些高级语言,我们都会发现只要是创建的类它们都会直接或者间接地继承自Object这个类。而这个Object类就是我这要说的万物的始祖,其被所有的事物所继承是一个根类型。Object这个名字也起得相当好,泛指了所有的物体,给予了一种无形态的抽象的定义。意味着我们作为程序中的造物主,需要将这种无形态的物体,变化成各种具体形态的事物,这就需要继承

那么,Object中所做的功能其实并不多,因为它不涉及具体的功能,因此它很多时候在实现上面仅仅能区分是否是同一个对象isEqual,又或者是告诉我们它的描述toString。但是这样已经是有很大的引导意义,因为只要继承了Object类型,那么你的类型将会得到这样功能,阐述了继承功能的强大;同时,你还可以把你的子类型对象赋予给一个Object的实例,因为Object是根类,它可以保留任意的子类类型对象,这是面向对象多态的基本体现。

有了Object这样的一个例子,更加表明了我们在开发项目过程中也应该为项目的代码设计基类。而这里的基类要比Object的功能稍微具体一点。因为它涉及到具体要实现的功能需求,而且还应该不止是一个基类。下面来说一下我是怎么样设计基类的。

和基类来个约定

要怎么设计基类,其实还是要根据实际的项目需求而定。假设你所开发的项目包含很多的后台服务,那么就应该定义一个叫BaseService的基类,然后通过这个基类来分化成多种服务类型。如果你的项目涉及到多种UI的处理,那么你就应该考虑可能需要一个基础的UI管理类型(通常UI组件有自己的基类,这里需要的是对UI进行管理和维护的基础类型),如:UIController。然后根据这个基类分化出各种的UI展现。

基本上是根据如果程序存在多种特性相近或相似的功能模块,那么就应该为这些模块建立基类,以便日后更好的进行扩展。这里不建议要给未来有可能会有多种近似功能的模块定义基类,还是老话,你的假设不一定成立。而且等到出现这样的需求在进行定义也不迟,毕竟要改造的也只是一个类型而已。

上面我们知道如何去抽象出基类,那么基类应该要如何封装属性和方法,其实完全可以视其子类而定,因为这样是最贴近需求的。如前面所说的多种后台服务,假设有如下服务类型:

class ServiceA
{
    //服务名称
    property name:String;

   //调用服务
    function exec(config:Dicationary):void
    {
          //执行服务
    }
}

class ServiceB
{
    //服务名称
    property name:String;

    //调用服务
     function call():void
     {
          //执行服务
     }
}

上面所描述的服务从代码结构上面其实不大相同,特别是在执行服务的行为上接收参数也不一样。如果要从它们身上抽象出基类,确实会让人有所困惑。但是,我们既然要抽象基类,那么就肯定会改造子类。我自己总结一些抽象的规则:

  • 子类中相同的属性和方法,要封装到基类中。
  • 对于大多数子类存在的属性和方法(如5个子类中有3个以上类型存在相同的属性和方法),可以考虑封装到基类中,没用到类型可以对基类的属性和方法进行忽略。
  • 对于意义相近的属性,可以考虑将属性合并或替换(如:子类中分别用id或者name属性来表示对象的唯一,那么基类可以考虑只取其中一项属性或者创建一个集合两者的属性定义)。
  • 对于子类相同行为的方法,如果声明的方法参数的数量或类型不同时,可以考虑基类的方法集合子类该方法中的所有参数(即取方法参数的并集)或者考虑定义共有参数,特殊参数则由子类转换为属性来实现(一般可以持续持有的参数可以这样设计,如系统的配置)。

基于上面所说的,我们可以把ServiceAServiceB,通过抽象基类改成下面的样子:

class BaseService
{
    //服务名称
    property name:String;

    //调用服务
    function exec():void;
}

class ServiceA extends BaseService
{
    //这里将参数设定为属性
    property config:Dictionary;

    function exec():void
    {
          //执行服务
    }
}

class ServiceB extends BaseService
{
    function exec():void
    {
         //执行服务
     }
}

类似这样,我们就可以把基类给抽象出来了。

让类型进化

在开发和维护代码的过程里面,难免会碰到由于产品的迭代,导致需求发生重大的变更。那么可能会出现某些功能的废弃或者功能的合并或演进。这时候,之前定义的类型就要面临着重构或者调整的命运。

遇到这样的问题我们先别急着把之前的东西全盘否定,然后彻底重写。而是先考虑之前的定义中是否还存在可用的东西。要把可以重用的模块给提取出来,那么,模块的取舍就是我们需要评估的事情。

如果新的需求中已经废弃的功能,那么模块所涉及的类型都可以进行废弃。对于这些模块的处理,我个人比较喜欢对相关的类型进行直接删除,然后进行编译,报错后直接修改引用到该类型的代码,直到编译成功为止。对于缺失该类型后需要改造的方法,我会暂时不进行改造,而是打上标记(如TODO或者warning宏),等待正式开发新功能时再寻找会这些标记一一进行解决。

如果新需求中包含该类功能,但是又融合了一些新的元素,那么,我们的类型还不能直接拿来使用。要评估新的元素是否作为类型的一部分,还是作一种新的类型。例如:一款聊天应用,刚开始的时候只有私聊(一对一聊天),那么,发送消息就在User类型中:

//用户类型
class User
{
    //给某个用户发送消息
    function sendMessage(user : User,  message : Message) : void;
}

到了第二个版本的时候,可能支持用户给好友群发消息。那么,明显上面的代码不能满足需求,这时候需要对类型进行改造:

//用户类型
class User
{
    //给某个用户发送消息
    function sendMessage(user : User,  message : Message) : void;

    //给多个用户发送消息
    function sendMessage(users : Arrray<User>,  message : Message) : void
    {
        for (User user in users)
        {
            this.sendMessage(user, message);
        }
    }
}

增加了一个sendMessage多态版本的方法用于群发消息。这样既能保证方法的出口统一,又能保证之前的方法不被修改,就可以轻松地解决群发问题。

那么,到了第三个版本的时候出现了聊天室的概念,这时候需求中多了一个聊天室的关键字,运用之前提到的方法,这里应该需要新增一个类型,不能直接在User类型中进行扩展。那么,可以设计为:

//聊天室
class ChatRoom
{
    //聊天室名称
    property name:String;

    //聊天室用户
    property users:Array<User>;

    //发送消息
    function sendMessage(user:User, message:Message):void;
}

这里增加了一个ChatRoom的类型,主要是用来记录那些人在哪个聊天室中。该类型有一个sendMessage的方法,用于表示哪个用户要在聊天室中发言,发言的内容是什么。

由上面的例子可以看出来,产品迭代有时候需要让类型进行,有时候也需要诞生新的类型。要做那种选择则需要根据产品需求来决定。

只要这种多态

多态在我的理解中就是多种状态。就好比我们用手拿东西的时候,如果拿的是球,那么我们可能是想要打球,如果拿的是苹果,那么我们可能要把它吃掉。根据不同东西我们可能会作出不同的反映。在面向对象中多态就是用于解决这类的问题。我们来假设一下没有多态的时候,我们的代码看起来会是多挫:

function doSomething(obj:Object):void
{
    if (obj instanceof ObjectA)
    {
        //do something by ObjectA
    }
    else if (obj instanceof ObjectB)
    {
        //do something by ObjectB
    }
}

要判断的类型越多,if的语句就越长。那么有了多态后,我们可以很优雅地设计:

function doSomething(obj:ObjectA):void
{
    //do something by ObjectA
}

function doSomething(obj:ObjectB):void
{
    //do something by ObjectB
}

多态可以保证代码设计的出口名字统一,不需要外部调用的人要根据不同的类型调用不同名字的方法,对于外部调用只需要传入的类型不同即可决定要调用哪个方法。

如果不是解决上面所说的问题,我不建议使用多态。因为多态会使到类型变得复杂,如果这个使用多态的类型被继承,然后继承的子类进行了多态处理,那么会影响到程序的质量,并且一旦出现问题,排查的难度会有所增加。

后话

以上说的都是我个人在这些年的开发中所理解的东西,面向对象这东西已经存在我脑海里面许多个日夜了,总想着写些什么,今天总算把它给完成了。后续我会继续写下其他的一些关于程序思维的文章,希望大家支持。

写这篇文章的时候没有参考其他资料,可能存在错漏的地方。如果你是一位好心的猿/媛,麻烦给我指正一下。

时间: 2024-10-13 11:46:26

我眼中的面向对象分析的相关文章

【第二周作业】面向过程(或者叫结构化)分析方法与面向对象分析方法到底区别在哪里?

书上的一些概念这里不再复述,仅谈谈自己通过阅读教材.上课听讲后自己的一些理解: 面向过程分析方法注重自顶向下,逐层分析,把整个软件系统的功能逐布分解,各个击破.可以用生活中的一个例子来加以理解——去食堂吃饭.到达食堂(比如琴湖食堂)后,要遵从排队——打饭——阿姨打菜——拿筷子——找位子坐好——开吃,整个过程强调顺序性,比如不拿筷子就坐下是不行的,除非拿手抓.面向过程就是将软件系统所需要实现的功能按照以上类似的思路逐步细分,一步一步要做什么都要分析清楚. 面向对象分析方法则注重分析整个系统的逻辑结

面向过程(或者叫结构化)分析方法与面向对象分析方法到底区别在哪里?

结构化分析方法的分析步骤:1 理解和分析当前的现实环境 已获得当前系统的具体模型 2 建立当前系统的逻辑模型 3 建立目标系统的逻辑模型 4 进一步完善目标系统的逻辑模型 面向对象分析方法:根据面向对象的过程模型 面向对象的需求分析从概念上分为问题分析和应用分析两个方面  问题分析:主要收集并确认用户需求 最后将信息链接最终建立关于对象的分析模型 应用分析:主要是动态描述系统中对象的合法状态序列 并用动态模型表达对象的动态行为 对象之间的消息传递和协同工作的动态信息 综上:结构化分析方法是先创建

软件工程概论第七章--面向对象分析

本章主要讲了面向对象分析,从分析的概念.识别分析类.定义交互行为.建立分析类图和评审分析模式几个方面展开讲述.面向对象分析模型由三个独立模型,功能模型.分析对象模型.动态模型. 分析的概念中主要讲了分析类与分析活动,分析类用于描述系统中较高层次的对象,从软件功能需求来看能划分为实体类.边界类和控制类.分析活动把需求获取阶段产生的用例和场景转换成分析模型. 识别分析类讲了识别边界类.识别控制类.识别实体类三个方面,识别边界类,通常一个参与者与一个用例之间的交互或通信关联对应一个边界类.识别控制类,

面向对象分析与设计—四色原型模式(彩色建模、领域无关模型)(概念版)

阅读目录: 1.背景介绍 2.问自己,UML对你来说有意义吗?它帮助过你对系统进行分析.建模吗? 3.一直以来其实我们被一个缝隙隔开了,使我们对OOAD遥不可及 4.四色原型模式填补这个历史缝隙,让我们真的看见OOAD的希望 5.在四色原型上运用彩色建模增强视觉冲击力 6.通过四色原型模式建模出领域无关模型 7.结束语:建模时你可以不考虑具体实现,但是建模者要懂技术实现 1.背景介绍 至今我都清楚的记得我第一次被面试官问起什么叫"建模"技术时的情景,那是好几年前的事情了,当时是胸有成竹

深入浅出面向对象分析与设计笔记

1.在搜索匹配时注意大小写问题. 2.别为了解决旧问题而产生新问题. 3.使用enum的好处:使用enum的方法或类会受到它的保护,不会有未定义的enum的值.因此不会有打错字或拼错字,对任何具有标准范围或合法值的东西都能避免取得坏数据. 4.任何时候看到重复程序代码,就找个地方进行封装. 5.委托: 6.Java匿名内部类是一种特殊的继承方式,既可以扩展类,也可以实现接口,但是不能两者兼备,而且若实现接口也只能实现一个接口.由于其没有名字,因此不会有命名构造器,但可以实例初始化.如果定义一个匿

cocos2dx游戏开发学习笔记3-lua面向对象分析

在lua中,可以通过元表来实现类.对象.继承等.与元表相关的方法有setmetatable().__index.getmetatable().__newindex. 具体什么是元表在这里就不细说了,网上很多介绍,这里主要讲与cocos2dx相关联的部分. 在lua-binding库中extern.lua里,有如下方法: --Create an class. function class(classname, super) local superType = type(super) local c

《软件工程 ——理论、方法与实践》知识概括第七章 面向对象分析

第7章 面向对象分析    面向对象的分析模型:功能模型.分析对象模型.动态模型. 一.分析的概念 分析类可以划分为实体类.边界类和控制类. 在UML语言中,使用构造型<<entity>>.<<boundary>>和<<control>>分别表示实体类.边界类.控制类. 分析活动:理解用例模型.识别分析类(识别实体类.识别边界类.识别控制类).定义交互行为.建立分析类图(定义属性.定义行为.定义关系).评审分析模型.分析过程是一个循环

面向对象分析与设计

面向对象基本概念 对象:对象是系统中用来描述客观事物的一个实体,它是构成系统的一个基本单位.一个对象由一组属性和对这组属性进行操作的一组服务组成.从更抽象的角度来说,对象是问题域或实现域中某些事物的一个抽象,它反映该事物在系统中需要保存的信息和发挥的作用:它是一组属性和有权对这些属性进行操作的一组服务的封装体.客观世界是由对象和对象之间的联系组成的.主动对象是一组属性和一组服务的封装体,其中至少有一个服务不需要接收消息就能主动执行(称作主动服务). 类:把众多的事物归纳.划分成一些类是人类在认识

面向过程分析方法与面向对象分析方法到底区别

个人理解面向过程分析方法,就是相当于流水线作业,它的关注点是事件的具体过程,比如大学生一天的事情就是,起床->洗脸.刷牙->吃早餐->上课->吃午饭->午休->上课->吃晚饭->做作业.消遣->睡觉,面向过程注重模块化,比如,吃饭模块,睡觉模块等等,它的流程清晰便于组织. 而面向对象的分析方法,侧重于对象,而不是流程,通俗一点就是不注重流程,还是大学生一天的做的事情,用面向对象的方法就会抽象出一个大学生的类,这个类包含了大学生的属性和行为,比如大学生能