专栏首页DDD面向对象是什么

面向对象是什么

近两年设计了几个系统,不管是直接使用传统设计ER图,还是使用4C建模,但在做架构评审时,ER却都是重中之重,让人不得不深思,编程思想经过了一代代发展,为什么还在围绕ER,在远古时代,没有OO,没有DDD,但为什么延续至今的伟大软件也比比皆是

带着这个问题,需要回头看看,结构化编程为什么不行?面向对象因何而起,到底解决了什么问题?

《架构整洁之道》也特别介绍了面向对象编程,面向对象究竟是什么,大多从三大特性:封装、继承、抽象说起,但其实这三种特性并不是面向对象语言特有

结构化编程

提到结构化编程就自然想到其中的顺序结构:代码按照编写的顺序执行,选择结构:if/else,而循环结构:do/while

虽然这些对每个程序员都很熟悉,但其实在结构化编程之间还有非结构化编程,也就是goto语句时代,没有if else、while,一切都通过goto语句对程序控制,它可以让程序跑到任何地方执行,这样当代码规模变大之后,就几乎难以维护

编程是一项难度很大的活动。因为一个程序会包含非常多的细节,远超一个人的认知能力范围,任何一个细微的错误都会导致整个程序出现问题。因此需要将大问题拆分成小问题,逐步递归下去,这样,一个大问题就会被拆解成一系列高级函数的组合,而这些高级函数各自再拆分成一系列低一级函数,一步步拆分下去,每一个函数都需要按照结构化编程方式进行开发,这也是现在常被使用的模块功能分解开发方式

结构化编程中,各模块的依赖关系太强,不能有效隔离开来,一旦需求变动,就会牵一发而动全身,关联的模块由于依赖关系都得变动,那么组织大规模程序就不是它的强项

面向对象

正因为结构化编程的弊端,所以有了面向对象编程,可以更好的组织程序,相对结构局部性思维,我们有了更宏观视角:对象

封装

把一组相关联的数据和函数圈起来,使圈外的代码只能看见部分函数,数据则完全不可见;如类中的公共函数和私有成员变量

提取一下关键字:

1.数据,完全不可见2.函数,只能看见3.相关联

这些似乎就是我们追求的高内聚,也是常提的充血模型,如此看,在实践中最基本的封装都没有达成

到处是贫血模型,一个整体却分成两部分:满是大方法的上帝类service与只有getter和setter的model

service对外提供接口,model传输数据,数据库固化数据,哪有封装性,行为与数据割裂了

怎么才能做到一个高内聚的封装特性呢?

设计一个类,先要考虑其对象应该提供哪些行为。然后,我们根据这些行为提供对应的方法,最后才是考虑实现这些方法要有哪些字段

并且对于这些字段尽可能不提供getter 和 setter,尤其是 setter

暴露getter和setter,一是把实现细节暴露出来了;二是把数据当成了设计核心

方法的命名,体现的是你的意图,而不是具体怎么做

// 修改密码 public void setPassword(final String password) {     this.password = password; }
// 修改密码public void changePassword(final String password) {    this.password = password;}

把setter改成具体的业务方法名,把意图体现出来,将意图与实现分离开来,这是一个优秀设计必须要考虑的问题

构建一个内聚的单元,我们要减少这个单元对外的暴露,也就是定义中的只能看到的函数

这句话的第一层含义是减少内部实现细节的暴露,它还有第二层含义,减少对外暴露的接口

最小化接口暴露。也就是,每增加一个接口,你都要找到一个合适的理由。

总结: 基于行为进行封装,不要暴露实现细节,最小化接口暴露

继承

先看继承定义:

继承(英语:inheritance)是面向对象软件技术当中的一个概念。这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为

从定义看,继承就是为了复用,把一些公共代码放到父类,之后在实现子类时,可以少写一些代码,消除重复,代码复用

继承分为两类:实现继承与接口继承

Child object = new Child();
Parent object = new Child();

但有个设计原则:组合优于继承Composition-over-inheritance

为什么不推荐使用继承呢?

继承意味着强耦合,而高内聚低耦合才符合我们的道,但其实并不是说不能使用继承,对于行为需要使用组合,而数据还得使用继承

这样解释似乎不够形象,再进一步讲,继承也违背了《SOLID》中的OCP[1],继承虽然可以通过子类扩展新的行为,但因为子类可能直接依赖父类实现,导致一个变更可能会影响所有子类。也就是讲继承虽然能Open for extension,但很难做到Closed for modification

借用阿里大牛的示例:

有个游戏,基本规则就是玩家装备武器去攻击怪物

•玩家(Player)可以是战士(Fighter)、法师(Mage)、龙骑(Dragoon)•怪物(Monster)可以是兽人(Orc)、精灵(Elf)、龙(Dragon),怪物有血量•武器(Weapon)可以是剑(Sword)、法杖(Staff),武器有攻击力•玩家可以装备一个武器,武器攻击可以是物理类型(0),火(1),冰(2)等,武器类型决定伤害类型

