Java面向对象设计原则

在软件设计领域中,普遍认为一个好的软件设计,应该具有以下特性:1)可扩展性; 2)灵活性; 3)可插入性。

因此,针对上面三个特性,我们在进行Java程序设计的时候,应该遵守以下七个设计原则:单一性原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则、组合聚合复用原则、迪米特原则。

 

单一性原则

一个类只负责一项职责。这样可以降低类的复杂度,提高类的可读性,提高系统的可维护性。如果单一性原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。

使用单一性原则的最大的问题就是对职责的定义。什么是类的职责,以及怎么划分类的职责。这跟我们社会分工一样, 一些人干这个, 另一些人干那个,只有大家都这样做了, 我们的社会才更和谐。

 

开闭原则

在项目早期的时候,客户需求可能会经常发生变化。严重情况下,可能会对开发好的代码进行推倒重来。对于一个设计良好的程序,应该能够积极响应客户需求的变化。当需求发生变化时候,可以对现有代码进行扩展,以适应新的情况。所以,在软件设计中应该遵守开闭原则:对扩展开放、对修改封闭。封装变化,是实现开放封闭原则的重要手段。

例如:计算程序运行时间。

实现思路:假如for循环模拟了一段耗时的任务。那么在任务开始的时候应该获取系统当前时间,在任务结束时候再次获取系统当前时间,然后它们的差就是程序运行所花费的时间。

public class Demo {

	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		
		for (int i = 0; i < 1000000; i++) {
			System.out.print(i);
		}
		System.out.println();
		
		long end = System.currentTimeMillis();
		
		System.out.println("运行程序花费了" + (end - start) + "毫秒!");
	}
	
}

如果按照“开闭原则”修改程序,首先我们要找出这段代码中那部分的代码是变化,那些代码是固定不变的。

通过细心分析,是不是只有业务代码部分才会经常发生变化。而且获取系统时间、计算时间的代码是固定的。所以,我们可以固定的代码抽取到一个类的方法中。代码如下所示:

abstract class Runtime {
	
	public void getTime() {
		long start = System.currentTimeMillis();
		code();
		long end = System.currentTimeMillis();
		System.out.println("运行程序花费了" + (end - start) + "毫秒!");
	}
	
	// 实现业务功能的方法 
	public abstract void code();
}

public class Demo extends Runtime {
	
	@Override
	public void code() {
		for (int i = 0; i < 1000000; i++) {
			System.out.print(i);
		}
	}

	public static void main(String[] args) {
		Demo d = new Demo();
		d.getTime();
	}

}

上面Runtime类把计算程序运行时间的代码封装到一个getTime方法中。然后对外提供了code方法。该方法主要用于实现具体的业务功能。当其他地方也需要计算程序运行时间,那么就不需要每次都重复计算,只需要继承Runtime类,并把需要计算运行时间的业务代码放在code方法中即可。

 

里氏替换原则(Liskov Substitution Principle)

里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常。反过来则不成立。如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

需求:定义一个方法,该方法可以接收任何动物。

需求分析:为了进行测试,这里我们定义了3个实体类(Animal、Dog、Bird)。其中,Dog和Bird都是Animal的子类。按照需求,这里我们在测试类中定义了一个test方法,该方法要接收任何类型的Animal对象。那么test方法的形参应该定义成什么样类型比较合适呢?

经过思考得出结论:这里应该定义成Animal类型才能够接收任何动物实体。这里的Animal就是动物的基类。如果这里不使用Animal,而是定义成Dog或Bird类型,那么test方法就只能够传入Dog或Bird类型或者是它们的子类对象。

abstract class Animal {
	
	public abstract void run();
	
}

class Dog extends Animal {

	@Override
	public void run() {
		System.out.println("小狗在马路边上奔跑...");
	}

}

class Bird extends Animal {

	@Override
	public void run() {
		System.out.println("鸟儿在天空上自由翱翔...");
	}

}

public class Demo {
	
	public static void test(Animal a) {
		a.run();
	}
	
	public static void main(String[] args) {
		Dog d = new Dog();
		test(d);
		
		Bird b = new Bird();
		test(b);
	}
	
}

其实,上面程序就是一个多态的实现。当程序调用test方法的时候,根据不同Animal对象,参数a的类型也会发生变化。如果传入的是Dog类型的对象,那么参数a就是Dog类型。如果传入的是Bird类型的对象,那么参数a就是Bird类型。

Java的多态就是里氏替换原则的具体应用。

 

依赖倒置原则

依赖倒置原则的包含如下含义:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象

比如说,A类需要使用到B类的功能,这时候A类就需要依赖B类。具体在程序中怎么实现呢?

假如Student类需要使用Pencil类的work方法。那么,Student类中应该包含Pencil的引用。具体代码如下所示:

class Student {
	Pencil pencil;
	
	public void study() {
		System.out.println("看书");
		pencil.work();
	}
}

class Pencil {
	
	public void work() {
		System.out.println("写字...");
	}
	
}

上面程序Student直接依赖了Pencil,在软件设计中称为“强耦合”。强耦合的程序设计不利于软件后期维护和功能的扩展。假如某一天,Student不再依赖铅笔(Pencil),而是依赖圆珠笔(BallPen),那么到时候就要修改Student类,把Pencil改为BallPen类型。

