面向对象编程

Posted ayanwan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象编程相关的知识,希望对你有一定的参考价值。

一、面向对象 vs 面向过程

      实际上,面向对象的技术是在面向过程的基础上,为程序设计提供了更多的手段和方法。两种技术不是非此既彼的对立关系,不能说面向对象的技术替代了面向过程,如果用面向对象的技术就要把一个软件任务划分成两个层次,第一个层次是做什么,第二层次是怎么做。那么面向对象技术是解决第一个层次的问题,面向过程则是解决第二个层次的问题,只有在两个层次上的任务都完成之后,才能说完成了整个程序设计任务。
     也就是说这两种程序设计方法只是从不同的层次来完成程序设计的任务。面向对象的技术是在面向过程的基础之上,又提供了抽象、封装、继承、多态技术手段,从而能够设计出更好更复杂的软件。可以说面向对象技术中存在着面向过程,或者说面向对象技术就是抽象、封装、继承、多态加上顺序、条件、循环三种控制结构的使用。

1.1 面向过程:3

      面向过程有“三”种控制结构:顺序、条件、循环。

1.2 面向对象:4+3

       面向对象可以认为又提供“四”种技术:抽象、封装、继承、多态;
       用面向对象技术设计类后,一个类中就会有若干方法,每个方法完成一个特定的功能在考虑类的行为功能实现时,显然是面向过程的思维方式,只是这些方法不再独立,而是与数据一起形成了一个有机的整体。面向过程技术存在于也就是类方法实现之中。

       面向对象的设计包含了面向过程,面向对象比面向过程站到了一个更高的层次上,主要进行总体结构模型设计(构建类),对数据和方法进行封装,面向过程主要是写函数或过程也就是确定类中的方法的实现,这也是类中的方法必须要做的步骤。所以说用面向对象的技术设计软件时包含了面向过程。

      【类的关系】

      泛化(继承)

      实现   

      依赖use

      关联and

      聚合has

      组合contain    

1.3 面向对象与面向过程的同一性

       众所周知,无论是面向对象还是面向过程在描述问题时总是围绕着两个方面:方法和数据。面向对象是方法和数据合并或封装,面向过程是方法和数据分开或隔离。
       由于面向对象实现了数据和方法的封装,它的好处就是使得类中方法的输入数据多了一个来源,也就是输入数据有了两个来源,一个是类中的属性变量,另一个是方法的形式参
数。也就是说当类中的属性能够满足类中的方法输入数据的需要,就可以无参数。虽然无参数,但具备有参数的功能,原因是类中的属性是可以随时变化的。这归功于面向对象的封装技术。当然,也可使用方法本身的数据----形式参数,这就形成了同一方法的两种不同的实现,面向对象技术中的多态性,使这两种方法能够共存于一个类中,增加了类的可适应性和简洁性。
       如果用面向过程的技术,则只能设计出一个独立的过程或函数,因为面向过程的程序和数据是分开的,每个过程也都是独立的,所以过程或函数在设计时没有别的数据来源只有依赖于参数,无法从别处获取数据。
       同理,类中方法处理的数据结果也变成了有两个去处,一个是类中的属性变量,一个是方法的返回值。
       这就是数据和方法封装在一起的好处,它使得输入输出有了更多的选择,这也是设计方法时解决算法之后要解决的重要问题。

1.4 面对对象六大原则

1.4.1 开闭原则(Open Close Principle)

       开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。
简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要做好抽象,并使用接口和抽象类。

1.4.2 里氏代换原则(Liskov Substitution Principle)

       里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。
里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

1.4.3 依赖倒转原则(Dependence Inversion Principle)

       这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象,而不依赖于具体。

1.4.4 接口隔离原则(Interface Segregation Principle)

       这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

1.4.5 迪米特法则(Demeter Principle)

       又称为最少知道原则,它是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

1.4.6 合成复用原则(Composite Reuse Principle)

       合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。

