美文网首页
什么是面向对象

什么是面向对象

作者: 码农戏码 | 来源:发表于2021-02-09 10:48 被阅读0次

    近两年设计了几个系统,不管是直接使用传统设计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,继承虽然可以通过子类扩展新的行为,但因为子类可能直接依赖父类实现,导致一个变更可能会影响所有子类。也就是讲继承虽然能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,扩展狙击枪Gun
    2. 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的局限性

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

    • 战士只能装备剑
    • 法师只能装备法杖
    @Data
    public class Fighter extends Player {
        private Sword weapon;
    }
    
    @Test
    public 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》中可以发现,几乎所有原则都需要基于接口编程,才能达到目的

    而这也就是多态的威力

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

    总结

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

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

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

    Reference

    《架构整洁之道》

    《软件之美》

    相关文章

      网友评论

          本文标题:什么是面向对象

          本文链接:https://www.haomeiwen.com/subject/qiwqxltx.html