七大设计原则
在软件开发中,为了提高软件系统的可维护性、复用性、可扩展性、可靠性、灵活性,让程序呈现出高内聚、低耦合。程序员需要尽量根据七条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本
参考链接:
一、单一职责原则
其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。
通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,
将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度
public class SingleResponsibility_1 {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.run("飞机");
}
//交通工具类
// 方式一:此时Vehicle中的run方法既管公路上跑的,又管天上飞的,违反了单一原则
// 解决方案:根据交通工具运行方法不同,分解成不同类
static class Vehicle{
//方式一:
public void run(String vehicle){
System.out.println(vehicle + " 在公路上跑");
}
}
}
public class SingleResponsibility_2 {
public static void main(String[] args) {
new RoadVehicle().run("汽车");
new AirVehicle().run("飞机");
}
/**
* 方式二:
* 1. 遵守单一职责原则
* 2. 但是改动太大,又要分解类,又要修改客户端
* 改进方案:修改Vehicle类,改动的代码会比较少
*/
static class RoadVehicle{
public void run(String vehicle){
System.out.println(vehicle + " 在公路上跑");
}
}
static class AirVehicle{
public void run(String vehicle){
System.out.println(vehicle + " 在天上飞");
}
}
static class WaterVehicle{
public void run(String vehicle){
System.out.println(vehicle + " 在水里游");
}
}
}
如果严格遵守单一职责原则,那么可以造成类急速增多,维护性反而下降
所以当类中逻辑简单,类中方法足够少,我们可以在方法级别保持单一职责原则
public class SingleResponsibility_3 {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.runRoad("汽车");
vehicle.runWater("船");
vehicle.runAir("飞机");
}
/**
* 方式三:
* 1. 这种修改方法没有对原来的类做大的修改,只是增加方法
* 2. 这里虽然没有在类级别遵守单一职责,但是在方法级别上遵守单一职责
*/
static class Vehicle{
public void runRoad(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 abstract class AbstractSkin {
abstract public void display();
}
//AbstractSkin的两个子类
public class DefaultSkin extends AbstractSkin{
@Override
public void display() {
System.out.println("默认");
}
}
public class HeimaSkin extends AbstractSkin{
@Override
public void display() {
System.out.println("黑马");
}
}
public class SougouInput {
private AbstractSkin skin;
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
public void display(){
skin.display();
}
public static void main(String[] args) {
SougouInput input = new SougouInput();
//如下例所示,搜狗皮肤的设置依赖于AbstractSkin
//如果要扩展搜狗皮肤,只需在写一个AbstractSkin的子类即可
//input.setSkin(new DefaultSkin());
input.setSkin(new HeimaSkin());
input.display();
}
}
三、里氏代换原则
- 任何基类可以出现的地方,子类一定可以出现。
- 本质上:子类可以扩展父类的功能,但不能改变父类原有的功能。
- 换句话说子类继承父类时,除了添加新的方法完成新增功能外,尽量不要重写父类的方法
- 好处:
- 如果重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差。
特别是多态使用频繁时,程序运行出错的概率会非常大。 - 提高继承体系的可复用性,保证使用多态时不出错。
- 如果重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差。
违背里氏代换的情况
//长方形
public class Rectangle {
private double length;
private double width;
public double getWidth() return width;
public void setWidth(double width) this.width = width;
public double getLength() return length;
public void setLength(double length) this.length = length;
}
//正方形
//这里的正方形,重写了长方形中的set方法
//保证了长与宽的相等。
public class Square {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setLength(width);
}
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
}
public class RectangleDemo {
//扩宽方法
public static void resize(Rectangle rectangle){
while(rectangle.getWidth() <= rectangle.getLength()){
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
//测试
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setLength(20);
//这里对长方形进行扩宽,没有问题
resize(rectangle);
System.out.println("==========================");
Square square = new Square();
square.setWidth(10);
//square是rectangle子类,可以使用多态
//但由于改写了set方法,square中的长宽一直相等,导致程序陷入死循环
//而里氏代换原则中基类出现的地方,子类一定能出现。这里显然不满足。
//这里子类改写了父类的方法,那么在多态中容易引发错误
resize(square);
}
}
根据里氏代换原则改进
//四边形
public interface Quadrilateral {
double getLength();
double getWidth();
}
public class Rectangle implements Quadrilateral{
private double length;
private double width;
public void setLength(double length) { this.length = length; }
public void setWidth(double width) { this.width = width; }
@Override
public double getLength() { return length; }
@Override
public double getWidth() { return width; }
}
四、依赖倒转原则
高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
简单的说:就是对抽象编程,不要对实现编程,这样就降低了客户与实现模块间的耦合。
组合关系: 整体类对象 能管理 部分对象的 生命周期。整体归西,部分必归西。
聚合关系: 整体类对象 能利用 部分对象,而不能管理部分对象生命周期。整体归西,部分不一定归西。
违反依赖倒转原则案例:
public Class IntelCpu {
public void run(){
System.out.println("cpu is running...");
}
}
public Class XiJieHardDisk {
public void get(){
System.out.println("getting data from XiJieHardDisk");
}
public void save(){
System.out.println("save data to XiJieHardDisk");
}
}
public Class Computer {
private IntelCpu cpu;
private XiJieHardDisk hardDisk;
public void setIntelCpu(IntelCpu cpu) this.cpu = cpu;
public void setXiJieHardDisk(XiJieHardDisk hardDisk) this.hardDisk = hardDisk;
//这里是面向实现类编程,简单易懂,但扩展不方便
//cpu用别的还得改代码,硬盘hardDisk也是如此
public void run(){
hardDisk.get();
cpu.run();
hardDisk.save();
}
}
用依赖倒转原则改进
public interface Cpu {
void run();
}
public interface HardDisk {
void get();
void save();
}
public Class Computer {
//成员变量 使用接口,而不是实现类
//所以现在可以给Cpu设置cpu接口的多种实现类了
private Cpu cpu;
private HardDisk hardDisk;
public void setCpu(Cpu cpu) this.cpu = cpu;
public void setHardDisk(HardDisk hardDisk) this.hardDisk = hardDisk;
public void run(){
hardDisk.get();
cpu.run();
hardDisk.save();
}
}
五、接口隔离原则
接口隔离原则也叫最小接口原则
本质就是不应该让类*依赖它不使用的方法
一个类对另一个类的依赖应该简历在最小的接口上
- 违背接口隔离的例子:
public class Segregation_1 {
/**
* 显然,
* 类B,类D都是Interface1的实现类,都实现了Interface1的五种方法
* 类A通过接口依赖类B,类C通过接口依赖类D
* 如果接口对于类A和类C来说不是最小接口(即没有用到接口中的所有非default方法)
* 那么则会违反接口隔离原则
*
* 使用接口隔离原则改进
* 处理方法:把接口拆分独立的几个接口,类A和类C分别与他们需要的接口进阿里依赖关系。
*/
interface Interface1 { void method1(); void method2(); void method3(); }
class B implements Interface1 {
@Override
public void method1() { System.out.println("B-method1"); }
@Override
public void method2() { System.out.println("B-method2"); }
@Override
public void method3() { System.out.println("B-method3"); }
}
class D implements Interface1 {
@Override
public void method1() { System.out.println("D-method1"); }
@Override
public void method2() { System.out.println("D-method2"); }
@Override
public void method3() { System.out.println("D-method3"); }
}
//A类通过接口依赖B类,但只用到method1、method2
class A {
public void depend1(Interface1 interface1){ interface1.method1(); }
public void depend2(Interface1 interface2){ interface2.method2(); }
}
//C类通过接口依赖D,但只用到method1、method3
class C {
public void depend1(Interface1 inf){ inf.method1(); }
public void depend3(Interface1 inf){ inf.method3(); }
}
}
- 改进,改进的话,只需把接口拆分为多个,让实现类B、D实现多个接口即可
六、迪米特法则
迪米特法则也叫最少知识原则。只和你的直接朋友说话,不跟“陌生人”说话
-
含义
如果两个软件实体无须直接通信,那么就不应该发生直接的相互调用,可以通过第三方转发该调用。
-
目的:降低类的耦合度,提高模块的相对独立性
-
迪米特法则中的朋友是指:(这些对象存在关联、聚合或组合关系,可以直接访问这些对象的方法)
- 当前对象本身
- 当前对象的成员对象
- 当前对象所创建的对象
- 当前对象的方法参数等。
违背迪米特法则的案例:
//老板类
//此时老板需要拿到员工类,让员工打印名字。
//但员工类不是老板类的朋友,因此这个老板类的设计不符合迪米特法则。
public class Boss {
public void getEmloye(TeamLeader teamLeader){
//此处的employ是其他对象方法的返回值,不属于boss的朋友
Employe employ = teamLeader.getEmploy();
System.out.println(employ.getName());
}
}
//项目组长类
public class TeamLeader {
//返回一个员工对象
public Employe getEmploy(){
return new Employe("张三");
}
}
//员工类
public class Employe{
private String name;
public setName(String name) this.name = name;
public getName() return this.name;
}
改进:
//老板类
public class Boss {
public void getEmloye(TeamLeader teamLeader){
//同员工通信细节完全交给项目组长实现
//老板类不直接同员工类通信
teamLeader.getEmploy();
}
}
//项目组长类
public class TeamLeader {
public void getEmploy(){
System.out.println(new Employe("张三").getName());
}
}
七、合成复用原则
尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然简单易于实现,但也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的试下细节暴露给子类,父类对子类是透明的,所以这种复用又称为”白箱“复用
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生裱花,这不利于类的扩展与维护
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之称为新对象的一部分,新对象可以调用已有对象的功能,其有以下优点:
- 它维持了类的封装性。因为成员对象的内部细节是新对象看不见的,所以这种复用又称为”黑箱“复用
- 对象之间的耦合度低。可以在类的成员位置声明抽象
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的对象
用继承的情况
继承复用,简单易于实现,但一旦扩展,则要生成多个类。
用接口的情况
合成复用,扩展只需生成一个类