【Java设计模式】一、软件设计的七个原则
设计模式即一种解决问题的最佳实践。
0、UML图
Unified Modeling Language,UML,统一建模语言。图形化表达软件设计的动态与静态信息。下面用UML中的类图来表示类的静态信息:

以上的等价代码:
public class Employee{
private String name;
private int age;
private String address;
public void work(){
//...
}
}
用UML的类图表示类与类之间的关系:
关联关系
- 单向关联:如老师类中有个属性类型为学生类,顾客有个地址属性:

- 双向关联:两个类各自持有对方类型的成员变量

- 自关联,如Node类包含Node类型的成员变量

聚合关系
强关联关系,成员对象是整体对象的一部分,但成员对象可以脱离整体对象独立存在。如大学类和老师类,大学没了,老师可再找工作。空心菱形表示,且空心菱形一侧是整体的类

组合关系
更强烈的一种聚合关系,强烈体现在:整体对象不存在的化,部分对象也将不存在,如Head类和Mouth类。用实心菱形表示:

依赖关系
一个类的方法通过局部变量或者形参来访问另一个类的某个方法。如Driver类有形参Car car,并调用了car类的move方法

继承关系
实线 + 实线箭头从子类指向父类:

实现关系
虚线+虚线箭头
1、开闭原则
- 对扩展开放,对修改关闭
- 需求拓展时,尽量不改旧代码,热插拔的效果
- 实现思路:使用抽象类或接口进行构建,使用实现类扩展细节
- 面向抽象编程
- 提高了可扩展性和可维护性
以搜狗输入法支持切换各种主题皮肤为例,皮肤对象就是背景图片、颜色、字体等属性的组合。思路是:定义个抽象类,具体皮肤是其子类。搜狗输入法注入这个抽象类的对象来进行展示。类图:

皮肤的抽象类:
public abstract class AbstractSkin {
/**
* 展示输入法皮肤
*/
public abstract void display();
}
实现类:
/**
* 默认皮肤类
*/
public class DefaultSkin extends AbstractSkin{
@Override
public void display() {
System.out.println("默认皮肤");
}
}
/**
* 自定义皮肤一
*/
public class AnotherSkin extends AbstractSkin{
@Override
public void display() {
System.out.println("自定义皮肤一");
}
}
搜狗输入法,聚合抽象类,展示皮肤:
/**
* 搜狗输入法
* 通过成员变量,聚合抽象类
*/
public class SouGouInput {
private AbstractSkin skin; //聚合抽象类
/**
* 提供set,方便注入和初始化赋值,当然用构造方法来初始抽象类对象也可
*/
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
public void display(){
skin.display();
}
}
客户端模拟:
/**
* 客户端
*/
public class ClintTest {
public static void main(String[] args) {
//搜狗输入法对象
SouGouInput souGouInput = new SouGouInput();
//创建皮肤对象
DefaultSkin defaultSkin = new DefaultSkin();
//将皮肤设置到输入法中
souGouInput.setSkin(defaultSkin);
//展示
souGouInput.display();
}
}

