抽象 订阅
抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,抽象就是人们在实践的基础上,对于丰富的感性材料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。 [1] 展开全文
抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,抽象就是人们在实践的基础上,对于丰富的感性材料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。 [1]
信息
过    程
分离、提纯、简略 [1]
类    别
表征性抽象、原理性抽象 [2]
中文名
抽象 [1]
外文名
abstract [1]
抽象抽象的概念
抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,抽象就是人们在实践的基础上,对于丰富的感性材料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。 [1]  实际上,抽象是与具体相对应的概念,具体是事物的多种属性的总和,因而抽象亦可理解为由具体事物的多种属性中舍弃了若干属性而固定了另一些属性的思维活动。 [1] 
收起全文
精华内容
下载资源
问答
  • 接口 抽象类 C++
    千次阅读
    2022-01-09 17:07:54

    1. 数据抽象

    数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。

    数据抽象是一种依赖于接口和实现分离的编程(设计)技术。

    让我们举一个现实生活中的真实例子,比如一台电视机,您可以打开和关闭、切换频道、调整音量、添加外部组件(如喇叭、录像机、DVD 播放器),但是您不知道它的内部实现细节,也就是说,您并不知道它是如何通过缆线接收信号,如何转换信号,并最终显示在屏幕上。

    因此,我们可以说电视把它的内部实现和外部接口分离开了,您无需知道它的内部实现原理,直接通过它的外部接口(比如电源按钮、遥控器、声量控制器)就可以操控电视。

    现在,让我们言归正传,就 C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。

    例如,您的程序可以调用 sort() 函数,而不需要知道函数中排序数据所用到的算法。实际上,函数排序的底层实现会因库的版本不同而有所差异,只要接口不变,函数调用就可以照常工作。

    在 C++ 中,我们使用来定义我们自己的抽象数据类型(ADT)。您可以使用类 iostream 的 cout 对象来输出数据到标准输出,如下所示:

    在这里,您不需要理解 cout 是如何在用户的屏幕上显示文本。您只需要知道公共接口即可,cout 的底层实现可以自由改变。

    访问标签强制抽象

    在 C++ 中,我们使用访问标签来定义类的抽象接口。一个类可以包含零个或多个访问标签:

    • 使用公共标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
    • 使用私有标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。

    访问标签出现的频率没有限制。每个访问标签指定了紧随其后的成员定义的访问级别。指定的访问级别会一直有效,直到遇到下一个访问标签或者遇到类主体的关闭右括号为止。

    数据抽象的好处

    数据抽象有两个重要的优势:

    • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
    • 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。

    如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。

    数据抽象的实例

    C++ 程序中,任何带有公有和私有成员的类都可以作为数据抽象的实例。请看下面的实例:

    设计策略

    抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。

    在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

    2. 接口与抽象类

    接口描述了类的行为和功能,而不需要完成类的特定实现。

    C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。

    如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的,如下所示:

    class Box
    {
       public:
          // 纯虚函数
          virtual double getVolume() = 0;
       private:
          double length;      // 长度
          double breadth;     // 宽度
          double height;      // 高度
    };

    设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

    因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。

    可用于实例化对象的类被称为具体类

    抽象类只能作为父类被继承,且子类必须实现存续函数,实现后的函数在子类中是虚函数。抽象类不能定义对象,但是抽象类可以定义指针。

    C++中接口设计满足如下几个基本条件:

    (1)类中没有定义任何的成员变量

    (2)所有成员函数都是共有的

    (3)所有成员函数都是纯虚函数

    (4)接口是一种特殊的抽象类

    #include <iostream>
    #include <string>
    
    using namespace std;
    
    class Channel
    {
    public:
        virtual bool open() = 0;
        virtual void close() = 0;
        virtual bool send(char* buf, int len) = 0;
        virtual int receive(char* buf, int len) = 0;
    };
    
    int main()
    {
        return 0;
    }

    设计策略

    面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。

    外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。

    这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。

    3. C++对外提供库

    使用C++开发语言开发程序,C++分装成的类库文件通过接口方式直接对外提供对应的接口调用方式。首先定义接口类,实现类继承接口实现接口函数,最终外部直接使用接口来对模块或者库进行操作,而不会对内部代码有复杂的更改。

    class interface
    {
        public:
            static interface *create();
            virtual int add(int a, int b) = 0;
    };
    #include <interface.h>
    
    class interfaceimpl : public interface
    {
        public:
            int add(int a, int b)
            {
                return a+ b;
            }
    };
    
    interface* interface::create()
    {
        return new interfaceimpl;        // C++11可以使用只能指针方式来实现
    }
    #include <stdio.h>
    #include <interface.h>
    
    int main()
    {
        interface *in = interface::create();
        int c = in->add(2, 3);
        printf("c : %d\n", c);
    
        return 0;
    }

    提供C++语言形式的头文件时,我们不希望暴露太多的细节,因此,一般定义一个抽象接口,然后把这个接口头文件提供出去。

    这个抽象接口还必须有一个静态的工厂函数,而这个工厂函数的具体实现也是在库里面实现。 通过这个工厂函数就可以将真正的

    类对象创建出来,这个类对象是继承并实现了接口类的。

    抽象类的介绍

    抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

    • (1)抽象类的定义:称带有纯虚函数的类为抽象类。

    • (2)抽象类的作用:抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

    • (3)使用抽象类时注意:a、抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。b、抽象类是不能定义对象的。


    总结:

    • 1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。

    • 2、虚函数声明如下:virtual ReturnType FunctionName(Parameter); 虚函数必须实现,如果不实现,编译器将报错,错误提示为:

      error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
    • 3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

    • 4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。

    • 5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。

    • 6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。

    • 7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。

    • 8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

    有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。

    定义纯虚函数就是为了让基类不可实例化化

    因为实例化这样的抽象数据结构本身并没有意义。

    或者给出实现也没有意义

    实际上我个人认为纯虚函数的引入,是出于两个目的

    • 1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。

    • 2、为了效率,不是程序执行的效率,而是为了编码的效率。

    更多相关内容
  • 主要介绍了JAVA抽象类和抽象方法(abstract),结合实例形式分析了java抽象类及抽象方法相关定义、使用技巧与操作注意事项,需要的朋友可以参考下
  • 抽象代数基础 丘维声.pdf
  • abstract 关键字用于定义抽象方法与抽象类。 抽象方法 抽象方法指没有方法体的方法,具体就是在方法声明的时候没有 {} 括弧以及其中的内容,而是直接在声明时在方法名后加上分号结束。 abstract 关键字用于定义抽象...
  • 抽象代数包含群论、环论、伽罗瓦理论、格论、线性代数等许多分支,并与数学其它分支相结合产生了代数几何、代数数论、代数拓扑、拓扑群等新的数学学科。抽象代数也是现代计算机理论基础之一。 最完整, 最清晰
  • 抽象类与抽象方法由abstract修饰 abstract的使用注意 抽象方法没有方法体 抽象成员只能存在于抽象类中 抽象类可以有非抽象成员(侧重族群的概念) 抽象类的子类必须实现抽象方法体 抽象类you构造函数 抽象类只能...
  • 今天在云和学院学了很多,我这次只能先总结一下C#中的虚方法和抽象的运用。 理论: 虚方法: 用virtual修饰的方法叫做虚方法 虚方法可以在子类中通过override关键字来重写 常见的虚方法:ToString() Equals 抽象...
  • 抽象工厂模式uml类图

    2018-03-07 10:41:31
    java设计模式 抽象工厂模式详解 一张图让你彻底明白抽象工厂模式
  • 丘维声-抽象代数

    2017-04-27 20:53:24
    丘维声的抽象代数
  • 1、如果一个类中有一个方法是抽象方法,则这个类就是抽象类; 2、抽象类必须加上abstract关键字修饰; 抽象方法: 定义:一个方法如果没有方法体,则这个方法就是抽象方法 1、一个方法没有方法体,直接用分号结束的...
  • 程序员必备的思维能力:抽象思维

    万次阅读 多人点赞 2021-02-12 18:03:28
    若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。——大卫·林奇抽象思维是我们工程师最重要的思维能力。因为软件技术 本质上就是一门抽象的艺术。我们的工作是存思维...

    若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。——大卫·林奇

    抽象思维是我们工程师最重要的思维能力。因为软件技术 本质上就是一门抽象的艺术。我们的工作是存思维的“游戏”,虽然我们在使用键盘、显示器,打开电脑可以看到主板、硬盘等硬件。但我们即看不到程序如何被执行,也看不到0101是如何被CPU处理的。

    我们工程师每天都要动用抽象思维,对问题域进行分析、归纳、综合、判断、推理。从而抽象出各种概念,挖掘概念和概念之间的关系,对问题域进行建模,然后通过编程语言实现业务功能。所以,我们大部分的时间并不是在写代码,而是在梳理需求,理清概念,当然,也包括尝试看懂那些“该死的、别人写的”代码。

    在我接触的工程师中,能深入理解抽象概念的并不多,能把抽象和面向对象、架构设计进行有机结合,能用抽象思维进行问题分析、化繁为简的同学更是凤毛麟角。

    对于我本人而言,每当我对抽象有进一步的理解和认知,我都能切身感受到它给我在编码和设计上带来的质的变化。同时感慨之前对抽象的理解为什么如此肤浅。如果时间可以倒流的话,我希望我在我职业生涯的早期,就能充分意识到抽象的重要性,能多花时间认真的研究它,深刻的理解它,这样应该可以少走很多弯路。

    1.1 什么是抽象

    关于抽象的定义,百度百科是这样说的:

    抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,抽象就是人们在实践的基础上,对于丰富的感性材料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。
    实际上,抽象是与具体相对应的概念,具体是事物的多种属性的总和,因而抽象亦可理解为由具体事物的多种属性中舍弃了若干属性而固定了另一些属性的思维活动。[1]

    Wikipedia的解释是:

    抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个皮质的足球,我们可以过滤它的质料等信息,得到更一般性的概念,也就是球。从另外一个角度看,抽象就是简化事物,抓住事物本质的过程。[2]

    简单而言,“抽”就是抽离,“象”就是具象,字面上理解抽象,抽象的过程就是从“具象”事物中归纳出共同特征,“抽取”得到一般化(Generalization)的概念的过程。英文的抽象——abstract来自拉丁文abstractio,它的原意是排除、抽出。

    为了更好的方便你理解抽象,让我们先来看一幅毕加索的画,如下图所示,图的左边是一头水牛,是具象的,右边是毕加索画,是抽象的。怎么样,是不是感觉自己一下子理解了抽象画的含义。 

    可以看到,抽象牛只有几根线条,不过这几根线条是做了高度抽象之后的线条,过滤了水牛的绝大部分细节,保留了牛最本质特征,比如牛角,牛头,牛鞭、牛尾巴等等。这种对细节的舍弃使得“抽象牛”具有更好的泛化(Generalization)能力。可以说,抽象更接近问题的本质。也就是说所有的牛都逃不过这几根线条。

    1.2 抽象和语言是一体的

    关于抽象思维,我们在百度百科上可以看到如下的定义:

    抽象思维,又称词(概念)的思维或者逻辑思维,是指用词(概念)进行判断、推理并得出结论的过程。抽象思维以词(概念)为中介来反映现实。这是思维的最本质特征,也是人的思维和动物心理的根本区别。[3]

    之所以把抽象思维称为词思维或者概念思维,是因为语言和抽象是一体的。当我们说“牛”的时候,说的就是“牛”的抽象,他代表了所有牛共有的特征。同样,当你在程序中创建Cow这个类的时候,道理也是一样。在生活中,我们只见过一头一头具象的牛,“牛”作为抽象的存在,即看不见也摸不着。

    这种把抽象概念作为世界本真的看法,也是古希腊哲学家柏拉图的最重要哲学思想。柏拉图认为,我们所有用感觉感知到的事物,都源于相应的理念。他认为具体事物的“名”,也就是他说的“理念世界”才是本真的东西,具体的一头牛,有大有小,有公有母,颜色、性情、外形各自不同。因此我们不好用个体感觉加以概括,但是这些牛既然都被统称为“牛”,则说明它们必然都源于同一个“理念”,即所谓“牛的理念”或者“理念的牛”,所以它们可以用“牛”加以概括。尚且不论“理念世界”是否真的存在,这是一个哲学问题,但有一点可以确定,我们的思考,对概念的表达都离不开语言。[4]

    这也是为什么,我在做设计和代码审查(Code Review)的时候,会特别关注命名是否合理的原因。因为命名的好坏,在很大程度上反应了我们对一个概念的思考是否清晰,我们的抽象是否合理,反应在代码上就是,代码的可读性、可理解性是不是良好,以及我们的设计是不是到位。

    有人做过一个调查,问程序员最头痛的事情是什么,通过Quora和Ubuntu Forum的调查结果显示,程序员最头疼的事情是命名。如果你曾经为了一个命名而绞尽脑汁,就不会对这个结果感到意外。

    就像Stack Overflow的创始人Joel Spolsky所说的:“起一个好名字应该很难,因为,一个好名字需要把要义浓缩在一到两个词。(Creating good names is hard, but it should be hard, because a great name captures essential meaning in just one or two words)。”

    是的,这个浓缩的过程就是抽象的过程。我不止一次的发现,当我觉得一个地方的命名有些别扭的时候,往往就意味着要么这个地方我没有思考清楚,要么是我的抽象弄错了。

    关于如何命名,我在《代码精进之路》里已经有比较详尽的阐述,这里就不赘述了。

    我想强调的是语言是明晰概念的基础,也是抽象思维的基础,在构建一个系统时,值得我们花很多时间去斟酌、去推敲语言。在我做过的一个项目中,就曾为一个关键实体讨论了两天,因为那是一个新概念,尝试了很多名字,始终感觉到别扭、不好理解。随着我们讨论的深入,对问题域理解的深入,我们最终找到了一个相对比较合适的名字,才肯罢休。

    这样的斟酌是有意义的,因为明晰关键概念,是我们设计中的重要工作。虽然不合理的命名,不合理的抽象也能实现业务功能。但其代价就是维护系统时,极高的认知负荷。随着时间的推移,就没人能搞懂系统的设计了。

    1.3 抽象的层次性

    回到毕加索的抽象画,如下图所示,如果映射到面向对象编程,抽象牛就是抽象类(Abstract Class),代表了所有牛的抽象。抽象牛可以泛化成更多的牛,比如水牛、奶牛、牦牛等。每一种牛都代表了一类(Class)牛,对于每一类牛,我们可以通过实例化,得到一头具体的牛实例(Instance)。 

    从这个简单的案例中,我们可以到抽象的三个特点:

    1. 第一,抽象是忽略细节的。抽象类是最抽象的,忽略的细节也最多,就像抽象牛,只是几根线条而已。在代码中,这种抽象可以是Abstract Class,也可以是Interface。

    2. 第二,抽象代表了共同性质。类(Class)代表了一组实例(Instance)的共同性质,抽象类(Abstract Class)代表了一组类的共同性质。对于我们上面的案例来说,这些共同性质就是抽象牛的那几根线条。

    3. 第三,抽象具有层次性。抽象层次越高,内涵越小,外延越大,也就是说它的涵义越小,泛化能力越强。比如,牛就要比水牛更抽象,因为它可以表达所有的牛,水牛只是牛的一个种类(Class)。

    抽象的这种层次性,是除了抽象概念之外,另一个我们必须要深入理解的概念,因为小到一个方法要怎么写,大到 一个系统要如何架构,以及我们后面第三章要介绍的结构化思维,都离不开抽象层次的概念。

    在进一步介绍抽象层次之前,我们先来理解一下外延和内涵的意思:

    抽象是以概念(词语)来反映现实的过程,每一个概念都有一定的外延和内涵.概念的外延就是适合这个概念的一切对象的范围,而概念的内涵就是这个概念所反映的对象的本质属性的总和.例如“平行四边形”这个概念,它的外延包含着一切正方形、菱形、矩形以及一般的平行四边形,而它的内涵包含着一切平行四边形所共有的“有四条边,两组对边互相平行”这两个本质属性。

    一个概念的内涵愈广,则其外延愈狭;反之,内涵愈狭,则其外延愈广。例如,“平行四边形”的内涵是“有四条边,两组对边互相平行”,而“菱形”的内涵除了这两条本质属性外,还包含着“四边相等”这一本质属性。“菱形”的内涵比“平行四边形”的内涵广,而“菱形”的外延要比“平行四边形”的外延狭。

    所谓的抽象层次就体现在概念的外延和内涵上,这种层次性,基本可以体现在任何事物上,比如一份报纸就存在多个层次上的抽象,“出版品”最抽象,其内涵最小,但外延最大,“出版品”可以是报纸也可以是期刊杂志等。

    1. 一个出版品

    2. 一份报纸

    3. 《旧金山纪事报》

    4. 5 月 18 日的《旧金山纪事报》

    当我要统计美国有多少个出版品,那么就要用到最上面第一层“出版品”的抽象,如果我要查询旧金山5月18日当天的新闻,就要用到最下面第四层的抽象。

    每一个抽象层次都有它的用途,对于我们工程师来说,如何拿捏这个抽象层次是对我们设计能力的考验,抽象层次太高和太低都不行。

    比如,现在要写一个水果程序,我们需要对水果进行抽象,因为水果里面有红色的苹果,我们当然可以建一个RedApple的类,但是这个抽象层次有点低,只能用来表达“红色的苹果”。来一个绿色的苹果,你还得新建一个GreenApple类。

    为了提升抽象层次,我们可以把RedApple类改成Apple类,让颜色变成Apple的属性,这样红色和绿色的苹果就都能表达了。再继续往上抽象,我们还可以得到水果类、植物类等。再往上抽象就是生物、物质了。

    你可以看到,抽象层次越高,内涵越小,外延越大,泛化能力越强。然而,其代价就是业务语义表达能力越弱。 

    具体要抽象到哪个层次,要视具体的情况而定了,比如这个程序是专门研究苹果的可能到Apple就够了,如果是卖水果的可能需要到Fruit,如果是植物研究的可能要到Plant,但很少需要到Object。

    我经常开玩笑说,你把所有的类都叫Object,把所有的参数都叫Map的系统最通用,因为Object和Map的内涵最小,其延展性最强,可以适配所有的扩展。从原理上来说,这种抽象也是对的,万物皆对象嘛。但是这种抽象又有什么意义呢?它没有表达出任何想表达的东西,只是一句正确的废话而已。

    越抽象,越通用,可扩展性越强,然而其语义的表达能力越弱。越具体,越不好延展,然而其语义表达能力很强。所以,对于抽象层次的权衡,是我们系统设计的关键所在,也是区分普通程序员和优秀程序员的关键所在。

    1.4 软件中的分层抽象无处不在

    越是复杂的问题越需要分层抽象,分层是分而治之,抽象是问题域的合理划分和概念语义的表达。不同层次提供不同的抽象,下层对上层隐藏实现细节,通过这种层次结构,我们才有可能应对像网络通信、云计算等超级复杂的问题。

    网络通信是互联网最重要的基础实施,但同时它又是一个很复杂的过程,你要知道把数据包传给谁——IP协议,你要知道在这个不可靠的网络上出现状况要怎么办——TCP协议。有这么多的事情需要处理,我们可不可以在一个层次中都做掉呢?当然是可以的,但显然不科学。因此,ISO制定了网络通信的七层参考模型,每一层只处理一件事情,低层为上层提供服务,直到应用层把HTTP,FTP等方便理解和使用的协议暴露给用户。 

    编程语言的发展史也是一个典型的分层抽象的演化史。

    机器能理解的只有机器语言,即各种二进制的01指令。如果我们采用O1的输入方式,其编程效率极低(学过数字电路的同学,体会下用开关实现加减法)。所以我们用汇编语言抽象了二进制指令。然而汇编还是很底层,于是我们用C语言抽象了汇编语言。而高级语言Java是类似于C这样低级语言的进一步抽象,这种逐层抽象极大的提升了我们的编程效率。 

    1.5 重复代码是抽象的缺失

    如果说抽象的本质是共性的话,那么我们代码中的重复代码,是不是就意味着抽象的缺失呢?

    是这样的,重复代码是典型的代码坏味道,其本质问题就是抽象的缺失。因为我们Ctrl+C加Ctrl+V的工作习惯,导致没有对共性代码进行抽取,或者虽然抽取了,只是简单的用了一个Util名字,没有给到一个合适的名字,没有正确的反应这段代码所体现的抽象概念,都属于抽象不到位。

    有一次,我在Review团队代码的时候,发现有一段组装搜索条件的代码,在几十个地方都有重复。这个搜索条件还比较复杂,是以元数据的形式存在数据库中,因此组装的过程是这样的:

    • 首先,我们要从缓存中把搜索条件列表取出来;

    • 然后,遍历这些条件,将搜索的值填充进去;

    //取默认搜索条件
    List<String> defaultConditions = searchConditionCacheTunnel.getJsonQueryByLabelKey(labelKey);
    for(String jsonQuery : defaultConditions){
    	jsonQuery = jsonQuery.replaceAll(SearchConstants.SEARCH_DEFAULT_PUBLICSEA_ENABLE_TIME, String.valueOf(System.currentTimeMillis() / 1000));
    	jsonQueryList.add(jsonQuery);
    }
    //取主搜索框的搜索条件
    if(StringUtils.isNotEmpty(cmd.getContent())){
        List<String> jsonValues = searchConditionCacheTunnel.getJsonQueryByLabelKey(SearchConstants.ICBU_SALES_MAIN_SEARCH);
        for (String value : jsonValues) {
    		String content = StringUtil.transferQuotation(cmd.getContent());
    		value = StringUtil.replaceAll(value, SearchConstants.SEARCH_DEFAULT_MAIN, content);
        	jsonQueryList.add(value);
    	}
    }
    

    简单的重构无外乎就是把这段代码提取出来,放到一个Util类里面给大家复用。然而我认为这样的重构只是完成了工作的一半,我们只是做了简单的归类,并没有做抽象提炼。

    简单分析,不难发现,此处我们是缺失了两个概念:一个是用来表达搜索条件的类——SearchCondition;另一个是用来组装搜索条件的类——SearchConditionAssembler。只有配合命名,显性化的将这两个概念表达出来,才是一个完整的重构。

    重构后,搜索条件的组装会变成一种非常简洁的形式,几十处的复用只需要引用SearchConditionAssembler就好了。

    public class SearchConditionAssembler {
        public static SearchCondition assemble(String labelKey){
            String jsonSearchCondition =  getJsonSearchConditionFromCache(labelKey);
            SearchCondition sc = assembleSearchCondition(jsonSearchCondition);
            return sc;
        }
    }
    

    由此可见,提取重复代码只是我们重构工作的第一步。对重复代码进行概念抽象,寻找有意义的命名才是我们工作的重点。

    因此,每一次遇到重复代码的时候,你都应该感到兴奋,想着,这是一次锻炼抽象能力的绝佳机会,当然,测试代码除外。

    1.6 强制类型转换是抽象层次有问题

    面向对象设计里面有一个著名的SOLID原则是由Bob大叔(Robert Martin)提出来的,其中的L代表LSP,就是Liskov Substitution Principle(里氏替换原则)。简单来说,里氏替换原则就是子类应该可以替换任何父类能够出现的地方,并且经过替换以后,代码还能正常工作。

    思考一下,我们在写代码的过程中,什么时候会用到强制类型转换呢?当然是LSP不能被满足的时候,也就是说子类的方法超出了父类的类型定义范围,为了能使用到子类的方法,只能使用类型强制转换将类型转成子类类型。

    举个例子,在苹果(Apple)类上,有一个isSweet()方法是用来判断水果甜不甜的;西瓜(Watermelon)类上,有一个isJuicy()是来判断水分是否充足的;同时,它们都共同继承一个水果(Fruit)类

    此时,我们需要挑选出甜的水果和有水分的习惯,我们会写一个如下的程序:

    public class FruitPicker {
    
        public List<Fruit> pickGood(List<Fruit> fruits){
            return fruits.stream().filter(e -> check(e)).
                    collect(Collectors.toList());
        }
        
        private boolean check(Fruit e) {
            if(e instanceof Apple){
                if(((Apple) e).isSweet()){
                    return true;
                }
            }
            if(e instanceof Watermelon){
                if(((Watermelon) e).isJuicy()){
                    return true;
                }
            }
            return false;
        }
    }
    

    因为pick方法的入参的类型是Fruit,所以为了获得Apple和Watermelon上的特有方法,我们不得不使用instanceof做一个类型判断,然后使用强制类型转换转成子类类型,以便获得他们的专有方法,很显然,这是违背了里式替换原则的。

    这里问题出在哪里?对于这样的代码我们要如何去优化呢?仔细分析一下,我们可以发现,根本原因是因为isSweet和isJuicy的抽象层次不够,站在更高抽象层次也就是Fruit的视角看,我们挑选的就是可口的水果,只是具体到苹果我们看甜度,具体到西瓜我们看水分而已。

    因此,解决方法就是对isSweet和isJuicy进行抽象,并提升一个层次,在Fruit上创建一个isTasty()的抽象方法,然后让苹果和西瓜类分别去实现这个抽象方法就好了。 

    下面是重构后的代码,通过抽象层次的提升我们消除了instanceof判断和强制类型转换,让代码重新满足了里式替换原则。抽象层次的提升使得代码重新变得优雅了。

    public class FruitPicker {
    
        public List<Fruit> pickGood(List<Fruit> fruits){
            return fruits.stream().filter(e -> check(e)).
                    collect(Collectors.toList());
        }
        
        //不再需要instanceof和强制类型转换
        private boolean check(Fruit e) {
            return e.isTasty();
        }
    }
    
    

    所以,每当我们在程序中准备使用instanceof做类型判断,或者用cast做强制类型转换的时候。每当我们的程序不满足LSP的时候。你都应该警醒一下,好家伙,这又是一次锻炼抽象能力的绝佳机会。

    1.7 如何提升抽象思维能力

    抽象思维能力是我们人类特有的、与生俱来的能力,除了上面说的在编码过程中可以锻炼抽象能力之外,我们还可以通过一些其他的练习,不断的提升我们的抽象能力。

    多阅读

    为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面的时候会将你的大脑铺满,较少需要抽象和想象。

    这也是为什么我们不提倡让小孩子过多的暴露在电视或手机屏幕前的原因,因为这样不利于他抽象思维的锻炼。

    抽象思维的差别让孩子们的学习成绩从初中开始分化,许多不能适应这种抽象层面训练的,就去读技校了,因为技校比大学会更加具象:车铣刨磨、零部件都能看得见摸得着。体力劳动要比脑力劳动来的简单。

    多总结沉淀

    小时候不理解,语文老师为什么总是要求我们总结段落大意、中心思想什么的。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维能力。

    记录也是很好的总结习惯。就拿读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳书中的内容,这样不仅可以加深理解,而且还可以提升自己的抽象思维能力。

    我从四年前开始系统的记录笔记,做总结沉淀,构建自己的知识体系。这种思维训练的好处显而易见,可以说我之前写的《从码农到工匠》和现在正在写的《程序员必备的思维能力》都离不开我总结沉淀的习惯。

    命名训练

    每一次的变量命名、方法命名、类命名都是一次难得的抽象思维训练机会,前面已经说过了,语言和抽象是一体的,命名的好坏直接反应了我们的问题域思考的是否清晰,反应了我们抽象的是否合理。

    现实情况是,我们很多的工程师常常忽略了命名的重要性,只要能实现业务功能,名字从来就不是重点。

    实际上,这是对系统的不负责任,也是对自己的不负责任,更是对后期维护系统的人不负责任。写程序和写文章有很大的相似性,本质上都是在用语言阐述一件事情。试想下,如果文章中用的都是些词不达意的句子,这样的文章谁能看得懂,谁又愿意去看呢。

    同样,我一直强调代码要显性化的表达业务语义,其中命名在这个过程中扮演了极其重要的角色。为了代码的可读性,为了系统的长期可维护性,为了我们自身抽象思维的训练,我们都不应该放过任何一个带有歧义、表达模糊、意不清的命名。

    领域建模训练

    对于技术同学,我们还有一个非常好的提升抽象能力的手段——领域建模。当我们对问题域进行分析、整理和抽象的时候,当我们对领域进行划分和建模的时候,实际上也是在锻炼我们的抽象能力。

    我们可以对自己工作中的问题域进行建模,当然也可以通过阅读一些优秀源码背后的模型设计来学习如何抽象、如何建模。比如,我们知道Spring的核心功能是Bean容器,那么在看Spring源码的时候,我们可以着重去看它是如何进行Bean管理的?它使用的核心抽象是什么?不难发现,Spring是使用了BeanDefinition、BeanFactory、BeanDefinitionRegistry、BeanDefinitionReader等核心抽象实现了Bean的定义、获取和创建。抓住了这些核心抽象,我们就抓住了Spring设计主脉。

    除此之外,我们还可以进一步深入思考,它为什么要这么抽象?这样抽象的好处是什么?以及它是如何支持XML和Annotation(注解)这两种关于Bean的定义的。

    这样的抽象思维锻炼和思考,对提升我们的抽象能力和建模能力非常重要。关于这一点,我深有感触,初入职场的时候,当我尝试对问题域进行抽象和建模的时候,会觉得无从下手,建出来的模型也感觉很别扭。然而,经过长期的、刻意的学习和锻炼之后,很明显可以感觉到我的建模能力和抽象能力都有很大的提升。不但分析问题的速度更快了,而且建出来的模型也更加优雅了。

    1.8 小结

    • 抽象思维是程序员最重要的思维能力,抽象的过程就是寻找共性、归纳总结、综合分析,提炼出相关概念的过程。

    • 语言和抽象是一体的,抽象思维也叫词思维,因为抽象的概念只能通过语言才能表达出来。

    • 抽象是有层次性的,抽象层次越高,内涵越小,外延越大,扩展性越好;反之,抽象层次越低,内涵越大,外延越小,扩展性越差,但语义表达能力越强。

    • 对抽象层次的拿捏,体现了我们的设计功力,视具体情况而定,抽象层次既不能太高,也不能太低。

    • 重复代码意味着抽象缺失,强制类型转换意味着抽象层次有问题,我们可以利用这些信号来重构代码,让代码重新变的优雅。

    • 我们可以通过刻意练习来提升抽象能力,这些练习包括阅读、总结、命名训练、建模训练等。

    [1] https://baike.baidu.com/item/抽象/9021828

    [2] https://zh.wikipedia.org/wiki/抽象化

    [3] https://baike.baidu.com/item/抽象思维

    [4] https://www.sohu.com/a/359915387_260616

    展开全文
  • 该书的前8章首次提出编程领域中的一些十分有用的抽象概念,如过程抽象、迭代抽象以及最重要的数据抽象等。此外,本书通过大量的例子,用非形式化的规范来详细定义这些数据抽象,描述模块所需完成的任务,并定义了...
  • 当程序员具备了抽象思维

    万次阅读 2021-03-22 14:32:46
    硕大而抽象,且非常美丽。 作者:张建飞 若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。——大卫·林奇 前言 抽象思维是我们工程师最重要的思维能力。因为软件技术 ...

    简介: 若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。

    作者:张建飞

     

    若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。——大卫·林奇

     

     

    前言

     

    抽象思维是我们工程师最重要的思维能力。因为软件技术 本质上就是一门抽象的艺术。我们的工作是存思维的“游戏”,虽然我们在使用键盘、显示器,打开电脑可以看到主板、硬盘等硬件。但我们即看不到程序如何被执行,也看不到 0101 是如何被 CPU 处理的。

     

    我们工程师每天都要动用抽象思维,对问题域进行分析、归纳、综合、判断、推理。从而抽象出各种概念,挖掘概念和概念之间的关系,对问题域进行建模,然后通过编程语言实现业务功能。所以,我们大部分的时间并不是在写代码,而是在梳理需求,理清概念。当然,也包括尝试看懂那些“该死的、别人写的”代码。

     

    在我接触的工程师中,能深入理解抽象概念的并不多,能把抽象和面向对象、架构设计进行有机结合,能用抽象思维进行问题分析、化繁为简的同学更是凤毛麟角。

     

    对于我本人而言,每当我对抽象有进一步的理解和认知,我都能切身感受到它给我在编码和设计上带来的质的变化。同时,感慨之前对抽象的理解为什么如此肤浅。如果时间可以倒流的话,我希望我在我职业生涯的早期,就能充分意识到抽象的重要性,能多花时间认真的研究它,深刻的理解它,这样应该可以少走很多弯路。

     

    什么是抽象

     

    关于抽象的定义,百度百科是这样说的:

     

    抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,抽象就是人们在实践的基础上,对于丰富的感性材料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。实际上,抽象是与具体相对应的概念,具体是事物的多种属性的总和,因而抽象亦可理解为由具体事物的多种属性中舍弃了若干属性而固定了另一些属性的思维活动。[1]

     

    Wikipedia 的解释是:

     

    抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个皮质的足球,我们可以过滤它的质料等信息,得到更一般性的概念,也就是球。从另外一个角度看,抽象就是简化事物,抓住事物本质的过程。[2]

     

    简单而言,“抽”就是抽离,“象”就是具象,字面上理解抽象,抽象的过程就是从“具象”事物中归纳出共同特征,“抽取”得到一般化(Generalization)的概念的过程。英文的抽象——abstract 来自拉丁文 abstractio,它的原意是排除、抽出。

     

    为了更好的方便你理解抽象,让我们先来看一幅毕加索的画,如下图所示,图的左边是一头水牛,是具象的;右边是毕加索画,是抽象的。怎么样,是不是感觉自己一下子理解了抽象画的含义。

    image.png

     

    可以看到,抽象牛只有几根线条,不过这几根线条是做了高度抽象之后的线条,过滤了水牛的绝大部分细节,保留了牛最本质特征,比如牛角,牛头,牛鞭、牛尾巴等等。这种对细节的舍弃使得“抽象牛”具有更好的泛化(Generalization)能力。

     

    可以说,抽象更接近问题的本质,也就是说所有的牛都逃不过这几根线条。

     

    抽象和语言是一体的

     

    关于抽象思维,我们在百度百科上可以看到如下的定义:

     

    抽象思维,又称词(概念)的思维或者逻辑思维,是指用词(概念)进行判断、推理并得出结论的过程。抽象思维以词(概念)为中介来反映现实。这是思维的最本质特征,也是人的思维和动物心理的根本区别。[3]

     

    之所以把抽象思维称为词思维或者概念思维,是因为语言和抽象是一体的。当我们说“牛”的时候,说的就是“牛”的抽象,他代表了所有牛共有的特征。同样,当你在程序中创建 Cow 这个类的时候,道理也是一样。在生活中,我们只见过一头一头具象的牛,“牛”作为抽象的存在,即看不见也摸不着。

     

    这种把抽象概念作为世界本真的看法,也是古希腊哲学家柏拉图的最重要哲学思想。柏拉图认为,我们所有用感觉感知到的事物,都源于相应的理念。他认为具体事物的“名”,也就是他说的“理念世界”才是本真的东西,具体的一头牛,有大有小,有公有母,颜色、性情、外形各自不同。因此我们不好用个体感觉加以概括,但是这些牛既然都被统称为“牛”,则说明它们必然都源于同一个“理念”,即所谓“牛的理念”或者“理念的牛”,所以它们可以用“牛”加以概括。尚且不论“理念世界”是否真的存在,这是一个哲学问题,但有一点可以确定,我们的思考和对概念的表达都离不开语言。[4]

     

    这也是为什么,我在做设计和代码审查(Code Review)的时候,会特别关注命名是否合理的原因。因为命名的好坏,在很大程度上反映了我们对一个概念的思考是否清晰,我们的抽象是否合理,反应在代码上就是,代码的可读性、可理解性是不是良好,以及我们的设计是不是到位。

     

    有人做过一个调查,问程序员最头痛的事情是什么,通过 Quora 和 Ubuntu Forum 的调查结果显示,程序员最头疼的事情是命名。如果你曾经为了一个命名而绞尽脑汁,就不会对这个结果感到意外。

     

    就像 Stack Overflow 的创始人 Joel Spolsky 所说的:“起一个好名字应该很难,因为,一个好名字需要把要义浓缩在一到两个词。(Creating good names is hard, but it should be hard, because a great name captures essential meaning in just one or two words)。”

     

    是的,这个浓缩的过程就是抽象的过程。我不止一次的发现,当我觉得一个地方的命名有些别扭的时候,往往就意味着要么这个地方我没有思考清楚,要么是我的抽象弄错了。

     

    关于如何命名,我在《代码精进之路》里已经有比较详尽的阐述,这里就不赘述了。

     

    我想强调的是,语言是明晰概念的基础,也是抽象思维的基础,在构建一个系统时,值得我们花很多时间去斟酌、去推敲语言。在我做过的一个项目中,就曾为一个关键实体讨论了两天,因为那是一个新概念,尝试了很多名字,始终感觉到别扭、不好理解。随着我们讨论的深入,对问题域理解的深入,我们最终找到了一个相对比较合适的名字,才肯罢休。

     

    这样的斟酌是有意义的,因为明晰关键概念,是我们设计中的重要工作。虽然不合理的命名、不合理的抽象也能实现业务功能。但其代价就是维护系统时需要极高的认知负荷。随着时间的推移,就没人能搞懂系统的设计了。

     

    抽象的层次性

     

    回到毕加索的抽象画,如下图所示,如果映射到面向对象编程,抽象牛就是抽象类(Abstract Class),代表了所有牛的抽象。抽象牛可以泛化成更多的牛,比如水牛、奶牛、牦牛等。每一种牛都代表了一类(Class)牛,对于每一类牛,我们可以通过实例化,得到一头具体的牛实例(Instance)。

     

    image.png

     

    从这个简单的案例中,我们可以到抽象的三个特点:

     

    1. 抽象是忽略细节的。抽象类是最抽象的,忽略的细节也最多,就像抽象牛,只是几根线条而已。在代码中,这种抽象可以是 Abstract Class,也可以是 Interface。

     

    2. 抽象代表了共同性质。类(Class)代表了一组实例(Instance)的共同性质,抽象类(Abstract Class)代表了一组类的共同性质。对于我们上面的案例来说,这些共同性质就是抽象牛的那几根线条。

     

    3. 抽象具有层次性。抽象层次越高,内涵越小,外延越大,也就是说它的涵义越小,泛化能力越强。比如,牛就要比水牛更抽象,因为它可以表达所有的牛,水牛只是牛的一个种类(Class)。

     

    抽象的这种层次性,是除了抽象概念之外,另一个我们必须要深入理解的概念,因为小到一个方法要怎么写,大到 一个系统要如何架构,以及我们后面第三章要介绍的结构化思维,都离不开抽象层次的概念。

     

    在进一步介绍抽象层次之前,我们先来理解一下外延和内涵的意思:

     

    抽象是以概念(词语)来反映现实的过程,每一个概念都有一定的外延和内涵。概念的外延就是适合这个概念的一切对象的范围,而概念的内涵就是这个概念所反映的对象的本质属性的总和。例如“平行四边形”这个概念,它的外延包含着一切正方形、菱形、矩形以及一般的平行四边形,而它的内涵包含着一切平行四边形所共有的“有四条边,两组对边互相平行”这两个本质属性。

     

    一个概念的内涵愈广,则其外延愈狭;反之,内涵愈狭,则其外延愈广。例如,“平行四边形”的内涵是“有四条边,两组对边互相平行”,而“菱形”的内涵除了这两条本质属性外,还包含着“四边相等”这一本质属性。“菱形”的内涵比“平行四边形”的内涵广,而“菱形”的外延要比“平行四边形”的外延狭。

     

    所谓的抽象层次就体现在概念的外延和内涵上,这种层次性,基本可以体现在任何事物上,比如一份报纸就存在多个层次上的抽象,“出版品”最抽象,其内涵最小,但外延最大,“出版品”可以是报纸也可以是期刊杂志等。

     

    1. 一个出版品
    2. 一份报纸
    3. 《旧金山纪事报》
    4. 5 月 18 日的《旧金山纪事报》

     

    当我要统计美国有多少个出版品,那么就要用到最上面第一层“出版品”的抽象,如果我要查询旧金山 5月18日当天的新闻,就要用到最下面第四层的抽象。

     

    每一个抽象层次都有它的用途,对于我们工程师来说,如何拿捏这个抽象层次是对我们设计能力的考验,抽象层次太高和太低都不行。

     

    比如,现在要写一个水果程序,我们需要对水果进行抽象,因为水果里面有红色的苹果,我们当然可以建一个 RedApple 的类,但是这个抽象层次有点低,只能用来表达“红色的苹果”。来一个绿色的苹果,你还得新建一个 GreenApple 类。

     

    为了提升抽象层次,我们可以把 RedApple 类改成 Apple 类,让颜色变成 Apple 的属性,这样红色和绿色的苹果就都能表达了。再继续往上抽象,我们还可以得到水果类、植物类等。再往上抽象就是生物、物质了。

     

    你可以看到,抽象层次越高,内涵越小,外延越大,泛化能力越强。然而,其代价就是业务语义表达能力越弱。

     

    image.png

     

    具体要抽象到哪个层次,要视具体的情况而定了,比如这个程序是专门研究苹果的可能到 Apple 就够了,如果是卖水果的可能需要到 Fruit,如果是植物研究的可能要到 Plant,但很少需要到 Object。

     

    我经常开玩笑说,你把所有的类都叫 Object,把所有的参数都叫 Map 的系统最通用,因为 Object 和 Map 的内涵最小,其延展性最强,可以适配所有的扩展。从原理上来说,这种抽象也是对的,万物皆对象嘛。但是这种抽象又有什么意义呢?它没有表达出任何想表达的东西,只是一句正确的废话而已。

     

    越抽象,越通用,可扩展性越强,然而其语义的表达能力越弱。越具体,越不好延展,然而其语义表达能力很强。所以,对于抽象层次的权衡,是我们系统设计的关键所在,也是区分普通程序员和优秀程序员的关键所在。

     

    软件中的分层抽象无处不在

     

    越是复杂的问题越需要分层抽象,分层是分而治之,抽象是问题域的合理划分和概念语义的表达。不同层次提供不同的抽象,下层对上层隐藏实现细节,通过这种层次结构,我们才有可能应对像网络通信、云计算等超级复杂的问题。

     

    网络通信是互联网最重要的基础实施,但同时它又是一个很复杂的过程,你要知道把数据包传给谁——IP协议,你要知道在这个不可靠的网络上出现状况要怎么办——TCP 协议。有这么多的事情需要处理,我们可不可以在一个层次中都做掉呢?当然是可以的,但显然不科学。因此,ISO制定了网络通信的七层参考模型,每一层只处理一件事情,低层为上层提供服务,直到应用层把HTTP、FTP等方便理解和使用的协议暴露给用户。

     

    image.png

     

    编程语言的发展史也是一个典型的分层抽象的演化史。

     

    机器能理解的只有机器语言,即各种二进制的 01 指令。如果我们采用 01 的输入方式,其编程效率极低(学过数字电路的同学,体会下用开关实现加减法)。所以我们用汇编语言抽象了二进制指令。

     

    然而汇编还是很底层,于是我们用 C 语言抽象了汇编语言。而高级语言 Java 是类似于 C 这样低级语言的进一步抽象,这种逐层抽象极大的提升了我们的编程效率。

     

    image.png

     

    重复代码是抽象的缺失

     

    如果说抽象的本质是共性的话,那么我们代码中的重复代码,是不是就意味着抽象的缺失呢?

    //取默认搜索条件
    List<String> defaultConditions = searchConditionCacheTunnel.getJsonQueryByLabelKey(labelKey);
    for(String jsonQuery : defaultConditions){
      jsonQuery = jsonQuery.replaceAll(SearchConstants.SEARCH_DEFAULT_PUBLICSEA_ENABLE_TIME, String.valueOf(System.currentTimeMillis() / 1000));
      jsonQueryList.add(jsonQuery);
    }
    //取主搜索框的搜索条件
    if(StringUtils.isNotEmpty(cmd.getContent())){
        List<String> jsonValues = searchConditionCacheTunnel.getJsonQueryByLabelKey(SearchConstants.ICBU_SALES_MAIN_SEARCH);
        for (String value : jsonValues) {
        String content = StringUtil.transferQuotation(cmd.getContent());
        value = StringUtil.replaceAll(value, SearchConstants.SEARCH_DEFAULT_MAIN, content);
          jsonQueryList.add(value);
      }
    }

     

    是这样的,重复代码是典型的代码坏味道,其本质问题就是抽象的缺失。因为我们 Ctrl+C 加 Ctrl+V 的工作习惯,导致没有对共性代码进行抽取;或者虽然抽取了,只是简单的用了一个 Util 名字,没有给到一个合适的名字,没有正确的反应这段代码所体现的抽象概念,都属于抽象不到位。

     

    有一次,我在 Review 团队代码的时候,发现有一段组装搜索条件的代码,在几十个地方都有重复。这个搜索条件还比较复杂,是以元数据的形式存在数据库中,因此组装的过程是这样的:

     

    • 首先,我们要从缓存中把搜索条件列表取出来;
    • 然后,遍历这些条件,将搜索的值填充进去;

     

     

    简单的重构无外乎就是把这段代码提取出来,放到一个Util类里面给大家复用。然而我认为这样的重构只是完成了工作的一半,我们只是做了简单的归类,并没有做抽象提炼。

     

    简单分析,不难发现,此处我们是缺失了两个概念:一个是用来表达搜索条件的类——SearchCondition;另一个是用来组装搜索条件的类——SearchConditionAssembler。只有配合命名,显性化的将这两个概念表达出来,才是一个完整的重构。

     

    重构后,搜索条件的组装会变成一种非常简洁的形式,几十处的复用只需要引用SearchConditionAssembler就好了。

    public class SearchConditionAssembler {
        public static SearchCondition assemble(String labelKey){
            String jsonSearchCondition =  getJsonSearchConditionFromCache(labelKey);
            SearchCondition sc = assembleSearchCondition(jsonSearchCondition);
            return sc;
        }
    }

     

    由此可见,提取重复代码只是我们重构工作的第一步。对重复代码进行概念抽象,寻找有意义的命名才是我们工作的重点。

     

    因此,每一次遇到重复代码的时候,你都应该感到兴奋,想着这是一次锻炼抽象能力的绝佳机会,当然,测试代码除外。

     

    强制类型转换是抽象层次有问题

     

    面向对象设计里面有一个著名的 SOLID 原则是由 Bob 大叔(Robert Martin)提出来的,其中的 L 代表 LSP,就是 Liskov Substitution Principle(里氏替换原则)。简单来说,里氏替换原则就是子类应该可以替换任何父类能够出现的地方,并且经过替换以后,代码还能正常工作。

     

    思考一下,我们在写代码的过程中,什么时候会用到强制类型转换呢?当然是 LSP 不能被满足的时候,也就是说子类的方法超出了父类的类型定义范围,为了能使用到子类的方法,只能使用类型强制转换将类型转成子类类型。

     

    举个例子,在苹果(Apple)类上,有一个 isSweet() 方法是用来判断水果甜不甜的;西瓜(Watermelon)类上,有一个 isJuicy() 是来判断水分是否充足的;同时,它们都共同继承一个水果(Fruit)类。

     

    此时,我们需要挑选出甜的水果和有水分的习惯,我们会写一个如下的程序:

     

    public class FruitPicker {
    ​
        public List<Fruit> pickGood(List<Fruit> fruits){
            return fruits.stream().filter(e -> check(e)).
                    collect(Collectors.toList());
        }
    ​
        private boolean check(Fruit e) {
            if(e instanceof Apple){
                if(((Apple) e).isSweet()){
                    return true;
                }
            }
            if(e instanceof Watermelon){
                if(((Watermelon) e).isJuicy()){
                    return true;
                }
            }
            return false;
        }
    }

     

    因为pick方法的入参的类型是 Fruit,所以为了获得 Apple 和 Watermelon 上的特有方法,我们不得不使用 instanceof 做一个类型判断,然后使用强制类型转换转成子类类型,以便获得他们的专有方法,很显然,这是违背了里式替换原则的。

     

    这里问题出在哪里?对于这样的代码我们要如何去优化呢?仔细分析一下,我们可以发现,根本原因是因为 isSweet 和 isJuicy 的抽象层次不够,站在更高抽象层次也就是 Fruit 的视角看,我们挑选的就是可口的水果,只是具体到苹果我们看甜度,具体到西瓜我们看水分而已。

     

    因此,解决方法就是对 isSweet 和 isJuicy 进行抽象,并提升一个层次,在 Fruit 上创建一个 isTasty() 的抽象方法,然后让苹果和西瓜类分别去实现这个抽象方法就好了。

     

    image.png

     

    下面是重构后的代码,通过抽象层次的提升我们消除了 instanceof 判断和强制类型转换,让代码重新满足了里式替换原则。抽象层次的提升使得代码重新变得优雅了。

     

    public class FruitPicker {    
    public List<Fruit> pickGood(List<Fruit> fruits){        
    return fruits.stream().filter(e -> check(e)).                collect(Collectors.toList());    }    
    //不再需要instanceof和强制类型转换    p
    rivate boolean check(Fruit e) {        
    return e.isTasty();    }}

     

    所以,每当我们在程序中准备使用 instanceof 做类型判断,或者用 cast 做强制类型转换的时候。每当我们的程序不满足 LSP 的时候。你都应该警醒一下,好家伙,这又是一次锻炼抽象能力的绝佳机会。

     

    如何提升抽象思维能力

     

    抽象思维能力是我们人类特有的、与生俱来的能力,除了上面说的在编码过程中可以锻炼抽象能力之外,我们还可以通过一些其他的练习,不断的提升我们的抽象能力。

     

    多阅读

     

    为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面的时候会将你的大脑铺满,较少需要抽象和想象。

    这也是为什么我们不提倡让小孩子过多的暴露在电视或手机屏幕前的原因,因为这样不利于他抽象思维的锻炼。

     

    抽象思维的差别让孩子们的学习成绩从初中开始分化,许多不能适应这种抽象层面训练的,就去读技校了,因为技校比大学会更加具象:车铣刨磨、零部件都能看得见摸得着。体力劳动要比脑力劳动来的简单。

    多总结沉淀

     

    小时候不理解,语文老师为什么总是要求我们总结段落大意、中心思想什么的。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维能力。

     

    记录也是很好的总结习惯。就拿读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳书中的内容,这样不仅可以加深理解,而且还可以提升自己的抽象思维能力。

     

    我从四年前开始系统的记录笔记,做总结沉淀,构建自己的知识体系。这种思维训练的好处显而易见,可以说我之前写的《从码农到工匠》和现在正在写的《程序员必备的思维能力》都离不开我总结沉淀的习惯。

    命名训练

     

    每一次的变量命名、方法命名、类命名都是一次难得的抽象思维训练机会,前面已经说过了,语言和抽象是一体的,命名的好坏直接反应了我们的问题域思考的是否清晰,反映了我们抽象的是否合理。

     

    现实情况是,我们很多的工程师常常忽略了命名的重要性,只要能实现业务功能,名字从来就不是重点。

     

    实际上,这是对系统的不负责任,也是对自己的不负责任,更是对后期维护系统的人不负责任。写程序和写文章有很大的相似性,本质上都是在用语言阐述一件事情。试想下,如果文章中用的都是些词不达意的句子,这样的文章谁能看得懂,谁又愿意去看呢。

     

    同样,我一直强调代码要显性化的表达业务语义,其中命名在这个过程中扮演了极其重要的角色。为了代码的可读性,为了系统的长期可维护性,为了我们自身抽象思维的训练,我们都不应该放过任何一个带有歧义、表达模糊、意不清的命名。

     

    领域建模训练

     

    对于技术同学,我们还有一个非常好的提升抽象能力的手段——领域建模。当我们对问题域进行分析、整理和抽象的时候,当我们对领域进行划分和建模的时候,实际上也是在锻炼我们的抽象能力。

     

    我们可以对自己工作中的问题域进行建模,当然也可以通过阅读一些优秀源码背后的模型设计来学习如何抽象、如何建模。比如,我们知道 Spring 的核心功能是 Bean 容器,那么在看Spring源码的时候,我们可以着重去看它是如何进行Bean管理的?它使用的核心抽象是什么?不难发现,Spring 是使用了 BeanDefinition、BeanFactory、BeanDefinitionRegistry、BeanDefinitionReader 等核心抽象实现了 Bean 的定义、获取和创建。抓住了这些核心抽象,我们就抓住了 Spring 设计主脉。

     

    除此之外,我们还可以进一步深入思考,它为什么要这么抽象?这样抽象的好处是什么?以及它是如何支持 XML 和 Annotation(注解)这两种关于 Bean 的定义的。

    这样的抽象思维锻炼和思考,对提升我们的抽象能力和建模能力非常重要。关于这一点,我深有感触,初入职场的时候,当我尝试对问题域进行抽象和建模的时候,会觉得无从下手,建出来的模型也感觉很别扭。

     

    然而,经过长期的、刻意的学习和锻炼之后,很明显可以感觉到我的建模能力和抽象能力都有很大的提升。不但分析问题的速度更快了,而且建出来的模型也更加优雅了。

     

    小结

     

    • 抽象思维是程序员最重要的思维能力,抽象的过程就是寻找共性、归纳总结、综合分析,提炼出相关概念的过程。

     

    • 语言和抽象是一体的,抽象思维也叫词思维,因为抽象的概念只能通过语言才能表达出来。

     

    • 抽象是有层次性的,抽象层次越高,内涵越小,外延越大,扩展性越好;反之,抽象层次越低,内涵越大,外延越小,扩展性越差,但语义表达能力越强。

     

    • 对抽象层次的拿捏,体现了我们的设计功力,视具体情况而定,抽象层次既不能太高,也不能太低。

     

    • 重复代码意味着抽象缺失,强制类型转换意味着抽象层次有问题,我们可以利用这些信号来重构代码,让代码重新变的优雅。

     

    • 我们可以通过刻意练习来提升抽象能力,这些练习包括阅读、总结、命名训练、建模训练等。

    原文链接

    本文为阿里云原创内容,未经允许不得转载。

    展开全文
  • 抽象类和接口

    千次阅读 2020-11-24 19:38:35
    抽象类 ​ 概述 ​ 父类当中的方法,被他的子类们重写,子类的各自实现又不一样。那么父类的方法声明和方法体,只有声明还有意义,而方法体内容则没有存在的意义。我们把这种没有方法体内容的方法称为抽象方法。...

    抽象类

    ​ 概述

    ​ 父类当中的方法,被他的子类们重写,子类的各自实现又不一样。那么父类的方法声明和方法体,只有声明还有意义,而方法体内容则没有存在的意义。我们把这种没有方法体内容的方法称为抽象方法。Java语言规定,如果一个类包含了抽象的方法。那么该类就是一个抽象类

    定义:

    ​ 抽象方法:没有方法体的方法

    ​ 抽象类:包含抽象方法的类

    abstract关键字的使用格式

    ​ 抽象方法

    ​ 使用abstract关键字修饰成员方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

    ​ 定义格式:

    修饰符   abstract 返回值类型  方法名(参数列表);
    

    示例代码:

    public abstract void run();
    // 吃饭的抽象方法
    public abstract void eat();
    // 跳跃的抽象方法
    public abstract void jump(); 
    
    抽象类

    ​ 如果一个类包含了抽象方法,那么该类就是一个抽象类。

    定义格式:

    修饰符 abstract class ClassName {}
    

    示例代码:

    public abstract class Animal {
        // 奔跑的抽象方法
        public abstract void run();
    }
    

    抽象的使用

    继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为一个抽象类。

    注意事项

    关于抽象类的使用,需要注意的事项:

    1. 抽象类不能创建对象,如果创建对象,编译无法通过。只能创建其非抽象子类的对象。
    2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。
    3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
    4. 抽象类的子类,必须重写父类中的所有的抽象方法,否则编译无法通过,除非该子类也是抽象类。

    接口

    概述

    ​ 接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是【封装了方法】,包含了抽象方法(JDK1.7及以前),默认方法和静态的方法(JDK1.8),私有方法(JDK1.9)

    ​ 接口的定义,它与类的定义很相似,但是使用interface关键字,他也会被编译生成class文件,但一定要明确他并不是类,而是另外一种引用数据类型。

    引用数据类型:类、数组、接口。

    接口的使用,不能直接创建对象,但是可以被实现(implements关键字,类似于被继承),一个实现接口的类(可以看做是接口的子类),需要重写接口中的所有的抽象方法,创建该类对象,就可以调用方法了。

    接口的定义格式:

    public interface 接口名称{
        // 抽象方法为主
        // 默认方法
        // 常量
        // 静态方法
        // 私有方法(JDK1.9)
    }
    

    含有抽象方法

    抽象方法:使用abstract关键字修饰,没有方法体内容,该方法主要是供子类使用的

    public interface InterfaceName{
        public abstract
    }
    

    含有默认方法和静态方法

    默认方法:使用default关键字修饰的方法,不可省略,供子类调用或者子类重写。

    静态方法:使用static关键字修饰的方法,供接口直接使用。

    含有私有方法和私有静态的方法

    私有方法:使用private关键字,供接口中的默认方法或者静态方法调用。

    代码如下:

    public interface InterfaceName{
        private void method(){
            // 方法体内容。
        }
    }
    

    基本的实现

    实现的概述

    类与接口的关系,为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为

    接口的子类,(实现的动作类似于继承,格式相仿,只是关键字不同,实现使用implements关键字)

    非抽象类实现接口:

    1.必须重写接口当中的所有的抽象方法

    2.继承了接口的当中的默认方法,既可以直接调用,也可以重写

    实现格式:

    public class 实现类 implements 接口名称{
        // 重写接口当中的所有的抽象方法
        // 重写接口当中的默认方法
    }
    

    抽象方法的使用:

    必须全部实现:

    代码如下: 定义一个接口:

    public interface Biological{// 生物
        // 定义一个吃东西的功能
        public abstract void eat();
        // 定义一个休息的功能
        public abstract void sleep();
    }
    // 定义一个实现类
    public class Animal implements Biological{
        @Override
        public void eat(){
            System.out.println("吃东西");
        }
        @Override
        public void sleep(){
            System.out.println("睡觉");
        }
    }
    
    // 定义测试类
    public class InterfaceDemo01{
        public static void main(String[] args){
            // 创建子类对象
            Animal ani = new Animal();
            // 调用重写之后的方法
            ani.eat();
            ani.sleep();
        }
    }
    // 输出结果:
    // 吃东西
    // 睡觉
    
    

    默认方法的使用

    可以继承,可以重写,二选一,但是只能通过实现类的对象来调用

    1.继承默认方法,代码如下

    public interface Biological{
      public default void fly(){
        System.out.println("天上飞");
    }
    }
    // 定义实现类
    public class Animal implements Biological{
        // 继承 什么也不写,直接调用
    }
    // 定义测试类
    public class InterfaceDemo02{
        
      public static void main(String[] args){
          // 创建子类对象
          Animal ani = new Animal();
          // 调用默认方法
          ani.fly();
      }
    }
    // 输出结果:天上飞
    

    2.重写默认方法,代码如下:

    public interface Biological{
      public default void fly(){
        System.out.println("天上飞");
    }
    }
    // 定义实现类
    public class Animal implements Biological{
     //重写
     @Override
        public void fly(){
            System.out.println("自由自在的飞");
        }
    }
    
    public class InterfaceDemo03{
        
      public static void main(String[] args){
          // 创建子类对象
          Animal ani = new Animal();
          // 调用默认方法
          ani.fly();
      }
    }
    // 输出结果:自由自在的飞
    

    静态方法的使用

    静态的一般都是和类.class文件相关,只能使用【接口名】来调用,不可以通过实现类的类名或者是实现类的对象来调用。代码如下:

    public interface Biological{
      public static void run(){
        System.out.println("跑起来。。");
    }
    }
    // 定义实现类
    public class Animal implements Biological{
     // 无法重写静态方法
    }
    
    // 定义测试类
    public class InterfaceDemo04{
      public static void main(String[] args){
          // 调用静态方法
         Biological.fly();
      }
    }
    // 输出结果:跑起来
    
    

    私有方法的使用

    私有方法: 只有默认方法可以调用

    私有静态方法: 默认方法和静态方法都可以调用

    如果一个接口中有多个默认方法,并且方法中有重复的内容,那么可以抽取出来,封装到私有方法中,

    供默认方法去调用。从设计的角度考虑,私有的方法是对默认的方法和静态的方法的一种辅助。

    // 定义一个接口
    public interface Biological{
        private void run1(){
            System.out.println("跑起来。。");
        }
        private default void funMethod01(){
            //System.out.println("跑起来。。");
            run1();
        }
        private default void funMethod02(){
            //System.out.println("跑起来。。");
            run1();
        }
    }
    

    接口的多实现

    在继承体系中,一个类只能直接继承一个父类,而对于接口来说,一个类可以实现多个接口,这叫做接

    口的【多实现】。并且,一个类能直接继承一个父类的同时还可以实现多个接口。

    实现格式:

    public class ClassName extends 父类 implements 接口名1,接口名2,...{
        //重写接口中的所有的抽象方法
        //重写接口中的默认方法(可选)
        //抽象方法重名
    }
    

    抽象方法

    接口中,有多个抽象方法,实现类必须重写所有的抽象方法,如果抽象方法名有重名的,只需要重写一

    次即可,代码如下:

    // 定义多个接口
    public interface Animal{
        public abstract void eat();
        public abstract void run();
    }
    public interface Human{
        public abstract void eat();
        public abstract void run();
    }
    // 继承实现类
    public class People implements Animal,Human{
        @Override
        public void eat(){
            System.out.println("吃东西!");
        }
        @Override
        public void run(){
            System.out.println("健身。。");
        }
    }
    

    默认方法

    接口中,有多个默认方法时,实现类都可继承使用,如果默认方法有重名的,【必须重写一次】代码如下:

    public interface A{
        public default void methodA(){}
        public default void method(){}
    }
    public interface B{
        public default void methodB(){}
        public default void method(){}
    }
    // 定义实现类
    public class C implements A,B{
        @Override
        public void method(){
            System.out.println("method方法被重写。。。");
        }
    }
    

    静态方法

    接口中,如果存在同名的静态方法并不会冲突,原因是只能通过各自的接口名访问静态方法。

    优先级的问题

    当一个类,既继承了一个父类,又同时实现类多个接口,父类中的成员方法与接口当中的默认方法

    重名,子类就近选择父类的成员方法。

    代码如下:

    public interface A{
        public default void methodA(){
            System.out.println("AAAAAAAA");
        }
    }
    // 定义父类
    public class D{
        public void methodA(){
            System.out.println("DDDDDDDD");
        }
    }
    // 定义子类
    public class E extends D implements A{
        // 未重写methodA()
    }
    // 定义测试类
    public class TestInterfaceDemo06{
        public static void main(String[] args){
            // 创建子类对象E
            E e = new E();
            e.methodA();
        }
    }
    //输出结果:DDDDDDDD
    

    接口的多继承【了解】

    一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用extends关键字,子接口继承父接口的方法,如果父接口中的默认方法有重名方法,那么子接口需要重写一次。代码如下:

    public interface A{
    public default void method(){
        System.out.println("AAAAAAAAAA");
    }
    }
    public interface B{
        public default void method(){
             System.out.println("BBBBBBBBBB");
        }
    }
    // 定义子接口
    public interface C extends A,B{
        @Override
        public default void method(){
            System.out.println("CCCCCCCCCC");
        }
    }
    

    备注:子接口重写默认方法,default保留。

    其他成员特点:

    接口中,无法定义成员变量,但是可以定义常量,因为值不可变,默认使用public static final 修饰的

    接口中,没有构造方法,不能创建对象

    接口当中,没有静态代码块

    接口的好处:

    设定了规则

    降低耦合性【高内聚,低耦合】

    扩展原有类的功能

    接口与抽象类的区别:

    相同点:

    • 都包含抽象方法,其子类都必须重写这些抽象方法
    • 都不能直接实例化对象
    • 都位于继承的顶端,用于被其他类实现或者继承

    区别 :

    • 抽象类里面可以包含普通成员方法,接口不能包含普通成员方法
    • 一个类只能直接继承一个父类(可以是抽象类),一个类也可以实现多个接口
    • 类与类之间只能时单继承关系,接口与接口之间可以多继承
    • 抽象类可以定义普通的成员变量和常量,接口只能定义常量 public static final 修饰的
    展开全文
  • 目录1. 如何调用抽象类中的抽象方法?2. 如何调用接口中的抽象方法? 1. 如何调用抽象类中的抽象方法? 2. 如何调用接口中的抽象方法?
  • AbstractKnowledgeGraph AbstractKnowledgeGraph, a systematic knowledge graph that concentrate on abstract thing including abstract entity and action. 抽象知识图谱,目前规模50...目标于抽象知识,包括抽象...
  • java抽象类和抽象方法

    千次阅读 2021-02-27 12:26:26
    一、什么是抽象类和抽象方法没有具体功能的实现,通过子类继承后重写方法来实现一定约束的类称之为抽象类,抽象类必须被abstract修饰。抽象方法就是只有方法声明,没有方法体的方法。抽象类的使用一般通过继承来实现...
  • 【Java语法】包 继承 多态 抽象类 接口

    千次阅读 多人点赞 2022-03-20 14:17:40
    这个普通类需要重写这个抽象类的所有抽象方法 6,抽象类最大作用就是为了被继承 7,一个抽象类A继承抽象类B,A可以不实现抽象父类B的抽象方法 8,抽象类不能被final修饰,抽象方法也不能被final修饰 9,针对7,普通...
  • C++设计模式:抽象工厂模式

    千次阅读 多人点赞 2021-12-11 20:20:47
    抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。
  • Java----多态+抽象类+抽象方法+接口

    千次阅读 2021-02-07 17:31:56
    Java基础创作日记 Day05 时间:2021年2月7日下午 阴 昨天的前天的总结少了点东西,今天...含有抽象方法的类必须要用abstract修饰,用abstract修饰的类叫做抽象类,抽象类可以不含有抽象方法 语法:abstract class 类名.
  • Java抽象类 详解

    万次阅读 多人点赞 2016-10-05 00:06:35
    一、抽象类的基本概念普通类是一个完善的功能类,可以直接产生实例化对象,并且在普通类中可以包含有构造方法、普通方法、static方法、常量和变量等内容。而抽象类是指在普通类的结构里面增加抽象方法的组成部分。...
  • C#中的抽象类、抽象属性、抽象方法

    千次阅读 2020-06-10 00:51:31
    前言:本博文讲述了C#面向对象中的抽象类,通过本博文的阅读,读者可以掌握C#抽象类的概念、应用、好处以及一些特性。 文章目录一、抽象类概念二、示例讲解三、抽象类的作用四、使用抽象类的好处五、抽象类总结 一、...
  • 抽象类总结 : ① 声明 : 抽象类中使用 abstract 声明 ; ② 成员 : 抽象类中既可以定义正常属性和方法 , 又可以定义抽象的属性和方法 ; ③ 继承 : 抽象类可以继承抽象类 , 抽象类也可以继承正常类 , 正常类可以...
  • 编写一个抽象类Shape

    2012-12-23 14:23:29
    1) 编写一个抽象类Shape,其中有抽象方法getArea()和getPerimeter() 2) 在Shape类的基础上派生出Rectangle和Circle类,二者都实现了计算面积的方法getArea()和计算周长的方法getPerimeter(); 3) 构造main...
  • Java-抽象

    千次阅读 2021-02-26 09:29:41
    抽象类含有抽象方法的类就是抽象类1) 抽象方法:该方法有返回值,有参数,就是没有方法体,该方法没有方法实现2) 抽象方法的格式:abstract 返回值类型 方法名(参数列表);3) 抽象类需要靠子类来重写抽象方法,以...
  • JAVA 抽象

    千次阅读 2021-03-01 08:03:17
    一、抽象类的概念C extends B, B extends A 在继承过程中,形成了一个继承金字塔。位于金字塔底部的类越来越具体(强大),位于金字塔顶部的类越来越抽象(简单)。例如:人类继承于(属于)动物类,鱼类继承于(属于)动物...
  • 抽象类是不是必须要有抽象方法

    千次阅读 2021-08-15 23:57:30
    在回答这个问题之前,先来了解一下抽象类。 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类...
  • 抽象工厂模式的详解

    千次阅读 2020-11-06 00:47:40
    1.为什么需要抽象工厂模式? 虽然工厂方法模式引入工厂等级结构,解决了简单工厂模式中工厂类职责过重的问题,但由于工厂方法模式中每个工厂只创建一类具体类的对象,如果需要的具体类很多时候,这将会导致系统当中...
  • 抽象类和抽象方法的使用

    千次阅读 2022-02-08 15:50:04
    有时候将一个类设计的非常抽象,以至于他没有具体的实例,这样的类叫做抽象类。 abstract关键字的使用 1、abstract可以用来修饰结构、类、方法 2、一旦父类抽象了,就不可以再实例化造对象 ps:父类还是应该提供...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,817,270
精华内容 726,908
关键字:

抽象