如何才能够减少上面类之间的耦合度呢?按照依赖倒置原则,假设A需要使用到B的功能,这个时候,A不应当直接使用B中的具体类;而应当定义一抽象接口,并由B来实现这个抽象类或接口,A只是使用该抽象类或接口,从而达到依赖倒置的目的,A也解除了对B的依赖。

按照依赖倒置原则修改上面代码:

class Student {
	Pen pen;
	
	public void study() {
		System.out.println("看书");
		pen.work();
	}
}

abstract class Pen {
	
	public abstract void work();
	
} 

class Pencil extends Pen {
	
	public void work() {
		System.out.println("铅笔写字...");
	}
	
}

class BallPen extends Pen {
	
	public void work() {
		System.out.println("圆珠笔写字...");
	}
	
}

public class Demo {

	public static void main(String[] args) {
		Student s = new Student();
//		s.pen = new Pencil();
		s.pen = new BallPen();
		s.study();
	}

}

上面程序定义了一个Pen和BallPen类,它们都继承了Pen抽象类,并实现了work方法。并且,Student不再直接依赖Pencil类,而且依赖了它的父类Pen。这样做的好处是,如果Student不想再使用铅笔(Pencil),而是换成圆珠笔(BallPen),那么不需要修改Student类的代码,只需要把BallPen对象传入到Student中即可。从而完成Student和Pencil之间的解耦。

 

接口隔离原则

建立单一接口,不要建立庞大的接口,尽量细化接口,接口中的方法尽量少。这样可以防止外来变更的扩散,提高系统的灵活性和可维护性。

 

组合/聚合复用原则

类之间的关系主要有两种:组合关系和继承关系。虽然它们都可以实现代码复用,但是相对于继承关系而言,组合关系可以使系统变得更加灵活,降低类与类之间的耦合度。如果一个类的变化对其他类造成的影响相对较少,一般首选使用组合来实现复用,其次才考虑继承。而且,为了复用而在两个不相干的类上使用继承,会让人感到很奇怪。比如下面代码:

class Person extends Pet {
	String name;
	
}

class Pet {
	String petName;
	String petColor;
	
	public void getPetInfo(String host) {
		System.out.println(host + "有一只" + petColor + "的" + petName); 
	}
}

public class Demo {

	public static void main(String[] args) {
		Person p = new Person();
		p.name = "小明";
		p.petName = "小狗";
		p.petColor = "白色";
		p.getPetInfo(p.name);
	}

}

Person和Pet是两个不相干的类,为了让Person复用Pet类的getPetInfo方法而使用继承,就会让人产生误会,认为人是一只宠物。这里应该使用组合:

class Person {
	String name;
	Pet pet;
}

class Pet {
	String petName;
	String petColor;
	
	public void getPetInfo(String host) {
		System.out.println(host + "有一只" + petColor + "的" + petName); 
	}
}

public class Demo {

	public static void main(String[] args) {
		Person p = new Person();
		p.name = "小明";
		
		Pet pet = new Pet();
		pet.petName = "小狗";
		pet.petColor = "白色";
		
		p.pet = pet;
		p.pet.getPetInfo(p.name);
	}

}

 

迪米特原则

 迪米特法则可以简单说成:talk only to your immediate friends。翻译过来就是说只和自己的朋友有说话

迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过第三者(中介类)来传达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。

例如:“教父“三部曲中的教父、杀手和中间人的关系。有一天教父想教训某人,但是教父是黑社会大佬,不方便出面。所以就找到了中间人。而中间人认识一些打手,可以帮教父去教训某人。

【示例来源:https://blog.csdn.net/zhonghuan1992/article/details/38358183

如果使用迪米特法则按照上面需求设计程序,代码如下所示:

// 教父想教训的人
class Person {
	String name;
}

// 教父
class GodFather {
	CoreMember coremember;

	public void kill(Person someone) {
		coremember.kill(someone);
	}
}

// 中间人
class CoreMember {
	Killer killer;

	public void kill(Person someone) {
		killer.kill(someone);
	}
}

// 杀手
class Killer {
	public void kill(Person someone) {
		System.out.println(someone.name + "被杀死了");
	}
}

public class Demo {

	public static void main(String[] args) {
		GodFather godFather = new GodFather();
		CoreMember coreMember = new CoreMember();
		Killer killer = new Killer();
		coreMember.killer = killer;
		godFather.coremember = coreMember;
		
		Person p = new Person();
		p.name = "小黑";
		godFather.kill(p);
	}

}

上述设计显然会更符合实际情况,并且这样做也是符合迪米特法则的。对于教父而言,中间人是它的亲密朋友,有直接关系。而中间人与killer也有直接关系。但是教父和killer没有直接关系。所以需要教父要通过中间人通过killer帮他完成任务。

在迪米特法则中,如何确定是否是朋友?

1)当前对象本身tihs

2)传入当前对象方法中的对象

3)当前对象实例变量直接引用的对象

4)当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友

【本文部分内容来源:https://www.cnblogs.com/sunflower627/p/4718702.html】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值