1.5 小结

       面向对象的继承技术,使得类中面向过程的方法有了更多的功能。就单纯的面向过程的设计方法,没有实现封装、多态、继承等技术做支撑,也就是说缺乏一个可靠的围墙来做保护,使之不能在围墙之内随着问题的变化而变化,所以软件的可维护性、重用性、复杂度具有劣势。而面向对象的技术中也包含了面向过程的设计,面向对象的封装、多态和继承技术又面向过程的方法有更多的实现方式和更多的功能的扩充。
面向对象类中的方法的实现可以等同于在封装、多态、继承技术下的结构化程序设计中的函数设计,它使得类中的面向过程的方法设计有更多的实现,具有同一性。只是这些函数都有自己的归属———某一个类。


二、设计模式

       计算机科学中对设计模式的简单定义就是对于一类重复出现的问题的一种可重用的解决方案,在软件工程中一个设计模式也许能解决一类软件设计问题。

2.1 四要素

→模式名称:是一个助记名,它用一两个词来描述模式的问题、解决方案和效果。
→问题:描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果, 它可能描述特定的设计问题,如怎样用对象表示算法,也可能描述了导致不灵活设计的类或对象结构,有时侯,问题部分还会包括使用模式必须满足的一系列先决条件。
→解决方案: 描述了一个设计的各个组成成分、结构,以及它们之间的相互关系及各自的职责和协作方式。
→效果: 描述了模式使用的效果及使用模式应注意的问题。

2.2分类

       根据模式是用来完成什么工作的不同,可将设计模式分为创建型模式、结构型模式、行为型模式三种。根据模式是用于类还是用于对象,可将其分为类模式和对象模式。
2.2.1 创建型设计模式
       创建型模式与对象的创建有关,即描述怎样创建一个对象,它隐藏对象创建的具体细节,使程序代码不依赖具体的对象。因此当我们增加一个新对象时几乎不需要修改代码即可。
       创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。
       创建型类模式有Factory Method(工厂方法)模式,创建型对象模式包括Abstract Factory(抽象工厂)、Builder(生成器)、Prototype(原型)、Singleton(单件)四种模式。
2.2.2 结构型设计模式
        结构型模式处理类或对象的组合,即描述类和对象之间怎样组织起来形成大的结构,从而实现新的功能。
(1)结构型类模式采用继承机制来组合类,如Adapter (适配器类)模式;
(2)结构型对象模式则描述了对象的组装方式,如 Adapter(适配器对象)模式、Bridge(桥接)模式、Composite(组合)模式、Decorator(装饰)模式、Facade(外观)模式、Flyweight (享元)模式、Proxy(代理)模式。
2.2.3 行为型设计模式
        行为型设计模式描述算法以及对象之间的任务(职责)分配,它所描述的不仅仅是类或对象的设计模式,还有它们之间的通讯模式。
        这些模式刻划了在运行时刻难以跟踪的复杂的控制流。
        行为型类模式使用继承机制在类间分派行为,如Template Method(模板方法)模式和Interpreter(解释器)模式;
        行为型对象模式使用对象复合而不是继承,它描述一组对象怎样协作完成单个对象所无法完成的任务,如Chain of Responsibility(职责链)模式、Command(命令)模式、Iterator (迭代器)模式、Mediator (中介者)模式、Memento (备忘录)模式、Observer(观察者)模式、State(状态)模式、Strategy (策略)模式、Visitor(访问者)模式。


三、组件设计原则

       传统的面向对象编程,关注的是类的设计原则,例如SOLID、CARP、LoD等,这些主要关注类的设计,而Robert C. Martin在《敏捷软件开发:原则、模式与实践》一书中,提出了一些组件(或包)的设计原则。
      下面要介绍的这些原则主要用于设计组件和包的结构,一共包括六个原则:前面三个关注组件的内聚性(Cohesion),用于指导我们如何将类组包;后面三个关注组件的耦合性(Coupling),帮助我们确定组件之间的相互关系。
       简单来说,组件(或包)的设计也要做到“高内聚,低耦合”。当然,原则一般描述的是一种理想状况,在实际使用时,更多的是尽量趋向于这些原则,通常很难做到百分之百满足这些原则,
      包的设计最好遵循包设计六大原则。
