软件设计原则
软件设计原则是指导软件开发过程中设计和构建软件系统的一组规则。这些规则的目的是为了让程序达到高内聚、低耦合以及提高扩展性,其实现手段是面向对象的三大特性:封装、继承以及多态。
设计原则名称 | 核心思想 |
---|---|
单一职责原则 | 一个类只负责一个特定的职责 |
开放封闭原则 | 软件实体应该可以扩展,但不应该修改其已有代码 |
依赖倒转原则 | 高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象 |
里氏替换原则 | 任何基类可以出现的地方,子类也可以出现 |
接口隔离原则 | 使用多个专门的接口,而不是一个通用的接口 |
合成复用原则 | 优先使用组合而不是继承来实现代码复用 |
迪米特法则 | 一个对象应尽量少地了解其他对象,从而降低耦合度 |
单一职责原则
其核心思想为,一个类最好只做一件事。单一职责原则可降低类的复杂度,提高代码可读性、可维护性、降低变更风险。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。
public class MainTest {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.running("汽车");
// 飞机不是在路上行驶
vehicle.running("飞机");
}
}
/**
* 在run方法中违反了单一职责原则
* 解决方法根据不同的交通工具,分解成不同的类即可
*/
class Vehicle{
public void running(String name) {
System.out.println(name + "在路上行驶 ....");
}
}
// 解决
public class MainTest {
public static void main(String[] args) {
Driving driving = new Driving();
driving.running("汽车");
Flight flight = new Flight();
flight.running("飞机");
}
}
class Driving {
public void running(String name) {
System.out.println(name + "在路上行驶 ....");
}
}
class Flight {
public void running(String name) {
System.out.println(name + "在空中飞行 ....");
}
}
只要类中方法数量足够少,可以在方法级别保持单一职责原则。
public class MainTest {
public static void main(String[] args) {
Vehicle2 vehicle2 = new Vehicle2();
vehicle2.driving("汽车");
vehicle2.flight("飞机");
}
}
/*
* 改进
*↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
*/
class Vehicle2 {
public void driving(String name) {
System.out.println(name + "在路上行驶 ....");
}
public void flight(String name) {
System.out.println(name + "在空中飞行 ....");
}
}
开放封闭原则
软件实体应该是可扩展的,而不可修改的。也就是对提供方扩展开放,对使用方修改封闭的。开放封闭原则主要体现在两个方面:
- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开放封闭原则的核心思想是对抽象编程,而不对具体编程。因为抽象相对稳定,让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。需求总是变化,当我们给程序添加或者修改功能时,需要用开放封闭原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。编程中遵循其他原则,以及使用其他设计模式的目的就是为了遵循开闭原则。
当软件需要变化时,尽量使用扩展的软件实体的方式行为来实现变化,而不是通过修改已有的代码来实现变化。
public class MainTest {
public static void main(String[] args) {
Mother mother = new Mother();
Son son = new Son();
Daughter daughter = new Daughter();
// 注入子类对象 如果扩展需要其他类 换成其他对象即可
mother.setAbstractFather(son);
mother.display();
}
}
abstract class AbstractFather {
protected abstract void display();
}
class Son extends AbstractFather{
@Override
protected void display() {
System.out.println("son class ...");
}
}
class Daughter extends AbstractFather{
@Override
protected void display() {
System.out.println("daughter class ...");
}
}
class Mother {
private AbstractFather abstractFather;
public void setAbstractFather(AbstractFather abstractFather) {
this.abstractFather = abstractFather;
}
public void display() {
abstractFather.display();
}
}
依赖倒置原则
该原则依赖于抽象,具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象,抽象不依赖于具体,具体依赖于抽象。我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现,即在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍。
依赖于抽象,就是对接口编程,不要对实现编程。
// 定义一个接口,表示消息发送者
interface MessageSender {
void sendMessage(String message);
}
// 实现接口的EmailSender类
class EmailSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending email with message: " + message);
}
}
// 实现接口的SmsSender类
class SmsSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS with message: " + message);
}
}
// 高层模块的MessageService类依赖于MessageSender接口
class MessageService {
private MessageSender sender;
// 通过构造函数注入依赖
public MessageService(MessageSender sender) {
this.sender = sender;
}
public void processMessage(String message) {
// 使用MessageSender接口发送消息
sender.sendMessage(message);
}
}
public class Main {
public static void main(String[] args) {
// 使用EmailSender发送消息
MessageSender emailSender = new EmailSender();
MessageService emailService = new MessageService(emailSender);
emailService.processMessage("Hello via Email!");
// 使用SmsSender发送消息
MessageSender smsSender = new SmsSender();
MessageService smsService = new MessageService(smsSender);
smsService.processMessage("Hello via SMS!");
}
}
接口隔离原则
使用多个小的专门的接口,而不要使用一个大的总接口。具体而言,接口隔离原则体现在,接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性,而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等。某些时候,实现类并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,应该将胖接口分离为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
分离的手段主要有以下两种:
- 委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。
- 多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
public class MainTest {
public static void main(String[] args) {
FuncImpl func = new FuncImpl();
func.func1();
func.func2();
func.func3();
}
}
interface Function1{
void func1();
// 如果将接口中的方法都写在一个接口就会造成实现该接口就要重写该接口所有方法。
// 当然Java 8 接口可以有实现,降低了维护成本,解了决该问题;
// 但是我们还是应当遵循该原则,使得接口看起来更加清晰
// void func2();
// void func3();
}
interface Function2 {
void func2();
}
interface Function3 {
void func3();
}
class FuncImpl implements Function1,Function2,Function3{
@Override
public void func1() {
System.out.println("i am function1 impl");
}
@Override
public void func2() {
System.out.println("i am function2 impl");
}
@Override
public void func3() {
System.out.println("i am function3 impl");
}
}
里氏替换原则
里氏替换原则这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。里氏替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了里氏替换原则,才能保证继承复用是可靠地。
实现的方法是面向接口编程,即将公共部分抽象为基类接口或抽象类,在子类中通过覆写父类的方法实现新的方式支持同样的职责。里氏替换原则是关于继承机制的设计原则,违反了里氏替换原则就必然导致违反开放封闭原则。里氏替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
简单来说就是子类可以扩展父类的功能,而不应该改变父类原有的功能。如果通过重写父类方法来完成新的功能,这样写起来虽然简单,但整个体系的可复用性会非常差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
public class MainTest {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(20);
rectangle.setHeight(10);
resize(rectangle);
print(rectangle);
System.out.println("=======================");
Rectangle square = new Square();
square.setWidth(10);
// 因为 Square类 重写了父类 setWidth setHeight 方法,会导致 while 循环变成一个无限循环
resize(square);
print(square);
}
public static void resize(Rectangle rectangle){
while (rectangle.getWidth() >= rectangle.getHeight()){
rectangle.setHeight(rectangle.getHeight() + 1);
}
}
public static void print(Rectangle rectangle){
System.out.println(rectangle.getWidth());
System.out.println(rectangle.getHeight());
}
}
// 正方形
class Square extends Rectangle{
@Override
public void setWidth(Integer width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(Integer height) {
super.setWidth(height);
super.setHeight(height);
}
}
// 长方形
class Rectangle {
private Integer width;
private Integer height;
public void setWidth(Integer width) {
this.width = width;
}
public void setHeight(Integer height) {
this.height = height;
}
public Integer getWidth() {
return width;
}
public Integer getHeight() {
return height;
}
}
合成复用原则
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。首先应该考虑使用组合/聚合,因为组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
尽量使用对象组合,而不是继承来达到复用的目的。
// 引擎接口
interface Engine {
void start();
}
// 电动引擎
class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("Electric engine starts...");
}
}
// 燃油引擎
class GasEngine implements Engine {
@Override
public void start() {
System.out.println("Gas engine starts...");
}
}
// 汽车类
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
System.out.println("Car starts...");
}
}
public class Main {
public static void main(String[] args) {
// 创建一个电动引擎汽车
Engine electricEngine = new ElectricEngine();
Car electricCar = new Car(electricEngine);
electricCar.start();
System.out.println("=======================");
// 创建一个燃油引擎汽车
Engine gasEngine = new GasEngine();
Car gasCar = new Car(gasEngine);
gasCar.start();
}
}
迪米特法则
迪米特法则又叫最少知识原则,就是说一个对象应当对其他对象有尽可能少的了解。其核心思想为,降低类之间的耦合。如果类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,所以一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,被耦合或调用的类的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public
方法,我就调用这么多,其他的一概不关心。迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成搏击,也就是说,信息的隐藏促进了软件的复用。
迪米特法则还有个更简单的定义,只与直接的朋友交谈,不跟“陌生人”说话。
朋友定义:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。
耦合的方式很多:依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
// 学生类
class Student {
private String name;
private Class myClass;
public Student(String name, Class myClass) {
this.name = name;
this.myClass = myClass;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 查询自己的班级信息
public void queryClassInfo() {
String className = myClass.getClassName();
System.out.println(name + " is in class " + className);
}
}
// 班级类
class Class {
private String className;
public Class(String className) {
this.className = className;
}
public String getClassName() {
return className;
}
}
// 老师类
class Teacher {
private String name;
public Teacher(String name) {
this.name = name;
}
// 不符合迪米特法则的方法示例,直接返回班级名
// public String getStudentClass(Student student) {
// return student.myClass.getClassName();
// }
// 通过学生对象调用公共方法获取班级信息
public String getStudentClass(Student student) {
return student.queryClassInfo();
}
}
public class Main {
public static void main(String[] args) {
// 创建班级
Class class1 = new Class("Class 1");
// 创建学生并设置班级
Student student = new Student("Alice", class1);
// 学生查询自己的班级信息
student.queryClassInfo();
System.out.println("=======================");
// 创建老师并查询学生的班级信息
Teacher teacher = new Teacher("Mr. Smith");
String studentClass = teacher.getStudentClass(student);
System.out.println(student.getName() + " is in class " + studentClass);
}
}