在软件设计领域中,普遍认为一个好的软件设计,应该具有以下特性: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的多态就是里氏替换原则的具体应用。
依赖倒置原则
依赖倒置原则的包含如下含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
比如说,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】