软件设计原则
简介
什么是软件设计原则?是一组帮助我们避开不良设计的指导方针。
是由 Robert Martin 在《敏捷软件开发:原则、模式与实践》一书中整理收集而来。
应该避免不良设计的以下三个重要特点:
- 僵化: 很难做改动,因为每一个细微的改动都会影响到系统大量的其他功能
- 脆弱: 每当你做一次改动,总会引起系统中预期之外的部分出现故障
- 死板: 代码很难在其他应用中重用,因其不能从当前应用中单独抽离出来
何为设计模式的原则
- 程序员在编写程序的时候,应当遵守的准则
- 设计模式的基础,依据
- 不是孤立存在的,相互依赖的,相互补充的,每一种都很重要
1.开放封闭原则
官方定义:软件实体,如类、模块和函数,应该对扩展开放,对修改关闭
对扩展开放——提供方,对修改关闭——调用方
如何达到效果?
需要使用抽象类或接口,因为抽象灵活性好。适用性广,只要抽象的合理,可以基本保持软件架构的稳定。
而软件中异变的细节可以从抽象派生来的实现类进行扩展,当软件需要发生变化时,只需求重新派生实现类来扩展即可;
案例
//皮肤抽象类
abstract class Skin{
//打印方法
public abstract void display();
}
//实现类1
class DefaultSkin extends Skin{
@Override
public void display() {
System.out.println("默认皮肤");
}
}
//实现类2
class KeFengSkin extends Skin{
@Override
public void display() {
System.out.println("克峰皮肤");
}
}
//搜狗输入法
class Sougoul{
private Skin skin;
public void setSkin(Skin skin){
this.skin=skin;
}
public void display(){
skin.display();
}
}
//测试类
public class Test {
public static void main(String[] args) {
Sougoul sougoul=new Sougoul(); //创建搜狗输入法对象
Skin skin=new DefaultSkin(); //创建皮肤对象
sougoul.setSkin(skin); //将皮肤设置到输入法中
sougoul.display(); //显示皮肤
}
}
备注:最基础最重要的设计原则,可以提高复用性和可维护性。
2.单一职责原则
官方定义:一个类,应该只有一个需要进行修改的原因
有且仅有一个原因引起类的变更,顾名思义就是各司其职。适用于: 类
、 接口
、 方法
。
如何遵守?其实就是职责分解,关键从业务出发从需求出发,识别出同一个类型的职责。
优点&细节
-
降低类的复杂度
(职责少了,相应的复杂度也就降低了)
-
提高可读性,可维护性
(相应的复杂度降低了,代码量就会减少,可读性就提高了,可维护性自然也就提高了)
-
降低变更引起的风险
(一个类职责越多,变更的可能性就越大,带来的风险页就越大)
-
通常情况下,我们应当遵守类级别的单一职责原则
(只有逻辑足够简单,才可以在代码中违反单一原则)
案例
下图的 XXXDao
类,负则User表的增删改,同时又负则Order表的增删改,违反了单一职责
根据原则,将 XXXDao
的力度进行分解成两个单一职责的类
注意:单一职责不是面向对象语言特有的,只要是模块化的程序设计,都要遵守。
3.接口隔离原则
官方定义
客户端不应该被强制依赖他们不需要使用的接口
类间的依赖应该建立在最小接口上
通俗来说,不要再一个接口里面定义过多的方法,接口应该尽量细化。
优点
- 提高系统的灵活性和可维护性
- 提高了系统的内聚性,减少了对外交互,降低了系统的耦合性
- 能减少项目工程中的代码冗余
案例
//行为
interface behavior{
void Eating(); //进食
void cannon(); //跑
void fly(); //飞行
}
//鸟类
class bird implements behavior{
@Override
public void Eating() {}
@Override
public void cannon() {}
@Override
public void fly() {}
}
//狗类
class dog implements behavior{
@Override
public void Eating() {}
@Override
public void cannon() {}
@Override
public void fly() {}
}
很明显上面案例不符合 接口隔离原则
,因为它们都存在着用不到的方法,比如狗类的飞行,但由于实现了接口 behavior
,所以也 必须要实现这些用不到的方法。这里把接口尽量细化后的效果如下:
//进食
interface eat{
void Eating(); //进食
}
//飞禽
interface Birds{
void fly(); //飞
}
//犬科
interface Canidae{
void cannon(); //跑
}
//鸟类
class bird implements eat,Birds{
@Override
public void Eating() {}
@Override
public void fly() {}
}
//狗类
class dog implements eat,Canidae{
@Override
public void Eating() {}
@Override
public void cannon() {}
}
小结:就是当我一个类通过接口依赖(使用)另一个类的时候,要保证依赖的该接口是最小的,接口里面有方法用不到的,就进行隔离,而做法就是对原来接口进行拆分为最小粒度,来避免耦合;
与单一职责对比
相同点:都要求对结构进行拆分,都要求更小粒度,都希望减少耦合。
不同点:审视角度不同,单一职责,注重的是职责,而接口隔离,要求尽量使用多个专门的接口,注重的是接口设计。
使用接口隔离原则进行接口拆分的时候,要遵循单一职责原则
4.依赖倒置原则
官方定义
也称为DIP原则,上层模块不应该依赖底层模块,它们都应该依赖于抽象
抽象不应该依赖细节,细节应该依赖抽象
抽象:接口或者抽象类,细节:实现类
核心理念:相对于细节来说,抽象要稳定得多
要求我们面向接口编程:也就是,对抽象类和接口的设计
传递的三种方式
通过接口传递
interface IMessage{
void sendMessage(Produce produce);
}
//消息生产者
interface Produce{
void produceMessage();
}
//工作人员类
class Worke implements IMessage{
@Override
public void sendMessage(Produce produce) {
produce.produceMessage();
}
}
通过构造方法传递
interface IMessage{
void sendMessage();
}
//消息生产者
interface Produce{
void produceMessage();
}
//工作人员类
class Worker implements IMessage{
public Produce produce;
//构造器传递
public Worker(Produce produce) {
this.produce=produce;
}
@Override
public void sendMessage() {
this.produce.produceMessage();
}
}
通过set方法传递
interface IMessage{
void sendMessage();
}
//消息生产者
interface Produce{
void produceMessage();
}
//工作人员类
class Worker implements IMessage{
public Produce produce;
public void setProduce(Produce produce) {
this.produce=produce;
}
@Override
public void sendMessage() {
this.produce.produceMessage();
}
}
案例
需求:假设有一个场景,现在有一个工作人员,收发钉钉消息
//测试类
public class InversionDemo{
public static void main(String[] args) {
new Worker().getMessage(new DingDing());
new Worker().getMessage(new WeChat());
}
}
//引入接口,制定消息规范
interface IMessage{
void SendMessage();
}
class Worker{
//公共接收消息,依赖接口
public void getMessage(IMessage iMessage) {
iMessage.SendMessage();
}
}
//钉钉类
class DingDing implements IMessage{
//发送消息
@Override
public void SendMessage() {
System.out.println("钉钉上,老板找你加班啦");
}
}
//微信类
class WeChat implements IMessage{
//发送消息
@Override
public void SendMessage() {
System.out.println("钉钉不回,老板打你微信电话");
}
}
小结:本质上就是通过抽象(抽象类和接口)使得各个类或模块实现彼此独立,互相不影响,实现模块间耦合,要先顶层再细节得方式进行代码细节设计
注意事项&细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好
- 变量得声明类型尽量是抽象类或接口,这样我们得变量引用和实际对象间,就存在一个缓冲量,利于程序扩展和优化
- 继承时遵循里式替换原则
5.里氏替换原则
官方定义:所有引用基类得地方必须透明地使用其子类对象
通俗来说:子类可以扩展父类得功能,但是子类不能修改父类得功能,就是给继承性的使用制定了规范
继承优势
- 提高代码得复用性(每个子类拥有父类得属性和方法)
- 提高代码可扩展性
继承劣势
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
- 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
- 增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。
继承是把双刃剑-->如何正确合理使用继承呢?——>里式替换原则
案例
需求:有一个计算机(父类)可以完成加减,定义其子类
//测试类
public class InversionDemo{
public static void main(String[] args) {
int result = new Calculator().add(3, 5);
System.out.println(result);
int mul = new SuperCalculator().mul(3, 5);
System.out.println("两数相加之和,与100求差的值为:"+mul);
}
}
//创建一个更加基础的基类(定义更加基础的成员或者功能)
class Base{}
class Calculator extends Base{
//定义加法
public int add(int a,int b) {
return a+b;
}
//定义减法
public int sub(int a,int b) {
return a-b;
}
}
class SuperCalculator extends Base{
//变为依赖关系
private Calculator calculator=new Calculator();
//增补需求,两数相加再加5
public int add(int a,int b) {
return a+b+5;
}
//两数相加之和,与100求差
public int mul(int a,int b) {
int count=calculator.add(a, b);
return 100-count;
}
}
小结:之间继承Calculator 类会无意间重写父类add方法,需要他俩继承同一个基类
注意事项
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
- 子类中可以扩展自己的方法
- 里式替换原则并非让我们避免使用继承
- 里式替换原则是实现开闭原则的重要方法之一
6.迪米特法则
官方定义
所以迪米特法则又叫做最少知识原则
只与直接的朋友通信
一个对象应该对其他对象有最少的了解
什么是朋友?只要两个对象有依赖关系,朋友关系
什么是直接朋友?
- 成员变量
- 方法的参数类型
- 方法的返回值类型
案例
需求:应该学校,下属有各个学院和总部,现要求打印总部员工的ID和学院员工的ID
//客户端
public class Demo{
public static void main(String[] args) {
new SchoolManager().printAIIEmployee(new CollegeManager());
}
}
//总部员工的基类
class SchoolEmployee{
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
//学校员工的基类
class CollegeEmployee{
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
//学院管理类
class CollegeManager{
//获取学院员工
public List<CollegeEmployee> getCollegeEmployees(){
ArrayList<CollegeEmployee> collegeEmployees =new ArrayList<>();
for (int i = 0; i <10; i++) {
CollegeEmployee collegeEmployee=new CollegeEmployee();
collegeEmployee.setId("学院员工,ID是"+i);
collegeEmployees.add(collegeEmployee);
}
return collegeEmployees;
}
//打印学院员工
public void printollegeEmployee(CollegeManager collegeManager) {
List<CollegeEmployee> collegeEmployees = collegeManager.getCollegeEmployees();
for (CollegeEmployee employee:collegeEmployees) {
System.out.println(employee.getId());
}
}
}
//总部管理类
class SchoolManager{
//获取总部员工
public List<SchoolEmployee> getSchoolEmployees(){
ArrayList<SchoolEmployee> schoolEmployees =new ArrayList<>();
for (int i = 0; i <10; i++) {
SchoolEmployee schoolEmployee=new SchoolEmployee();
schoolEmployee.setId("总部员工,ID是"+i);
schoolEmployees.add(schoolEmployee);
}
return schoolEmployees;
}
//打印方法
public void printAIIEmployee(CollegeManager collegeManager) {
//打印总部员工
List<SchoolEmployee> schoolEmployees = this.getSchoolEmployees();
for (SchoolEmployee employee:schoolEmployees) {
System.out.println(employee.getId());
}
System.out.println("---------------------------------------------");
//打印学院员工
collegeManager.printollegeEmployee(collegeManager);
}
}
类的依赖关系:两个类?具有依赖关系?凡是类中用到了对方
注意事项
- 核心是降低类之间的耦合
- 从被依赖者的角度来说,尽量将逻辑封装在类的内部,对外除了提供的public方法,不泄露任何信息
- 从依赖者的角度来说,只依赖应该依赖的对象
- 切忌不要为了用而用
7.合成复用原则
官方定义:尽量使用组合/聚合的方式,而不是使用继承
案例
需求:现在假设有一个A类,里面有两个方法,有一个类B,想要复用这两个方法,请问有几种方案
方式一:继承
方式二:组合或者聚合(业务逻辑)
方式三:依赖关系
设计原则总结
- 开闭原则:要求对扩展开发,对修改关闭
- 里氏替换:不要破坏继承关系
- 依赖倒置原则:要求面向接口编程
- 单一职责原则:实现类职责单一
- 接口隔离原则:在设计接口的时候要简单单一
- 迪米特法则:只与直接的朋友的通信
- 合成复用原则:尽量使用组合和聚合的方式,二不是使用继承
核心思想
- 找出应用中可能需要变化之处,独立出来,不要和不需要变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 为了交互对象的松耦合设计二努力
遵循设计原则·就是为了让程序高内聚,地耦合