(1)前三增加内聚:重用发布等价原则;共同重用原则;共同封闭原则。
(2)后三增加解耦:无环依赖原则;稳定依赖原则;稳定抽象原则(ADP/SDP/SAP)。

3.1 重用发布等价原则

       重用-发布等价原则(The Reuse/Release Equivalence Principle, REP)。REP要求我们从重用的角度去考虑一个组件的内容,一个组件中的类要么都可以重用,要么都不是可重用的。
       REP与面向对象设计原则中的SRP(单一职责原则)类似,在一个组件中不应该包含太多不同类型的类,不要把一些完全不相干的类放在一个组件中,这样会导致组件的职责过重,增加修改和发布的频率。因此,我们应该让一个组件中的所有类对于同一类用户或者面向同一场景是可以重用的,不应该让组件中的一部分类对用户而言有用而其他类不适用。

3.2 共同重用原则

       共同重用原则(The Common Reuse Principle, CRP)。CRP告诉我们需要将哪些类放在同一个组件中,在略为复杂一点的系统中,类很少会孤立的重用。例如,有时候需要将一个具体类和它的抽象层一起重用,需要将一个聚合类和它的迭代器一起重用,需要将工厂类和产品类一起重用,此时,最好将它们设计在同一个组件中。如果将它们分离开,放在两个不同的组件里,势必会在一些组件之间增加依赖关系,被依赖方的组件发生修改和重新发布时,依赖方的组件也需要重新验证和发布,导致维护和升级工作量增加。
需要注意的是,如果一个组件中类太多,只要该组件中一个类发生改变重新发布时,所有依赖这个组件的客户类都应该进行测试验证,看是否引入bug,即使发生修改的类与大部分客户类都没有任何关系,这样一来,导致测试工作量也会有所增加,需要进行大量不必要的重新验证和重新发行,费时费力。
       CRP更多是告诉我们没有紧密联系的类不应该放在一个组件中,在一个组件中应该只包含那些需要一起被重用的类。

3.3 共同封闭原则

       共同封闭原则(The Common Closure Principle, CCP),即:一个变化若对一个组件产生影响,则将影响该组件中所有的类,而对其他组件不造成影响。如果一个应用中的代码需要发生修改,尽量让这种修改都集中在一个组件中,而不是分散在多个组件中。

3.4 无环依赖原则

      无环依赖原则(The Acyclic Dependencies Principle, ADP),就是说在组件与组件之间不应该存在环形依赖关系。

3.5 稳定依赖原则

       稳定依赖原则(The Stable-Dependencies Principle, SDP),指的是:包应该朝着稳定的方向进行依赖。比如我们有3个包A、B、C,其中A依赖于B,B依赖于C,SDP原则要求包的稳定性关系是A<=B<=C。也就是说越高层的包越不稳定,越底层的包越稳定。
       在软件中,组件的稳定性是指改变它的难易程度。要想提高一个组件的稳定性,使得它难以修改,一个最常用的方法是让更多其它的组件依赖它。依赖一个组件的其他组件越多,它的修改所造成的影响也就越大,修改所带来的工作量也越大,我们认为它就越稳定。
【稳定性度量】
Ca:输入耦合(Afferent Coupling),即有多少包调用了它,用于衡量pacakge的职责。该数值越大,也就越稳定。
Ce:输出耦合(Effernet Coupling),即它调用了多少其他包,用于衡量package的独立性。 该数值越大,说明该包越不独立,也越不稳定。
I:不稳定性因子(Instability),其中I = Ce / (Ca + Ce),该度量的取值范围是[0, 1]。I=0表示该包具有最大的稳定性,I=1表示该包具有最大的不稳定性。
简单来说,SDP要求我们将I值小的组件放在底层,将I值大的组件放在顶层,因为I值越小意味着依赖它的外部组件越多,它的改变所带来的影响越大,越底层的模块应该越稳定,因此,不稳定因子I应该越小。沿着依赖链,I值应该逐步减小。

