目录
一、单一职责原则
1.理解
就是面向对象的思想,每个对象只需要负责好自己的事情,例如此时有一个People类,里面有一些方法,比如送外卖、打螺丝、摸鱼,这些虽然都属于人类的行为,但这样写的话类会显得极为臃肿,此时我们创建外卖员类和打工人类,送外卖就是外卖员的行为,打螺丝和摸鱼就是打工人的行为,这样就划分出了每个类的职责。
二、开闭原则
1.理解
“对扩展开放,对修改关闭,强调使用抽象接口进行架构,通过实现类来扩展细节”,其实不太好理解,通俗易懂来说就是提供一个接口或者抽象类,通过多个实现类去实现,然后提供一种热插拔的效果。
例如搜狗输入法的换皮肤功能,皮肤的提供者可能是厂商A或厂商B,此时搜狗输入法就可以为不同厂商提供一个统一的接口,然后厂商去实现这个接口以更换自己的皮肤。和Web开发中MVC架构类似,例如service层,我们要先定义接口,然后去实现这个接口,只不过大多数情况下我们的接口和实现类是1:1的。实现热插拔的效果的话,接口和实现类比例为1:n。
2.搜狗输入法案例
/**
* 抽象皮肤类
*/
public abstract class AbstractSkin {
public abstract void display();
}
/**
* 默认皮肤类
*/
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 display() {
skin.display();
}
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
}
/**
* 开闭原则:对扩展开放,对修改关闭。实现热插拔。我们需要使用到接口或者抽象类
*/
public class Test {
public static void main(String[] args) {
//1.创建搜狗输入法
SouGouInput souGouInput = new SouGouInput();
//2.插入默认皮肤
souGouInput.setSkin(new DefaultSkin());
//3.展示
souGouInput.display();
//4.替换成黑马皮肤
souGouInput.setSkin(new HeiMaSkin());
//5.展示
souGouInput.display();
}
}
三、里氏替换原则
1.理解
“父类出现的地方,子类也一定可以出现;子类可以拓展父类的功能,但不能重写父类原有的功能”。
2.四边形案例
刚开始学面向对象编程的时候,一直是通过extends长方形去实现的正方形,但是这样就不符合里氏替换原则,因为实现正方形的时候重写了长方形已经实现好了的方法。此时我们可以单独抽象出来一个四边形接口或者抽象类,然后正方形和长方形都去实现这个接口,这时对于四边形接口来说,我们只是对其进行了扩展,而不是重写覆盖了原有的功能。
/**
* 四边形
*/
public interface Quadrilateral {
double getLength();
double getWidth();
}
public class Rectangle implements Quadrilateral {
private double length;
private double width;
@Override
public double getLength() {
return this.length;
}
@Override
public double getWidth() {
return this.width;
}
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
}
public class Square implements Quadrilateral {
private double side;
public void setSide(double side) {
this.side = side;
}
@Override
public double getLength() {
return this.side;
}
@Override
public double getWidth() {
return this.side;
}
}
/**
* 里氏代换原则:父类出现的地方,子类也一定能出现。子类可以扩展父类的功能,但不能重写父类原有的功能。
* 案例:正方形不是长方形
* 原本正方形是需要继承长方形的,但这样会导致在resize的时候长方形可以,但正方向不行,违背了里氏代换原则,
* 所以单独拿出来一个四边形接口,让长方形和正方形去实现这个接口,这样即抽取了他们共有的部分,也避免了之间的继承关系
*/
public class Test {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setLength(20);
rectangle.setWidth(10);
resize(rectangle);
printLengthAndWidth(rectangle);
}
public static void resize(Rectangle rectangle) {
while (rectangle.getLength() >= rectangle.getWidth()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println("Length = " + rectangle.getLength());
System.out.println("Width = " + rectangle.getWidth());
}
}
四、依赖倒转原则
1.理解
“高层模块不应该依赖于底层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象”。例如我们使用spring进行开发的时候,通过构建mvc架构,每一层有每一层的接口,然后是实现类,通过@Service、@Mapper、@Controller等注解把bean注入到容器中,在我们需要使用到这些实现类的时候,我们只需要@Autowired它的接口即可,这就是高层模块依赖于抽象,当我们需要改动实现类的时候,只要实现了这个接口,相比我们直接使用实现类会少改动很多代码。
依赖倒转原则和开闭原则很像,依赖倒转是高层模块不依赖底层模块,而是依赖于抽象,抽象不依赖于细节,细节应该依赖于抽象。开闭原则是对扩展开放,对修改关闭,强调使用抽象接口进行架构,通过实现类来实现具体细节。我的理解就是依赖倒转原则实现了开闭原则,因为依赖倒转原则说的更加具体一些,而且也是通过抽象接口的方式去进行依赖倒转的,依赖倒转原则强调高层不依赖底层,开闭原则强调使用接口架构。
2.组装电脑案例
/**
* 依赖倒转原则:
* 通过在computer类中定义cpu、硬盘、内存的抽象,这样我们不光可以使用intel的cpu,
* 也可以使用其他厂商的cpu,此时上层computer不会依赖于底层cpu的实现,而是依赖cpu的抽象,
*/
public class Computer {
private Cpu cpu;
private HardDisk hardDisk;
private Memory memory;
@Override
public String toString() {
return "Computer{" +
"cpu=" + cpu +
", hardDisk=" + hardDisk +
", memory=" + memory +
'}';
}
public Cpu getCpu() {
return cpu;
}
public void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public HardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Memory getMemory() {
return memory;
}
public void setMemory(Memory memory) {
this.memory = memory;
}
}
public interface Cpu {
void run();
}
public class IntelCpu implements Cpu{
@Override
public void run() {
System.out.println("IntelCpu...");
}
}
public interface HardDisk {
void run();
}
public class XiJieHardDisk implements HardDisk{
@Override
public void run() {
System.out.println("XiJieHardDisk...");
}
}
public interface Memory {
void run();
}
public class KingstonMemory implements Memory {
@Override
public void run() {
System.out.println("KingstonMemory...");
}
}
public class Test {
public static void main(String[] args) {
Computer computer = new Computer();
computer.setCpu(new IntelCpu());
computer.setMemory(new KingstonMemory());
computer.setHardDisk(new XiJieHardDisk());
computer.getCpu().run();
computer.getMemory().run();
computer.getHardDisk().run();
System.out.println(computer);
}
}
五、接口隔离原则
1.理解
“客户端不应该依赖那些它不需要的接口”,换句话说,其实就是需要让我们细粒度化接口。
2.电子设备案例
电脑和风扇虽然都属于电子设备,但是风扇没有cpu和内存,如果我们使用统一的接口,这样会造成风扇使用该接口的时候有的方法返回null值,此时需要进行接口细粒度的划分,分为普通电子设备和智能电子设备,然后电脑和风扇分别去实现他们,每个实现类要充分利用到它所实现的接口。
public interface NormalDevice {
String getType();
}
public interface SmartDevice {
String getType();
String getCpu();
String getMemory();
}
public class Fan implements NormalDevice {
@Override
public String getType() {
return "风扇";
}
}
public class Computer implements SmartDevice {
@Override
public String getType() {
return "电脑";
}
@Override
public String getCpu() {
return "intel";
}
@Override
public String getMemory() {
return "kingston";
}
}
六、合成复用原则
1.理解
“优先使用对象组合的方式,而不是通过继承来达到复用的目的”。我们都知道java中的继承,大多数情况下我们为了减少代码的重复而去选择继承一个对象,但这样也会导致耦合度增加,例如此时我们有一个yourbatis的框架,他需要连接mysql数据库,我们可以直接让yourbatis继承connect类,但这样就会增加耦合度的同时导致connect类中的一些无关的方法和字段在yourbatis中也可以访问到,所以我们优先考虑使用组合的方式,在yourbatis中设置他的接口,再去set这个接口把connect传进去。
2.数据库连接案例
public class YourBatis {
private ConnectDatabase connectDatabase;
public void test() {
connectDatabase.connect();
}
public ConnectDatabase getConnectDatabase() {
return connectDatabase;
}
public void setConnectDatabase(ConnectDatabase connectDatabase) {
this.connectDatabase = connectDatabase;
}
}
public interface ConnectDatabase {
void connect();
}
public class MySQL implements ConnectDatabase {
@Override
public void connect() {
System.out.println("连接mysql数据库...");
}
}
public class Test {
public static void main(String[] args) {
YourBatis yourBatis = new YourBatis();
yourBatis.setConnectDatabase(new MySQL());
yourBatis.test();
}
}
七、迪米特法则
1.理解
“每个软件单位对其他单位都只有最少的知识,而且局限于那些于本单位密切相关的软件单位”,简单来说就是,一个类/模块对其他类/模块有越少的交互越好,当一个类发生改动,那么与其他相关的类(比如用到此类方法的类)需要尽可能少的受影响(比如修改了方法名、字段名等,可能其他用到这些方法或是字段的类也需要跟着修改),这样有利于维护。
2.网络地址案例
public class Socket {
private String domain;
private String port;
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getUrl() {
return this.domain + ":" + this.port;
}
@Override
public String toString() {
return "Socket{" +
"domain='" + domain + '\'' +
", port='" + port + '\'' +
'}';
}
}
/**
* 迪米特原则:
* 不同类或者模块间的交互越少越好,
* 在test入参时传入string类型的url,而不是传入socket对象,
* 如果传入socket对象进去,当socket对象内的方法发生改变,也会波及到test
*/
public class Test {
public static void main(String[] args) {
Test test = new Test();
Socket socket = new Socket();
socket.setDomain("www.soulzzz.cn");
socket.setPort("8080");
test.test(socket.getUrl());
}
public void test(String url) {
System.out.println(url);
}
}
八、总结回顾
1.单一职责原则:每个对象/模块只负责一类功能。
2.开闭原则:通过面向接口编程,对扩展开放,对修改关闭,强调使用接口进行架构,再通过实现具体的实现类来描述细节。
3.里氏替换原则:父类出现的地方,子类也一定可以出现。子类可以扩展父类的功能,但不能覆盖重写父类原有的实现。
4.依赖倒转原则:通过面向接口编程,使上层模块不依赖于底层模块,他们都依赖于抽象,而抽象不依赖于细节,细节应依赖于抽象。
5.接口隔离原则:一个类不应该依赖于它不需要的接口,保证接口的粒度,细粒化接口。
6.合成复用原则:当考虑复用功能或者对象时,尽量使用合成的方式,而不是继承的方式。
7.迪米特法则:对象/模块之间的交互越少越好,当一个对象/模块发生了改动,要使其他和这个对象/模块有关联的对象/模块受到的影响越小。
总之,我认为,设计模式主要目的就是通过oop优化我们的代码结构,减少耦合,易于维护。我们的项目中使用没使用到设计模式其实对于功能而言是无所谓的,这更多体现的是开发者的软实力和精益求精、追求完美的性格。每种设计原则都是针对于一些场景,如果没有具体的业务,那我们单单在这里扯设计模式就是纸上谈兵,所以此贴长期更新,如果有更好的案例我会进行替换,也欢迎大家一起讨论!