重新认识面向对象
封装变化
从宏观层面来看,面向对象的构建方式更能适 应软件的变化,能将变化所带来的影响减为最小。 封装变化包含两层含义:
-
将相同的变化封装到一个接口或抽象类中
-
将不同的变化封装到不同的接口或抽象类中,不应该有 两个不同的变化出现在同一个接口或抽象类中。
封装变化,就是受保护的变化,找出预计有变化或不稳定 的点,为这些变化点创建稳定的接口。
抽象约束
抽象是对一组事物的通用描述,没有具体的 实现,就表示它可以有非常多的可能性,可以随需求的变 化而变化。 通过接口或抽象类可以约束一组可能变化的行为,并且能 够实现对扩展开放,其包含三层含义:
- 通过接口或抽象类约束扩展,对扩展进行边界限定, 不允许出现在接口或抽象类中不存在的public方法。
- 参数类型,引用对象尽量使用接口或抽象类,而不是 实现类,这主要是实现里氏替换原则的一个要求。
- 抽象层尽量保持稳定,一旦确定就不要修改。
各司其职
- 从微观层面来看,面向对象的方式更强调各个类的“ 责任”
- 由于需求变化导致的新增类型不应该影响原来类型的 实现——各负其责。
软件设计的要求
- 使程序呈现高内聚,低耦合的特性
- 可维护性:代码完成之后,当需要修改程序的某个模块 时,对其他模块的影响和修改的代价。——需要修改的地 方很少,就是容易维护。
- 可扩展性:代码完成之后,当需要为程序添加新的功能 时,对其他模块的影响和添加的代价。——只需要添加添 加该功能的代码,不需要修改原来的代码(对之前的代码 没有影响),这就是可扩展。
- 重用性:代码完成之后,以后开发中可以复用部分代码, 提高效率,就是复用性强。
- 灵活性:代码完成之后,使用的地方可以通过多种方式 来调用该部分的代码,这就是灵活性好。
- 可读性:编程规范性,便于其他程序员的阅读和理解 (命名规范、代码排版、关键注释)。
- 可靠性:软件在一定的边缘条件下的出错机率、性能劣 化趋势等,又称稳定性。要求系统在发生硬件故障,软件 故障,或人为错误时,仍然可以正常工作。
- 可移植性:代码完成之后,稍微修改一下就可以在另外 一个环境中使用, 也就是说可以在两个环境以上使用, 就 具备可移植性。
设计模式七大原则
- 单一职责原则(SRP)
- 开放-封闭原则(OCP)——面向对象设计的终极目标
- 里氏代换原则(LSP)
- 依赖倒转原则(DIP)
- 合成/聚合复用原则(CARP)
- 接口隔离原则(ISP)
- 迪米特法则(LoD)
单一职责原则
职责就是对象能够承担的责任,并以某种行为方式来执行 。可以将数据库的增删改查理解成职责,即对数据的基本 操作。单一职责原则对类来说,即一个类应该只负责一项职责。
交通工具类
//方式1
class Vehicle{
public void run(String vehicle) { System.out. println(vehicle + " 在公路上跑 ");}
}
public class singleResponsibility {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("摩托车");
vehicle.run("汽车");
vehicle.run("飞机");
}
}
方案1的分析
1.在方式1的run方法中,违反 了单一职责原则
2.解决方案:根据交通工具运行 方法不同,分解成不同类
//方式2:根据交通工具运行方法不同,分解成不同类
class RoadVehicle {
public void run(String vehicle) {
System .out. println(vehicle + " 在公路上跑 ");
}
}
class AirVehicle {
public void run(String vehicle) {
System.out. println(vehicle + " 在天空中飞 ");
}
}
class WaterVehicle {
public void run(String vehicle) {
System.out. println(vehicle + " 在水里游 ");
}
}
public class singleResponsibility {
public static void main(String[] args) {
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("摩托车");
roadVehicle.run("汽车");
AirVehicle airVehicle = new AirVehicle();
airVehicle.run("飞机");
WaterVehicle waterVehicle = new WaterVehicle();
waterVehicle.run("轮船");
}
}
方案2的分析
1.遵守单一职责原则
2.但是改动很大,即要将类分解,又要修改客户端
3.改进:保留并修改Vehicle类,改动的代码会比较少
//方案3 保留并修改Vehicle类
class Vehicle2 {
public void run(String vehicle) { //不变
System .out. println(vehicle +"在公路上跑"); }
public void runAir(String vehicle) { //增加
System.out. println(vehicle + "在天空中飞"); }
public void runWater(String vehicle) { //增加
System.out. println(vehicle + "在水里游"); } }
public class singleResponsibility {
public static void main(String[] args) {
Vehicle2 Vehicle = new Vehicle2();
Vehicle.run("摩托车");
Vehicle.run("汽车");
Vehicle.runAir("飞机");
Vehicle.runWater("轮船");
}
}
方案3的分析
1.这种修改方法没有对原来的类做大的修改,只是增加方法;
2.增加的部分不影响原有部分,降低了变更引起的风险;
3.这里在类这个级别上没有遵守单一职责原则,但是在方法级 别上,仍然是遵守单一职责。
单一职责原则注意事项和细节
1)降低类的复杂度,一个类只负责一项职责。
2)提高类的可读性,可维护性
3)降低变更引起的风险
4)通常情况下,
应当遵守单一职责原则。
只有逻辑足够简单,才可以在代码级违反单一职责原则:
只有类中方法数量足够少,可以在方法级别保持单一职责 原则。
开放—封闭原则
基本介绍
开闭原则(Open Closed Principle)是编程中最基础、 最重要的设计原则——面向对象设计的终极目标 。
一个软件应当对扩展开放,对修改关闭。
开闭原则(OCP)是面向对象设计中“可复用设计”的 基石,是面向对象设计中最重要的原则之一,其它很多 的设计原则都是实现开闭原则的一种手段。
编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
怎么做?
核心--------抽象
画图类
//方式1
class Rectangle{
void draw(){ System.out.println("矩形"); }
}
class Circle { void draw(){ System.out.println("圆形"); }
}
class Triangle { //新增三角形——肯定要改
void draw(){ System.out.println("三角形"); }
}
class GraphicDraw {
void drawgraph(int type){
if(type==1){
Rectangle rec=new Rectangle();
rec.draw();
}
else if(type==2){
Circle c=new Circle();
c.draw();
}
else if(type==3){ //新增绘制三角形
Triangle t=new Triangle();
t.draw();}
}
}
public class graphDraw {
public static void main(String[] args) {
GraphicDraw graphicdraw=new GraphicDraw();
graphicdraw.drawgraph(2); //客户端肯定要改
}
}
方式1的优缺点:
- 优点是比较好理解,简单易操作。
- 缺点是违反了开闭原则(对扩展(提供方)开放,对 修改(使用方)关闭):需要给类增加新功能的时候,尽量不要修改代码,或者尽可能少修改代码。
- 本例,需要新增一个图形种类时,修改的地方比较多。
//方式2
//Shape类,基类
abstract class Shape {
public abstract void draw(); //抽象方法
}
class GraphicDraw { //新增绘制图形不需修改此类
void drawgraph(Shape s){ s.draw();
}
}
class Rectangle extends Shape {
void draw(){ System.out.println("矩形");
}
}
class Circle extends Shape {
void draw(){ System.out.println("圆形");
}
}
class Triangle extends Shape { //新增三角形
void draw(){ System.out.println("三角形");
}
}
public class graphDraw {
public static void main(String[] args) {
GraphicDraw graphicdraw=new GraphicDraw();
graphicdraw.drawgraph(new Circle());
graphicdraw.drawgraph(new Rectangle());
graphicdraw.drawgraph(new Triangle());
}
}
改进思路: 发现Rectangle和Circle有共同的draw方法,创 建抽象Shape类做成基类,并提供抽象的draw方法,让子类 去实现,当需要新的图形时,只需要让新的图形继承Shape, 并实现draw方法。画图类GraphicDraw不出现具体的类, 用抽象类Shape。这样使用方的代码就不需要修改,满足开 闭原则。
里氏代换原则
继承的优点:
- 子类拥有父类的所有方法和属性,从而可以减少创建类的工 作量。
- 提高了代码的重用性。
- 提高了代码的扩展性,子类不但拥有了父类的所有功能,还 可以添加自己的功能。
继承的缺点:
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和 方法。
- 降低了代码的灵活性。因为继承时,父类会对子类有一种约 束。
- 增强了耦合性。当需要对父类的代码进行修改时,必须考虑 到对子类产生的影响。有时修改了一点点代码都有可能需要 对大段程序进行重构。
继承包含这样一层含义:父类中已经实现的方法,实际上是 在设定规范和契约,虽然它不强制要求所有的子类必须遵 循这些契约,但是如果子类对这些已经实现的方法任意修 改,就会对整个继承体系造成破坏。
如何正确使用里氏代换原则
- 子类型(subtype)必须能够替换它们的基(父)类型。(子 类可以以父类的身份出现)
- 是关于继承机制的原则,是实现开放-封闭原则的具体规 范,违反了里氏代换原则必然违反了开放-封闭原则。
- 只要有父类出现的地方,都可以使用子类来替代。而且不会出现任何错误或者异常。
- 但是反过来却不行,子类出现的地方,不能使用父类来替代。
- 里氏代换原则的主要作用是规范继承时子类的一些书写规则。其主要目的就是保持父类方法不被覆盖。
- 在适当的情况下,可以不使用继承而是通过聚合,组合, 依赖来解决问题。
有如下程序
class A{
public int func1(int num1, int num2){
return num1-num2; }
}
class B extends A{
public int func1(int a,int b){
return a+b;}
public int func2(int a, int b){
return func1(a,b)+9;}
}
public class Liskov {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" +a.func1(11,3));
System.out.println("1-8="+a.func1(1,8));
System.out.println ("-------");
B b = new B();
System.out.println("11-3=" + b.func1(11,3));
//这里本意是求出11-3
System.out.println("1-8="+ b.func1(1,8));
System.out.println("11+3+9="+b.func2(11,3));
}
}
问题所在
是类B重写了父 类的方法func1,造成原有功能出现错误。
如何解决
通用的解决思路:
让原来的父类和子类都继承一个更通俗的基类, 原有的继承关系去掉, 采用依赖,聚合,组合等关系代替。
//创建-个更加基础的基类
class Base {
//把更加基础的方法和成员写到Base类
}
// A类
class A extends Base {
//返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2; }
}
class B extends Base{
A a=new A();
public int func1(int a,int b){
return a+b; }
public int func2(int a, int b){
return func1(a,b)+9; }
public int func3(int a, int b){
return this.a.func1(a,b); }
}
public class Liskov {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" +a.func1(11,3));
System.out.println("1-8="+a.func1(1,8));
System.out.println ("-------");
B b= new B();
//因为B类不再继承A类,因此调用者,不会再func1是求减法
//调用完成的功能就会很明确
System. out.println("11+3=" + b. func1(11, 3));//这里本意是求出11+3
System. out.println("1+8=" + b.func1(1, 8));// 1+8
System. out.println("11+3+9=" + b.func2(11, 3));
//使用组合仍然可以使用到A类相关方法
System. out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3
}
}
里氏代换原则规定,子类不能覆写父类已实现的方法。父 类中已实现的方法其实是一种已定好的规范和契约,如 果随意修改它,那么可能会带来意想不到的错误。
依赖倒转原则
基本介绍
依赖倒转原则(Dependence Inversion Principle)是指:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转(倒置)的中心思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念: 相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。 在Java中,抽象指的是接口或抽象类,细节就是具体的实现类。
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体 的操作,把展现细节的任务交给他们的实现类去完成。
精简定义:面向接口编程(Object-Oriented Design, OOD)
好处: 采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
实现person类
public class DependecyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email{ public String getInfo() {
return "电子邮件信息: hello world ";
}
}
/* Person 类 接受消息,将Email类 作为参数 产生了依赖 *
如果 参数发生变化,即接受的是微信 或短信 整个方法需要改动 */
class Person{ public void receive(Email email) {
System.out.println(email.getInfo());
}
}
改进
/*定义接受的接口 */
interface IReciver{
public String getInfo();
}
class Email implements IReciver{ public String getInfo() {
return "电子邮件信息: hello world ";
}
}
class WenXin implements IReciver{
public String getInfo() {
return "微信信息: hello WenXin ";
}
}
注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳 定性更好。
- 变量的声明类型尽量是抽象类或接口,这样变量引用和实际对 象间,就存在一个缓冲层,利于程序扩展和优化。