3.6 稳定抽象原则

       稳定抽象原则(The Stable-Abstractions Principle, SAP),该原则规定:一个稳定的组件应该是抽象的,这样在具备稳定性的同时也将具备较好的可扩展性;相应的,一个不稳定的组件应该是具体的,它的不稳定性要求其内部的具体代码应该是易于修改的,这种修改不会给其他组件带来太大的影响。
在一个相对稳定的组件中,应该包含一些抽象类,这样就可以对它进行扩展。
       SAP和SDP一起构成了组件的DIP(依赖倒转原则)。SDP要求依赖应该朝着稳定的方向进行,而SAP则规定稳定性意味着抽象性,组件的稳定程度要与其抽象程度一致。因此,依赖也应该朝着抽象的方向进行。根据DIP,为了降低类与类之间的耦合度,我们要针对接口编程,而不要针对实现编程,同理,为降低组件之间的耦合度,我们要针对抽象组件编程,依赖应该沿着抽象而又稳定的组件来进行。
【抽象性度量】
       由于在一个组件中可以包含多个类,其中有些类是抽象的,而另一些类是具体的,因此定义指标Na,Nc,A。
Na:包中抽象类的数目,对应java中的是抽象类和接口。
Nc:包中类的数目。
A:抽象性因子,A = Na/Nc,A的取值范围是[0,1],A=0意味着包没有任何抽象类,A=1意味着包中只包含抽象类。
【主序列分析】
       为了更好地研究组件的稳定性和抽象性,我们可以引入了一个二维坐标图,其中抽象性A作为纵轴,不稳定性I作为横轴,纵轴和横轴的坐标刻度都是从0到1,最稳定、最抽象的组件位于坐标左上角(0,1)处,也就是说当不稳定性I=0,抽象性A=1时,组件是最稳定、最抽象的;而那些最不稳定、最具体的组件位于坐标的右下角(1,0)处,此时不稳定性I=1而抽象性A=0。理想情况下,我们希望组件都能够落在这两个位置附近,也就是说,组件要么是最稳定最抽象的,要么是最不稳定最具体的,但是这毕竟只是理想情况,绝大部分组件的抽象性和稳定性都具有一定程度,位于这两个点之间。
       在(0,0)附近的组件,是一些具有高度稳定性且具体的组件,但是这种组件僵化程度很高,因为它是具体的,无法对其进行扩展,又因为它是高度稳定的,因此很难对它进行更改。简单来说,位于此处的组件,它会被很多其他组件所依赖(稳定性),但是它很难扩展和修改(具体性)。(0,0)附近的区域被称为痛苦地带(Zone of Pain)。
在(1,1)附近的组件,也不是一个好的位置,此处的组件虽然具有最低的稳定性和最高的抽象性,但是由于其稳定性低,因此没有其他组件依赖它们,即使具有很高的抽象性也不能得到使用。所以这个(1,1)附近的区域被称为无用地带(Zone of Uselessness)。
       我们希望所设计的组件能够尽量远离这两个区域,而将距离这两个区域都最远的轨迹点连接成一条线,这条线就是连接(1,0)和(0,1)点的线,这条线称为主序列(Main Sequence)。位于主序列上或者靠近主序列的组件都具有一定程度的稳定性和抽象性,虽然组件的理想位置是主序列的两个端点,但是在实际项目中,大部分组件能够位于主序列上或者主序列附近就已经很不错了。

       为了更好地度量一个系统的组件设计是否足够好,还有一个度量:到主序列的距离。

四、重构

       重构(Refactoring):在不改变软件的功能和外部可见性的情况下,为了改善软件的结构,提高清晰性、可扩展性和可重用性而对软件进行的改造,对代码内部的结构进行优化。

