软件设计应该按照某些原则,这样子能让软件的可复用性和可维护性更强。
单一职责原则 (Single responsibility principle)
定义
就一个类而言,应该只有一个引起它变化的原因。如果一个类既可以用来渲染游戏界面,又可以用来控制游戏逻辑,我们就说这个类有两个引起它变化的原因。比如有一个UserInfo类,UserInfo类中有设置属性的方法(eg. setName)和行为(eg. sayHello)的方法,我们可以把UserInfo类分为设置属性的类和设置行为的类。
优点
- 使用单一职责原则之后,我们修改一个功能,对其他功能的影响显著降低。
- 能使我们的代码更模块化,更容易阅读。
- 使代码的可维护性更高,因为如果分为不同的职责,我们修改一个职责后,对另外的职责影响就降低很多。
违背的后果
- 一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
- 某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置
开闭原则 (Open Close Principle)
定义
一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
(PS:对xml和properties的修改不算是对原有代码的修改,因为他们不需要经过编译)
举个栗子
有一只猫,他会说喵喵喵...
public class Cat {
public void say(){
System.out.println("喵喵喵...");
}
}
有一个动物园,我们传入猫,他也会喵喵喵...
public class Zoo {
public void say(String animal){
if("cat".equals(animal)){
Cat cat = new Cat();
cat.say();
}
}
}
如果这时候动物园新来了一只狗...
public class Dog {
public void say(){
System.out.println("汪汪汪...");
}
}
那么我们就要修改Zoo的代码为:
public class Zoo {
public void say(String animal){
if("cat".equals(animal)){
Cat cat = new Cat();
cat.say();
}else if("dog".equals(animal)){
Dog dog = new Dog();
dog.say();
}
}
}
假如以后还有来牛,兔子,老虎,我们就要对这个Zoo类不停的修改...很明显代码的扩展性很差,那么我们可以怎么做呢?
我们可以新增一个抽象类Animal,Animal中有抽象接口say,Cat和Dog继承Animal,Animal作为Zoo的一个成员变量。
public abstract class Animal {
abstract void say();
}
public class Cat extends Animal{
@Override
public void say(){
System.out.println("喵喵喵...");
}
}
public class Dog extends Animal{
@Override
public void say(){
System.out.println("汪汪汪...");
}
}
public class Zoo {
private Animal animal;
public void setAnimal(Animal animal) {
this.animal = animal;
}
public void say(){
animal.say();
}
}
采用spring的依赖注入功能,以后我们新添一只牛的时候,也只要新添牛的类,然后把spring配置文件的zoo依赖的animal改成牛就可以了。
迪米特原则(Law of Demeter)
定义
- 类的朋友
类自己,类的成员变量,方法参数,方法返回。 - 类的陌生类
指我们在方法中创建的局部变量。 - 概念
一个类应该对陌生类保持最少的了解,只与朋友保持联系,即不要在方法中随便创建无关紧要的类,不要随便调用这些类的方法。如果一个类一定要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
为什么
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大;通过迪米特原则,可以解耦类与类之间的关系,只有弱耦合,类或者方法的复用率才能提高。
举个栗子
校长需要知道某个班的学生的数量:
/**
* 校长类
*/
public class Principal {
public void countOneClassStudent(String className){
System.out.println("去" + className);
List<Student> students = getStudents();
System.out.println("这个班一共有" + students.size());
}
public List<Student> getStudents(){
List<Student> students = new ArrayList<>();
students.add(new Student("小明"));
students.add(new Student("小红"));
return students;
}
}
校长平时很忙的,他与学生接触是很少的,更不可能亲自下到某个班里去统计学生的数量,所以学生不是校长的“朋友”。在校长这个类的统计学生数量的方法,校长只需要知道学生的数量,但是却出现了学生的类,这很明显违背了迪米特原则。那什么才是校长的朋友呢?对,就是老师。。。而学生又是老师的朋友。所以我们可以把代码做如下调整:
/**
* 校长类
*/
public class Principal {
Map<String, Teacher> teacherMap = new HashMap<>();
public Teacher getTeacher(String className){
return teacherMap.get(className);
}
public void countOneClassStudent(String className){
Teacher teacher = getTeacher(className);
teacher.countOneClassStudent();
}
}
/**
* 老师类
*/
public class Teacher {
private String className;
public void countOneClassStudent(){
System.out.println("去" + className);
List<Student> students = getStudents();
System.out.println("这个班一共有" + students.size());
}
public List<Student> getStudents(){
List<Student> students = new ArrayList<>();
students.add(new Student("小明"));
students.add(new Student("小红"));
return students;
}
}
里式替换原则(Liskov Substitution Principle)
定义
- 如果我们把代码中所有使用父类的地方都替换成子类,代码还能够执行,不产生错误和异常。
- 替换后的代码为什么还能够运行呢?因为子类继承了父类的方法。
- 如果要想不产生错误和异常,最好子类不要重写和重载父类的方法,因为父类是已经验证过的代码,子类你自己写的代码没有验证过,不保证一定没有问题。
怎么做
- 最好不要重写父类的方法
- 如果差不多相同的需求,最好子类另扩展一个不同名字的方法
- 如果一定要重写父类的方法,首先要保证方法的参数类型要比原方法的参数类型宽松,即可以是原方法参数的父类;其次要保证重写后的方法的返回值要比原方法的严格,即是它的子类型,最后要保证逻辑没有问题。
接口隔离原则(Interface Segregation Principle)
定义
不能强迫类去实现它们不需要的接口,这样子会增大工作量,即使我们是做空实现也不好;我们可以把一个总接口拆成多个分接口,类只需要实现它们需要的接口即可。
举个栗子
比如家政中心是一个接口,它有打扫卫生,家电维修等功能。我们的保洁阿姨是家政中心的一员,她只会打扫卫生,但是假如我们要保洁阿姨实现家政中心的接口,那么肯定也要实现家电维修的功能,即使它是一个空实现,那么我们可以怎么做呢?我们可以把家政中心的接口拆成打扫卫生和家电维修两个接口(家政中心可以继承打扫卫生和家电维修两个接口),而保洁阿姨只需要实现打扫卫生的接口即可。
依赖倒置原则(Dependence Inversion Principle)
依赖倒置是什么
定义
抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
倒置?
传统软件开发中,高层模块依赖底层模块,当底层模块发生变化的时候,高层模块也要随之发生变化。比如一个车身依赖一个轮胎,当轮胎的尺寸设计变大以后,车身也要做相应的处理,以适应这个轮胎(车身依赖轮胎)。而依赖倒置就是车身给出一个轮胎的模具,轮胎根据这个模具来实现(轮胎依赖模具)。这样子不管新造的轮胎是怎么样的,车身都不用做任何调整。
举个栗子
网上依赖倒置的例子太多了,就是面向接口编程。
合成复用原则(Composite Reuse Principle)
定义
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合。
怎么做
一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继承。"Is-A"是严格的分类学意义上的定义,意思是一个类是另一个类的"一种";而"Has-A"则不同,它表示某一个角色具有某一项责任。
为什么
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。
由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。
网友评论