public abstract class Player {      Weapon weapon}public class Fighter extends Player {}public class Mage extends Player {}public class Dragoon extends Player {}
public abstract class Weapon {    int damage;    int damageType; // 0 - physical, 1 - fire, 2 - ice etc.}public Sword extends Weapon {}public Staff extends Weapon {}

攻击规则如下:

•兽人对物理攻击伤害减半•精灵对魔法攻击伤害减半•龙对物理和魔法攻击免疫,除非玩家是龙骑,则伤害加倍

public class Player {    public void attack(Monster monster) {        monster.receiveDamageBy(weapon, this);    }}
public class Monster {    public void receiveDamageBy(Weapon weapon, Player player) {        this.health -= weapon.getDamage(); // 基础规则    }}
public class Orc extends Monster {    @Override    public void receiveDamageBy(Weapon weapon, Player player) {        if (weapon.getDamageType() == 0) {            this.setHealth(this.getHealth() - weapon.getDamage() / 2); // Orc的物理防御规则        } else {            super.receiveDamageBy(weapon, player);        }    }}
public class Dragon extends Monster {    @Override    public void receiveDamageBy(Weapon weapon, Player player) {        if (player instanceof Dragoon) {            this.setHealth(this.getHealth() - weapon.getDamage() * 2); // 龙骑伤害规则        }        // else no damage, 龙免疫力规则    }}

如果此时,要增加一个武器类型:狙击枪,能够无视一切防御,此时需要修改

1.Weapon,扩展狙击枪Gun2.Player和所有子类(是否能装备某个武器)3.Monster和所有子类(伤害计算逻辑)

public class Monster {    public void receiveDamageBy(Weapon weapon, Player player) {        this.health -= weapon.getDamage(); // 老的基础规则        if (Weapon instanceof Gun) { // 新的逻辑            this.setHealth(0);        }    }}
public class Dragon extends Monster {    public void receiveDamageBy(Weapon weapon, Player player) {        if (Weapon instanceof Gun) { // 新的逻辑                      super.receiveDamageBy(weapon, player);        }        // 老的逻辑省略    }}

由此可见,增加一个规则,几乎链路上的所有类都得修改一遍,越往后业务越复杂,每一次业务需求变更基本要重写一次,这也是为什么建议尽量不要违背OCP,最核心的原因就是现有逻辑的变更可能会影响一些原有代码,导致一些无法预见的影响。这个风险只能通过完整的单元测试覆盖来保障,但在实际开发中很难保障UT的覆盖率

也由此可见继承的确不是代码复用的好方式

从设计原则角度看,继承不是好的复用方式;从语言特性看,也不是鼓励的做法。一是像Java,只能单继承,一旦被继承就再也无法被其他继承,而且java中有Variable Hiding的局限性

比如现在添加一个业务规则:

•战士只能装备剑•法师只能装备法杖

@Datapublic class Fighter extends Player {    private Sword weapon;}
@Testpublic void testEquip() {    Fighter fighter = new Fighter("Hero");
    Sword sword = new Sword("Sword", 10);    fighter.setWeapon(sword);
    Staff staff = new Staff("Staff", 10);    fighter.setWeapon(staff);
    assertThat(fighter.getWeapon()).isInstanceOf(Staff.class); // 错误了}

其实只是修改了父类的weapon,并没有修改子类的;由此编程语言的强类型无法承载业务规则。

继承并不是复用的唯一方法,如ruby中有mixin机制

多态

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态

在上一讲,接口继承更多是多态特性

只使用封装和继承的编程方式,称之为基于对象编程,而只有把多态加进来,才能称之为面向对象编程,有了多态,才将基于对象与面向对象区分开;有了多态,软件设计才有了更大的弹性

多态虽好,但想要运用多态,需要构建出一个抽象,构建抽象需要找出不同事物的共同点,这也是最有挑战地方。在构建抽象上,接口扮演着重要角色:一接口将变的部分和不变部分隔离开来,接口是约定,约定是不变的,变化的是各自的实现;二接口是一个边界,系统模块间通信重要的就是通信协议,而接口就是通信协议的表达

ArrayList<> list = new ArrayList();
List<> list = new ArrayList();

二者之间的差别就在于变量的类型,是面向一个接口,还是面向一个具体的实现类;看似没什么意义,但在《SOLID》[2]中可以发现,几乎所有原则都需要基于接口编程,才能达到目的

而这也就是多态的威力

就java这门语言,继承与多态相互依存,但对于其他语言并不是如此

总结

除了结构化编程和面向对象编程,现在还有函数式编程,然通过上面的阐述,回到开篇的问题,我应该是把编程语言与编程范式搞混了,像结构化编程、面向对象编程是一种编程范式,而具体的C、Java其实是编程语言,对于编程语言是年轻的,的确在很多伟大软件之后才诞生,但编程范式是一直存在的,面向对象范式并不是java之后才有

更不是C语言不能创造伟大软件,语言不过是工具,而最最重要的是思维方式,最近思考为什么TDD,DDD这些驱动式开发都很难,关键还是思维方式的转变

为什么都要看ER图呢,这里面又常被混淆的概念:数据模型与领域模型,下一篇再分解

References

《架构整洁之道》

《软件之美》

[1] 《SOLID》中的OCP: http://www.zhuxingsheng.com/blog/ocp-of-solid.html [2] 《SOLID》: http://www.zhuxingsheng.com/tags/SOLID/

本文分享自微信公众号 - 码农戏码(coder-game),作者:朱先生

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-02-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 面向对象的本质是什么?

      什么是面向对象的本质呢?   万物皆对象?No   抽象?No   复用?No   那到底是什么呢? 万物皆对象。问了几位网友,这是答复之一。看到了某个...

    用户1174620
  • Python - 面向对象编程 - 什么是对象和类

    类似的,上海中心大厦、北京中信大厦这些具体的大厦可以被称为对象,但是不能说大厦是一个对象

    小菠萝测试笔记
  • Python - 面向对象编程 - 什么是 Python 类、类对象、实例对象

    https://www.cnblogs.com/poloyy/p/15178423.html

    小菠萝测试笔记
  • 什么是面向对象

    面向对象的特征有3个,封装、继承、多态。至于抽象的话,个人认为,应该是前面3大特征中都有抽象的思想,毕竟面向对象本身就是一种抽象。 比如 子类 extends ...

    haoming1100
  • 面试官:什么是面向对象?

    Java 是一个支持并发、基于类和面向对象的计算机编程语言。面向对象软件开发具有以下优点:

    田维常
  • python中什么是面向对象

    当遇到一个需求的时候不用自己去实现,如果自己一步步实现那就是面向过程;应该找一个专门做这个事的人来做。

    砸漏
  • 什么是面向对象编程

    说到编程,对于初学者来讲,可能第一想到的就是敲键盘,写代码,做游戏,甚至于会联想到软件破解、网络攻防。另一方面,在学了一些编程的相关知识以后,一个最明显的感觉就...

    一头小山猪
  • 到底什么是面向对象编程?

    这个面试问题让我想到了面向过程与面向对象编程的区别,咱们先简单了解下这两者定义上区别:

    淡定的蜗牛
  • 面向对象的三大特征是什么?

    封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到...

    黑洞代码
  • Java什么叫面向对象

    什么是面向对象呢?这个问题真的是老生常谈,面试的时候经常问。到底什么是面向对象。。。我也不知道啊

    用户7886150
  • 为什么Java不是纯面向对象语言?

    纯面向对象语言或完全面向对象语言是指完全面向对象的语言,它支持或具有将程序内的所有内容视为对象的功能。它不支持原始数据类型(如int,char,float,bo...

    淡定的蜗牛
  • 什么是面向对象? Java 的灵魂概念

    为满足移动端和PC端的双重阅读体验,以及文章质量的保证,开始重构的以及新写的文章都会基于 “语雀” 平台编写,公众号会同步刚发布的文章,但随后的修改或者更新只会...

    BWH_Steven
  • 面向对象和面向过程最本质的区别是什么?

    这是技术面试过程中经常问的问题,真要彻底的讲明白,不是一件很容易的事情。可以简单的用C语言和C++两种语言的本质区别来具体解释,首先从设计模式来讲面向对象更加容...

    程序员互动联盟
  • 什么是面向对象? | Java核心知识点整理

    相信很多Java开发者,在最初接触Java的时候就听说过,Java是一种面向对象的开发语言,那么什么是面向对象呢?

    海拥
  • 什么是类?什么又是对象?

    面向对象的程序设计的核心是对象(上帝式思维),要理解对象为何物,必须把自己当成上帝,上帝眼里世间存在的万物皆为对象,不存在的也可以创造出来。

    以谁为师
  • 如何给女朋友解释什么是面向对象编程?

    周末午后,我正在愉快的打着王者荣耀,五杀在即之际。女朋友拿着一本我看过的《面向对象编程》过来找我。

    小白学视觉
  • 面向对象设计——你究竟想问什么

    面向对象设计(OOD)是技术面试中几乎必考的问题,也算新手村中的老大难问题。常听那些半路转CS的学生朋友们感慨,「算法问题还有刷题网站可以练习,面向对象这种开放...

    包子面试培训
  • 关于面向对象 女神告诉你什么是三大特性

    教材描述问题首先要考虑的就是严谨,不能有错误,但是正是因为严谨,导致语义晦涩难懂,所以往往成了劝退教材。有些看上去高大上,让人摸不着头脑的词,一旦你理解了,发现...

    用户5745563
  • 怎么理解面向对象?

    最近有粉丝提问:面相对象这里听得晕头转向的,根本听不明白什么个意思,大概知道了个class,但是在我看来定义一个类来执行程序和直接def一个函数没什么区别啊,也...

    用户8639654

扫码关注云+社区

领取腾讯云代金券