设计模式学习(一):设计原则
设计模式学习(一):设计原则
作者:Grey
原文地址:
UML 类图关系
可以用下面图来表示类的各种关系
-
聚合关系是非强依赖的,而组合关系是强依赖的。
-
继承关系中,实现关系是继承的抽象类,泛化关系是继承的非抽象类。
-
依赖关系是动态关系,而关联关系是静态关系。
开闭原则
对扩展开放,对修改关闭,降低维护带来的新风险
先看一个不符合开闭原则的代码,每次新增一种类型,都需要增加一个if
条件判断(修改),如下代码
public class GraphicEditor {
public void draw(Shape shape) {
if (shape.mType == 1) {
drawRectangle();
} else if (shape.mType == 2) {
drawCircle();
}
// ... 每次增加一种类型,这里的代码就要加一个if分支。不符合开闭原则
}
public void drawRectangle() {
System.out.println("画长方形");
}
public void drawCircle() {
System.out.println("画圆形");
}
class Shape {
int mType;
}
class Rectangle extends Shape {
Rectangle() {
super.mType = 1;
}
}
class Circle extends Shape {
Circle() {
super.mType = 2;
}
}
}
要调整为符合开闭原则的代码,需要先将 Shape 抽象为接口,GraphicEditor 中的 draw()
方法中直接传入 Shape 这个接口类型,并调用其 draw()
方法,每次增加一种 Shape 类型,只需要新增一个类,并实现 Shape 接口即可,这样就实现了对扩展开放,而且也不需要修改 GraphicEditor 中的 draw()
方法内容,实现了对修改关闭,修改后的代码如下。
public class GraphicEditor1 {
public void draw(Shape shape) {
shape.draw();
}
interface Shape {
void draw();
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("画矩形");
}
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("画圆形");
}
}
}
依赖倒置
高层不应该依赖低层,这样更利于代码结构的升级扩展
先看一段不符合依赖倒置的代码,driver
方法参数里面写了具体的车的实现,如果以后要换一种车,只能修改代码实现。
public class CarTest {
private static class Benz {
public void run() {
System.out.println("奔驰跑起来了!");
}
}
private static class Driver {
private String name;
public Driver(String name) {
this.name = name;
}
// driver方法参数里面写了具体的车的实现,如果以后要换一种车,只能修改代码实现。
// 不符合依赖倒置原则
public void driver(Benz benz) {
benz.run();
}
}
public static void main(String[] args) {
Benz benz = new Benz();
Driver driver = new Driver("张三");
driver.driver(benz);
}
}
调整后的代码如下,因为 driver()
方法的参数变成了抽象的 ICar 类型,所以,所有实现 ICar 类型的车都可以调用这个方法,这样就实现了高层不依赖低层。
public class CarTest1 {
public static void main(String[] args) {
IDriver driver = new Driver();
driver.driver(new Benz());
driver.driver(new BMW());
}
private interface ICar {
void run();
}
public static class Benz implements ICar {
public void run() {
System.out.println("奔驰跑起来了!");
}
}
public static class BMW implements ICar {
public void run() {
System.out.println("宝马跑起来了!");
}
}
private interface IDriver {
void driver(ICar car);
}
public static class Driver implements IDriver {
@Override
public void driver(ICar car) {
car.run();
}
}
}
单一职责
一个类只干一件事 便于理解,提高代码的可读性
举个例子,一般来说,系统有登录,注册,注销的功能,可以写成如下形式
public interface UserOperate {
void login(UserInfo userInfo);
void register(UserInfo userInfo);
void logout(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void login(UserInfo userInfo) {
// 用户登录
}
@Override
public void register(UserInfo userInfo) {
// 用户注册
}
@Override
public void logout(UserInfo userInfo) {
// 用户登出
}
}
以上设计针对各个方法都不是很复杂的情况,如果每个方法都比较复杂,可以考虑独立成类来做,示例如下
public interface Register {
void register();
}
public interface Login {
void login();
}
public interface Logout {
void logout();
}
public class RegisterImpl implements Register{
@Override
public void register() {
// 用户注册
}
}
public class LoginImpl implements Login{
@Override
public void login() {
// 用户登录
}
}
public class LogoutImpl implements Logout{
@Override
public void logout() {
// 登出
}
}
这就实现了单一职责的原则。
接口隔离
一个接口只干一件事,功能解耦,高聚合,低耦合
举个例子,学生成绩管理程序一般包含查询成绩、新增成绩、删除成绩、修改成绩、计算总分、计算平均分、打印成绩信息等功能,如果都写在一个类里面,就会变成如下形式
public interface IStudentScore {
// 查询成绩
public void queryScore();
// 修改成绩
public void updateScore();
// 添加成绩
public void saveScore();
// 删除成绩
public void delete();
// 计算总分
public double sum();
// 计算平均分
public double avg();
// 打印成绩单
public void printScore();
}
这样的方式不利于扩展. 比如:
学生只有查看成绩,打印成绩单的权限, 没有增删改的权限;
老师拥有所有的权限。
采用接口隔离原则设计,可以做如下改进
public interface IQueryScore {
// 查询成绩
public void queryScore();
// 打印成绩单
public void printScore();
}
public interface IOperateScore {
// 修改成绩
public void updateScore();
// 添加成绩
public void saveScore();
// 删除成绩
public void delete();
// 计算总分
public double sum();
// 计算平均分
public double avg();
}
分为查询接口和操作接口,这样学生端就不需要重写和他不相关的接口了。
迪米特法则
不该知道的不要知道,减少代码臃肿
示例:一个人用咖啡机煮咖啡的过程,例子中只有两个类,一个是人,一个是咖啡机。
首先是咖啡机类 CoffeeMachine ,咖啡机制作咖啡只需要三个方法
第一步:加咖啡豆;
第二步:加水;
第三步:制作咖啡。
如果把咖啡机的所有方法暴露给人调用,就会出现如下代码
/**
* 咖啡机抽象接口
*/
public interface ICoffeeMachine {
//加咖啡豆
void addCoffeeBean();
//加水
void addWater();
//制作咖啡
void makeCoffee();
}
/**
* 咖啡机实现类
*/
public class CoffeeMachine implements ICoffeeMachine{
//加咖啡豆
public void addCoffeeBean() {
System.out.println("放咖啡豆");
}
//加水
public void addWater() {
System.out.println("加水");
}
//制作咖啡
public void makeCoffee() {
System.out.println("制作咖啡");
}
}
/**
* 人, 制作咖啡
*/
public interface IMan {
/**
* 制作咖啡
*/
void makeCoffee();
}
/**
* 人制作咖啡
*/
public class Man implements IMan {
private ICoffeeMachine coffeeMachine;
public Man(ICoffeeMachine coffeeMachine) {
this.coffeeMachine = coffeeMachine;
}
/**
* 制作咖啡
*/
public void makeCoffee() {
coffeeMachine.addWater();
coffeeMachine.addCoffeeBean();
coffeeMachine.makeCoffee();
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
ICoffeeMachine coffeeMachine = new CoffeeMachine();
IMan man = new Man(coffeeMachine);
man.makeCoffee();
}
}
其实人根本不关心咖啡机具体制作咖啡的过程。所以我们可以作如下优化:
优化后的咖啡机类,只暴露一个 work()
方法,把制作咖啡的三个具体的方法addCoffeeBean()
、addWater()
、makeCoffee()
设为私有,work()
方法封装了这三个具体方法的实现。
/**
* 咖啡机抽象接口
*/
public interface ICoffeeMachine {
//咖啡机工作
void work();
}
/**
* 咖啡机实现类
*/
public class CoffeeMachine implements ICoffeeMachine {
//加咖啡豆
private void addCoffeeBean() {
System.out.println("放咖啡豆");
}
//加水
private void addWater() {
System.out.println("加水");
}
//制作咖啡
private void makeCoffee() {
System.out.println("制作咖啡");
}
@Override
public void work() {
addCoffeeBean();
addWater();
makeCoffee();
}
}
/**
* 人, 制作咖啡
*/
public interface IMan {
/**
* 制作咖啡
*/
void makeCoffee();
}
/**
* 人制作咖啡
*/
public class Man implements IMan {
private ICoffeeMachine coffeeMachine;
public Man(ICoffeeMachine coffeeMachine) {
this.coffeeMachine = coffeeMachine;
}
/**
* 制作咖啡
*/
public void makeCoffee() {
coffeeMachine.work();
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
ICoffeeMachine coffeeMachine = new CoffeeMachine();
IMan man = new Man(coffeeMachine);
man.makeCoffee();
}
}
通过减少 CoffeeMachine 对外暴露的方法,减少 Man 对 CoffeeMachine 的了解,从而降低了它们之间的耦合。
里氏替换原则
子类重写方法功能发生改变,不应该影响父类方法的含义,防止继承泛滥。
比如下述代码,就不符合里氏替换原则
class A{
public int func1(int a, int b){
return a-b;
}
}
public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
System.out.println("100-80="+a.func1(100, 80));
}
}
接下来需要增加一个新的功能:完成两数相加,然后再与 100 求和,由类 B 来负责。即类 B 需要完成两个功能:
-
两数相减。
-
两数相加,然后再加100。
由于类 A 已经实现了第一个功能,所以类 B 继承类 A 后,只需要再完成第二个功能就可以了,代码如下:
class B extends A{
public int func1(int a, int b){
return a+b;
}
public int func2(int a, int b){
return func1(a,b)+100;
}
}
public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
类B完成后,运行结果:
100-50=150
100-80=180
100+20+100=220
可以发现原本运行正常的相减功能发生了错误。原因就是类 B 在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类 A 完成的功能,换成子类 B 之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
UML 和 代码
更多
参考资料
本文来自博客园,作者:Grey Zeng,转载请注明原文链接:https://www.cnblogs.com/greyzeng/p/15775683.html