类的多态 (Polymorphism) 面向对象程序设计的最核心特征:
定义 : 接口的多种不同的实现方式为多态。 为类在面向程序设计中的一个重要特性。如果一种语言只是支持类和非支持多态性的话只能说此语言是基于对象的而非面向对象的。
可以理解为允许将父对象设置成一个或者更多的它的子对象的相等技术,赋值之后父对象可以根据当前赋值给它的子对象的特征以不同的方式运作。
目的 : 把不同的子类对象都看作父类对象,这样可以屏蔽不同子类对象之间的差异写出通用代码从而适应需求的不断变化。简单的说就是父类的行为像似子类。
也就是说可以使一个对象有着多重的特征,可以在特定的情况下来表现出不同的状态从而对应不同的属性和方法。
发送消息(调用类的方法)给某个对象让该对象自己决定响应何种行为(子类中的方法)。
实现方法 : 只针对基类写出程序同时可以适应这个类的家族,将子类对象地址赋值给基类,通过基类来访问其子类的对象。编译器通过动态绑定的手段来会找出合适的对象来执行操作。
运行时多态 : 通过引用变量发出的方法调用在编程的时候并不确定,而是在程序运行期间才能确定。即一个引用变量到底会指向哪个类的实例对象以及引用变量发出的方法调用到底是在哪个类中
实现的方法必须由程序运行期间才能决定。所以此多态是一种运行期的行为而非编译期的行为。
抽象类 (abstract):
定义 : 一个类中没有包含足够的信息来描绘一个具体的对象的话,这样的类就是抽象类。
分别 : 在C++中的纯虚拟函数类称为抽象类。在Java和PHP中含有抽象方法的类称为抽象类。此种类不能生成对象。同时抽象类由于是非完整的所以只能用作基类。
目的 : 用来表征对问题领域进行分析,设计中的出的抽象概念,是一系列看上出不同但是本质上相同的具体概念的抽象。(例如形状概念就是一个抽象概念)
特征 : 1 无法实例化。
2 从抽象类派生的非抽象类必须实现继承的所有抽象方法和抽象访问器。
3 可以包含抽象成员变量。
4 可以包含非抽象的成员方法。
5 意义上偏重IS-A(是一个什么东西)关系。
接口 (Interface) :
定义 : 逻辑抽象的一种方式,照比抽象类更加抽象的方式。接口属于类所具有的一种规范,定义了实现接口的类所应该遵循的标准。
目的 : 可以使类具有灵活性,可扩展性以及可插入性。同时保证实现同一接口的类遵守同一协议(抽象方法的实现)。
特征 : 接口只有方法的特征而没有方法的实现,从而接口的方法可以在不同的地方被不同的类来实现从而这些实现可以具有不同的功能。同抽象类一样,也不可以直接对接口进行实例化。
在Java中可以理解为只有抽象方法声明的抽象类。
接口中的方法都默认使public abstract类型的。
接口偏重于CAN-DO方面(实现接口的类需要实现接口的抽象方法从而达到类对接口所定义功能的实现)。
可插入性 : 在一个类的等级结构中的任何一个类都可以实现接口,这个接口会影响到此类的所有子类但是不会影响到此类的父级类(超类)。此种方式可以使大规模软件系统的灵活性,可扩展性和可插入性得到保证。
关于抽象和结构的一个个人理解分析 :
门的类设计相关问题 :
1 门在设计中,基本的门包含开关两个动作。所以定义开关这两个方法的时候需要设计一个抽象类来包含开关两个抽象定义(抽象类属于IS-A)。
2 如果有些闷具有报警功能的话,这个时候并不是所有的门具有报警功能,外加报警使属于门可以实现的一种动作(符合接口CAN-DO的定义),所以报警功能的定义应该放置到接口中。
让具有报警功能的门的类实现报警的接口,从而实现报警处理。
方法重载(Overloading Method):
定义 : 在一个类中顶一个多个同名的成员方法,但是每个同名方法具有不同的参数的类型或者参数个数。在调用重载方法的时候,编译器可以根据参数的类型或者个数选择适当的方法。
但重载与多态无关。
目的 : 可以让类以统一的方式处理不同类型数据的手段。
要求规范 : 1 方法名一定要相同。
2 参数表必须不同(参数的类型和个数)
3 参数的返回类型,修饰符可相同也可不同。[非必须项目,由于编译器是通过参数(非返回值)来判断程序是在调用哪个方法]
4 重载的方法要在同一个类中。
方法重写(Overwriting Method)
定义 : 方法重写又称为方法覆盖。子类对父类成员方法的重新改写覆盖的手段。
目的 : 子类想对父类的方法非原封不动的继承而是想做一定的修改。
要求规范 : 1 子类的方法名需要与父类的方法名同名,同返回类型以及同参数表。
2 子类的方法不能比父类中被覆盖的方法有更严格的访问权限。
3 如果子类需要使用父类的原有方法的时候,可使用super关键字来引用父类的方法。
方法隐藏 (Java):
场景 : 一个父类的引用指向子类的对象时候,这个时候要考虑到编译器的运行机制RTTI(run-time type identification 运行期类型检查),这个时候对这个实例化的对象进行访问的时候的情况。
定义 : 在子类中声明和父类相同的静态方法可以将父类的方法隐藏。
目的 : 由于不能重写父类的静态方法,所以通过隐藏的手段来隐藏父类的静态方法。但是此时便无法适用于多态的开发方式。
特征 : 1 隐藏父类的静态方法只是使父类和子类的静态方法各自独立开来。从而成为一种形式上的方法隐藏。
2 (由于RTTI机制)在使用多态的方式时,如果引用为父类引用而引用所指向的为子类的实例化对象的时候,这时如果调用子类的重名静态变量的时候,反而会去调用父类的静态方法。
3 (由于RTTI机制)在使用多态的方式时,如果引用为父类引用而引用所指向的为子类的实例化对象的时候,如果调用子类的的重名静态变量和非静态变量的时候,返而会去调用父类的静态变量和非静态变量。
4 (由于RTTI机制)在使用多态的方式时,如果引用为父类引用而引用所指向的为子类的实例化对象的时候,如果调用子类的的重名非静态方法的时候,会去调用子类的非静态方法。
总结 : 在多态的情况下,父类引用而指向子类实例的时候,静态变量和函数以及非静态变量的信息均不会通过RTTI进行多态,而非静态函数会通过RTTI来实现多态。
虚函数 (针对C++):
在基类中声明为virtual并且在一个或多个派生类中被重新定义的成员方法。主要目的是为了实现类的多态。
在基类中需要实现多态的函数加上virtual的前缀之后,子类中出现同名同参数表的函数的时候,在运用多态的情况下,就会现实子类的方法覆盖父类的方法。
作用 : 1 为面向对象编程定义了基本的规范,为面向对象编程的主要思想。
2 在软件设计阶段来从全局出发考虑来解决此阶段的问题。(比如类和代码的模块化以及重用等)
总体总结
1 不要破坏继承关系。 (里氏替换原则)
2 面向接口编程。 (依赖倒置原则)
3 设计接口的时候要精简单一。
4 降低耦合。
5 对扩展开放对修改关闭。(开闭原则)
6 实现的类要职责单一。(单一责任原则)
使用OOD的理由 :
1 软件处在时刻进化和变化中的,后期需求可能较前期来说有很大变化。
针对此,软件需要被设计成一款敏捷的软件。(可以轻松应对变化可被扩展和复用) OOD是完成此目的的关键。
2
判断一款软件是否应用了OOD的条件 :
1 面向对象
2 复用
3 变化的情况下所需的代价极小
4 无需修改代码即可扩展
SOLID 原则 (设计原则的基本):
S -- 单一责任原则
一个类或者模块应该只有一个改变的原因。也就是说不要存在多于一个导致类变更的原因。
例 : 正方形的类包含计算面积和描绘两个功能。应该将计算和描绘同时放到两个类中,以防止其中一个功能改变时影响到另一个功能。
在产生职责扩展的时候 : 应该立刻重构代码以防止代码在未来的不确定性。(如果类的本身现在情况以及预期的的内容都会极度简单的情况下可以暂时不遵循单一责任原则,否则就需要绝对遵守)
优点 1 降低类的复杂度并且高可读性且容易维护。 2 变更的情况下风险低,显著地降低对其他功能的影响。
O -- 开闭原则 (面向对象设计中最基础的设计原则)
定义 :一个如软件实体(类,模块和函数),应该对扩展开放而对修改关闭。
需要解决的问题场景 : 软件生命周期内,因为变化,升级,维护等原因需要对软件原有代码进行修改的时候,可能会对旧代码造成影响从而引发错误,从而导致不得不对整个功能重构和重新测试。
解决方案 : 软件的需求发生变化的时候,进行通过扩展实体(类,模块和函数)来实现变化,而非通过修改已有代码来实现变化。
主旨 : 用抽象构建框架,用实现来完成细节。
理解 : 设计原则中最抽象的原则,并且对单一责任原则,里氏替换原则,依赖倒置原则,接口隔离原则的遵守程度越好则对开闭原则达到了较好的遵守。
L -- Liscov替换原则(里氏替换原则)
有一个功能P1是由类A来完成的,此时对P进行功能扩展从而成为功能P(包含功能P1与新功能P2)。新扩展而成的功能P是由类A的子类B来完成。则子类B在王城新功能P2的同时,可能会有
导致功能P1发成故障。这是由于父类已经实现好的方法实际上是在设定一系列的规范和契约,虽然不强制要求所有的子类必须遵循这些契约但是如果子类对这些实现好的方法进行任意修改的话就会
给整个继承体系造成破坏。
此时需要的应对方案为类B在对类A进行继承的时候,完成新功能P2需要添加对应的方法外,尽量不要重写父类A的方法以及重载父类A的方法。
总体原则 : 子类可以扩展父类的功能,但不能改变父来原有的功能。
1 子类可以实现父类的抽象方法但是不可以覆盖父类的抽象方法。
2 子类可以增加自己特有的方法。
3 当子类的方法重载父类的方法时,方法的前置条件(参数)要比父类方法的输入参数更加宽松。(???? 不理解)
4 当子类的方法要实现父类的抽象方法时, 方法的后置条件(返回值)要比父类的更加严格。 (返回值要比父类的返回值更加具体)
I -- 接口隔离原则
场景 : 如果一个接口过于臃肿,会导致很类在实现其的时候同样要实现用不到的方法。为了避免这个问题,所以需要将这个臃肿的接口进行拆分,让类来实现对应需要的接口。
解决后的优点 : 可以预防外来的变更和扩展来提高系统的灵活性和可维护性。
含义 : 建立单一的接口而非庞大臃肿的接口同时尽量细化接口。
原则 : 1 接口尽量要小,但是小的限度可能会造成接口数量过多从而导致设计复杂化。
2 为依赖接口的类定制对应的方法,接口只需要定义调用的类需要的方法。只有专注地为一个模块定制方法才能建立最小的依赖关系。
3 提高内聚减少对外交互,使接口用最少的方法去完成最多的事情。
D -- 依赖倒置原则
定义 : 高层模块不应该依赖低层模块,二者都应该依赖其抽象。抽象不应该依赖细节,而细节应该依赖于抽象。
可解决的事例 : 类A依赖于类B,如果将类A改为依赖类C,必须通过修改类A的代码来完成。此场景中类A属于处理复杂业务的高级模块,类B和C属于处理基本的原子操作的底层模块。
此时如果修改类A的话会给程序带来风险。
上事例需要的解决方案 :
将类A修改为依赖接口I(关联的对象),类B和类C各自实现接口I。类A通过接口I和类B,类C进行联系。这样就会降低修改A的风险。
基于的思想 : 想对细节的多变性,抽象要稳定的多。以抽象为基础搭建起来的架构要比细节为基础的架构稳定的多。(抽象:抽象类或者接口, 细节 : 具体的实现类)
使用抽象类或者接口的目的是制定好规范和契约而不去干涉任何具体的操作,实现类去完成具体的实现细节任务。
所以总的核心思想为面向接口编程。
总体原则 : 类A中调用关联类B和类C的对象的时候,只需要让这个对象是接口I类型的就可以。所需要完成的动作放在接口中先进行抽象定义。
迪特米法则 (又称为最少知道原则 Law of Demeter):
定义 : 一个对象应该对其他的对象保持最少的了解,极为类与类之间的关系应该保持为低耦合高内聚。
目的 : 提高类的复用率以及减少类互相之间的影响,从而解决类之间的耦合问题。
实现方式 : 对于被依赖的类来说,无论逻辑多么复杂都要尽量地将逻辑封装在类的内部,对外除了提供public方法意外不泄漏任何信息。
总体原则 :
1 类与类之间不应该发生直接的相互左右。
2 一个类如果需要调用另一个类的某一个方法的话可以通过第三方的类(中介的类)转发这个调用。