此后,再扩展新皮肤,写实现类即可。
2、里氏替换原则
是继承复用的基石(对子类的继承关系进行约束),是对开闭原则的补充。里氏替换原则即:任何父类可以出现的地方,子类一定可以出现。比如某个方法的形参类型是父类A,那传个A的子类B也要行得通。为此,开发时,建议:
- 子类可以扩展父类的功能,但尽量不要重写父类原有的功能
- 重继承和复用,轻覆盖
- 子类可以实现父类的抽象方法,但是不能覆盖父类原有的方法
- 子类中可以增加自己特有的方法
以长方形和正方形为例:正方形属于长方形的一种,因此首先想到继承:
/**
* 长方形
*/
public class Rectangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
注意这里正方形做为子类,重写了父类的set方法
/**
* 正方形
*/
public class Square extends Rectangle{
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setLength(width);
}
}
写测试类,定义一个扩宽的方法:
public class RectangleDemo {
public static void main(String[] args) {
//长方形
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
resize(r);
printLengthAndWidth(r);
//正方形
System.out.println("=====");
Rectangle r2 = new Square();
r2.setWidth(10);
resize(r2);
printLengthAndWidth(r2);
}
/**
* 扩宽方法
* 当宽小于等于长时,宽+1
*/
public static void resize(Rectangle rectangle){
while(rectangle.getWidth() <= rectangle.getLength()){
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
/**
* 打印长方形的长和宽
*/
public static void printLengthAndWidth(Rectangle rectangle){
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
}
类图:


发现传入长方形正常,传入正方形则死循环,因为正方形这个子类重,set被重写,宽 <= 长始终满足。不满足里氏替换了。关于这个问题的改进:引入一个抽象的四边形接口Quadrilateral:

/**
* 四边形
*/
public interface Quadrilateral {
public double getLength();
public 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;
}
}
四边形只有get方法,正方形的set方法属于是新增的方法
/**
* 正方形
*/
public class Square implements Quadrilateral{
private double side;
@Override
public double getLength() {
return this.side;
}
@Override
public double getWidth() {
return this.side;
}
public void setSide(double side) {
this.side = side;
}
}
测试类:
public class RectangleTest {
public static void main(String[] args) {
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
resize(r);
printLengthAndWidth(r);
Square square = new Square();
square.setSide(10);
printLengthAndWidth(square);
}
/**
* 扩宽方法
* 当宽小于等于长时,宽+1
*/
public static void resize(Rectangle rectangle){
while(rectangle.getWidth() <= rectangle.getLength()){
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
/**
* 打印长方形的长和宽
*/
public static void printLengthAndWidth(Quadrilateral quadrilateral){
System.out.println(quadrilateral.getLength());
System.out.println(quadrilateral.getWidth());
}
}
对于resize方法,形参为长方形,正方形不再满足,当然也可instance of + 形参类型是四边形。总之:里氏替换要求父类可满足的,子类也可满足,为了达到这一点,coding时,对于父类已经实现的方法,尽量避免重写,以防违背里氏替换原则,带来Bug。
3、依赖倒转原则
高层模块不应该直接依赖底层模块,而是应该依赖其抽象。

- 是开闭原则的一种落地
- 面向接口进行编程
- 依赖抽象,而不是具体的实现
- 减少类与类之间的耦合性,提高系统稳定性,降低修改程序所造成的风险
以组装电脑为例,配件有cpu、硬盘、内存条。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。能想到的最直接的实现的类图:

类图翻译代码:

定义电脑类,其聚合硬盘、内存、CPU等类:
@Data
public class Computer{
private XiJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory;
//组装运行电脑
public void run(){
System.out.println("计算机工作");
cpu.run();
memory.save();
String data = hardDisk.get();
System.out.println("从硬盘获取的数据为:" + data);
}
}
测试类:

很明显,这么写,是直接依赖了其底层模块,而不是依赖抽象。当要换个厂商的CPU或者硬盘,就得大改Computer代码。改进方案的类图(对下游类抽取成一个接口):

部分代码:
//硬盘、内存以此类推
public interface Cpu{
public void run();
}
后续换不同的厂商,就写不同的实现类,而Computer自然要去依赖下游类的抽象(即Cpu接口,而非IntelCpu类):
@Data
public class Computer{
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
//组装运行电脑
public void run(){
System.out.println("计算机工作");
cpu.run();
memory.save();
String data = hardDisk.get();
System.out.println("从硬盘获取的数据为:" + data);
}
}
测试类:
public class Test{
//创建计算机组件对象
HardDisk hardDisk = new SiJieHardDisk();
Cpu cpu = new IntelCpu();
Memory memory = new KingstonMemory();
//创建计算机对象
Computer c = new Computer();
//组装计算机
c.setCpu(cpu);
c.setHardDisk(hardDisk);
c.setMemory(memory);
//运行计算机
c.run();
}
4、接口隔离原则
客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。

因此:
- 用多个接口,而不是使用单一的接口
- 尽量细化接口,接口中的方法尽量少
- 符合低耦合的设计思想,提高了可扩展性和可维护性
举个实际例子:安全门具有防火、防水、防盗功能,如果将三个功能对应的方法都放在一个接口,写的实现类如下:

此时,有个品牌只支持防火和防水,直接继承SafetyDoor就会出问题。根据接口隔离原则,可拆成三个接口,类图如下:

5、迪米特原则
如果两个对象或者软件实体无需直接通信,那就不应当发生直接的掉哦那个,可以通过第三方转发该调用,已达到解耦的目的。如需要做软件的公司应该去找开发软件的公司,而不是工程师个人。

- 一个对象应该对其他对象保持最少的了解(最少知道原则)
- 为了降低类与类之间的耦合
- 强调只和朋友交流,不和陌生人说话
- 这里的朋友指的是当前对象的成员变量对象、当前对象的方法的输入或输出参数,它们与当前对象存在关联、聚合、组合关系,可直接访问这些对象的方法。
案例一:开发者 --> 领导 --> 老板
实现思路:
- 需要和老板类的对象直接通信的是领导类的对象,这里以形参的方式
- 需要和领导类的对象直接通信的是开发者类的对象,这里以成员变量的形式
//人
public interface Human {
}
//代码开发者
public abstract class Programmer implements Human {
public abstract void work();
}
public class JavaProgrammer extends Programmer {
@Override
public void work() {
System.out.println("Java程序员在做开发工作");
}
}
public class PythonProgrammer extends Programmer {
@Override
public void work() {
System.out.println("Python程序员在做开发工作");
}
}
//管理者
public interface Manager extends Human{
}
老板类:
public class Boss implements Manager{
public void meet(TeamLeader teamLeader){ //需要和老板类的对象直接通信的是领导类的对象,这里以形参的方式
System.out.println("老板开会,分布任务");
//teamLeader.assginTasks();
}
}
项目经理类:
public class TeamLeader implements Manager{
private Programmer programmer; //需要和领导类的对象直接通信的是开发者类的对象,这里以成员变量的形式。
public void setProgrammer(Programmer programmer){
this.programmer = programmer;
}
public void assginTasks(){
System.out.println("给下属分配任务");
if(this.programmer instanceof JavaProgrammer){
//是Java开发者
(JavaProgrammer)programmer.work();
}else if(this.programmer instanceof PythonProgrammer){
//是Python开发者
(PythonProgrammer)programmer.work();
}
}
}
public class Test{
public static void main(String[] args){
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
JavaProgrammer javaProgrammer = new JavaProgrammer();
PythonProgrammer pythonProgrammer = new PythonProgrammer();
//老板只负责通知项目经理开会
boss.meet(teamLeader);
//项目经理根据会议内容进行任务分配
teamLeader.setProgrammer(javaProgrammer);
teamLeader.assginTasks();
teamLeader.setProgrammer(pythonProgrammer);
teamLeader.assginTasks();
}
}
Boss只与TeamLeader有关,TeamLeader则只与Programmer有关

案例二:明星 --> 经纪人 --> 粉丝
明星许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的"朋友",而粉丝和媒体公司是"陌生人",所以适合使用迪米特法则

明星类:
public class Star {
private String name;
public Star(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
粉丝类:
public class Fans {
private String name;
public Fans(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
媒体公司类:
public class Company {
private String name;
public Company(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
经纪人类,是粉丝、明星的"朋友",在这里进行聚合:
public class Agent {
private Star star;
private Fans fans;
private Company company;
public void setStar(Star star) {
this.star = star;
}
public void setFans(Fans fans) {
this.fans = fans;
}
public void setCompany(Company company) {
this.company = company;
}
public void meeting() {
System.out.println(fans.getName() + "与明星" + star.getName() + "见面了。");
}
public void business() {
System.out.println(company.getName() + "与明星" + star.getName() + "洽淡业务。");
}
}
6、合成复用原则
- 尽量使用对象组合,而不是使用继承关系,以达到代码复用的目的
- 如此就降低了类与类之间的耦合,一个类的变化对其他类造成的影响较小
这里的对象组合,指的是前面类图重的组合和聚合关系,通过成员变量或者形参等等,将已有的对象纳入新对象重,新对象通过已有对象类型的属性也可以调用已有类的方法。这比继承好,依赖继承破坏看封装性,二来有耦合,父类变,子类也得变。举个例子,车按动力分为汽油、电动。按颜色分为红黑白等。如果用继承来实现就是:

可以看到继承复用会产生很多子类,考虑用聚合复用,把颜色类做为属性(成员变量),明显更优:

如果现在需求变更,有个光能汽车,对继承复用就得新增三个类:

对聚合复用则只需增加一个类即可,因此日常开发优先考虑聚合复用。

7、单一原则
- 一个类、一个接口、一个方法,只去负责一项职责
- 降低程序复杂度以及代码变更带来的风险
更多推荐



所有评论(0)