4.1 为何重构

  1)改进软件设计(整理代码)
      重构和设计是相辅相成的,它和设计彼此互补。有了重构,你仍然必须做预先的设计,但是不必是最优的设计,只需要一个合理的解决方案就够了,如果没有重构、程序设计会逐渐腐败变质,愈来愈像断线的风筝,脱缰的野马无法控制。重构其实就是整理代码,让所有带着发散倾向的代码回归本位。
   2)提高代码质量和可读性,使软件系统更易理解和维护
       "任何一个傻瓜都能写出计算机可以理解的程序,只有写出人类容易理解的程序才是优秀的程序员"。有些程序员总是能够快速编写出可运行的代码,但代码中晦涩的命名使人晕眩得需要紧握坐椅扶手,试想一个新兵到来接手这样的代码他会不会想当逃兵呢?
       软件的生命周期往往需要多批程序员来维护,我们往往忽略了这些后来人。为了使代码容易被他人理解,需要在实现软件功能时做许多额外的事件,如清晰的排版布局,简明扼要的注释,其中命名也是一个重要的方面。一个很好的办法就是采用暗喻命名,即以对象实现的功能的依据,用形象化或拟人化的手法进行命名,一个很好的态度就是将每个代码元素像新生儿一样命名,也许笔者有点命名偏执狂的倾向,如能荣此雅号,将深以此为幸。
       对于那些让人充满迷茫感甚至误导性的命名,需要果决地、大刀阔斧地整容,永远不要手下留情!
  3)帮助尽早的发现错误(Defects)
       孔子说过:温故而知新。重构代码时逼迫你加深理解原先所写的代码。程序员经常对自己的程序逻辑不甚理解的情景,曾为此惊悚过,后来发现这种症状居然是许多程序员常患的"感冒"。当你也发生这样的情形时,通过重构代码可以加深对原设计的理解,发现其中的问题和隐患,构建出更好的代码。
  4)提高编程速度
       良好设计是维持软件开发速度的根本,重构可以帮助你更快的开发软件。因为它防止系统腐败变质。甚至还可以提高设计质量。当你发现解决一个问题变得异常复杂时,往往不是问题本身造成的,而是你用错了方法,拙劣的设计往往导致臃肿的编码。
       改善设计、提高可读性、减少缺陷都是为了稳住阵脚。良好的设计是成功的一半,停下来通过重构改进设计,或许会在当前减缓速度,但它带来的后发优势却是不可低估的。

4.2 何时重构

    1)重构应该是随时随地进行。不应该为重构而重构。
    2)三次法则:第一次做某件事只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做第三次,再做类似的事情,就应该重构了。
    3)添加功能
    4)修复bug
    5)复审代码,即Code Review时候
       重构可能会引入更多见阶层,重构往往需要把大型对象拆成多个小型对象。把大型函数拆成多个小型函数。间接层是把双刃剑:一是你需要管理多分内容,但间接层有以下作用:
    1)允许逻辑共享,小函数复用性高。
    2)分开解释意图和实现:可以选择类名和函数名解释实现意图的做法。
    3)隔离变化
    4)封装条件逻辑:对象有一种奇妙的机制:多态消息,可以灵活而清晰地表达条件逻辑。将条件逻辑转化为消息形式,往往能降低代码的重复。增加清晰度并提高弹性。

4.3 何时不该重构

    1)代码是在太混乱了,设计完全错误。
    2)如果项目已近最后期限,应该避免重构。  
    3)重构还不如重新编码。即重构的工作量显著的影响Estimate 

4.4 重构流程

   1)读懂代码(包括测试例子代码)
   2)进行重构
   3)运行所有的Unit Tests 

4.5 重构与设计

    1)很多人都把设计看作软件开发的关键。而把编程看作只是机械式的低级劳动。
    2)另外的观点就是:重构可以取代预先设计。你不必要做任何设计,只管按照最初的想法开始编码,让代码运作,然后再将它重构成型。
       实际上重构与设计是互补的,程序应该是先设计,而在开始编码后,设计上的不足可以用重构来弥补。设计应该是适度的设计,而不必过度的设计。如果能很容易的通过重构来适应需求的变化,那么就不必过度的设计,当需求改变时再重构代码。

4.6 重构与性能

       三种快速编写软件的方法:
1)时间预算法
      在设计时就对程序花费的时间进行预算,通常用于性能要求极高的实时系统。普通的企业应用程序一般对性能要求不高。只要不太慢就可以了。
2)持续关注法
      要求程序员在任何时间都要设法保持系统的高性能。这个方法有个缺陷,就是大部分的程序90%的优化工作都是白费劲,这样会浪费大量的时间。
3) 良好的分解方式
      这个方式是在开发程序阶段不对性能投以任何关注,直到进入性能优化阶段,再分析程序中性能差的程序,然后对这些程序进分解,查出性能差的程序,进行优化。

五、代码质量

        过多地把精力放在ABCD(需求文档/功能设计/架构设计/理解原理)上,写代码只是把想法翻译成编程语言而已,是一个没什么技术含量的事情。代码质量是软件产品质量的一部分,我们从软件产品质量说起:

5.1 软件产品质量

        软件产品质量通常可以从以下六个方面去衡量(定义) :
·功能性(Functionality),即软件是否满足了客户业务要求;
·可用性(Usability),即衡量用户使用软件需要付出多大的努力;
·可靠性(Reliability),即软件是否能够一直处在一个稳定的状态上满足可用性;
·高效性(Efficiency),即衡量软件正常运行需要耗费多少物理资源;
·可维护性(Maintainability),即衡量对已经完成的软件进行调整需要多大的努力;
·可移植性(Portability),即衡量软件是否能够方便地部署到不同的运行环境中;
       由此可见,软件产品的质量有其明显的特殊性。而目前提高软件产品质量的主要方法是软件过程质量控制。

5.2 代码质量

       围绕软件质量的可度量特性,代码质量的关注点主要有:
(1)可读性:代码是否可读易读,对于一个团队来说,编码标准是否一致,编码风格是否一致;
(2)功能性:代码正确得实现了业务逻辑;
(3)可维护性:代码逻辑是有层次的,是容易修改的;
(4)高效性:代码实现在时间和空间的使用上是高效的; 
       因此,代码质量所涉及的5个方面,这5方面很大程序上决定了一份代码的质量高低。下面,分别来看一下这5方面:
1、编码标准:这个想必都很清楚,每个公司几乎都有一份编码规范,类命名、包命名、代码风格之类的东西都属于其中。
2、代码重复:顾名思义就是重复的代码,如果你的代码中有大量的重复代码,你就要考虑是否将重复的代码提取出来,封装成一个公共的方法或者组件。
3、代码覆盖率:测试代码能运行到的代码比率,你的代码经过了单元测试了吗?是不是每个方法都进行了测试,代码覆盖率是多少?这关系到你的代码的功能性和稳定性。
4、依赖项分析:你的代码依赖关系怎么样?耦合关系怎么样?是否有循环依赖?是否符合高内聚低耦合的原则?通过依赖项分析可以辨别一二。
5、复杂度分析:以前有人写的程序嵌套了10层 if else你信吗?圈复杂度之高,让人难以阅读。通过复杂度分析可以揪出这些代码,要相信越优秀的代码,越容易读懂。
       上面解释了代码质量相关的5个方面,在实际开发环境中,已经有很多工具为我们解决以上5个方面的问题,下列5个eclipse插件分别对这5个问题有很好的支持:
(1)编码标准:CheckStyle  插件URL:http://eclipse-cs.sourceforge.net/update/
(2)代码重复:PMD的CPD  插件URL:http://pmd.sourceforge.net/eclipse/
(3)代码覆盖率:Eclemma 插件URL:http://update.eclemma.org
(4)依赖项分析:JDepend 插件URL:http://andrei.gmxhome.de/eclipse/
(5)复杂度分析:Eclipse Metric  插件URL:http://metrics.sourceforge.net/update/

以上是关于面向对象编程的主要内容,如果未能解决你的问题,请参考以下文章

十面向对象编程

面向对象设计的三个原则

面向对象设计原则

python抽象篇:面向对象

抽象数据类型(ADT)和面向对象编程(OOP)3.4 面向对象的编程

Java 面向对象编程 抽象类 抽象方法 abstract