设计模式
[toc]
前言
设计模式只是解决问题的一种思想,它可以提高代码的复用性,可维护性,可读性,稳健性,安全性。
六大原则
1. 开闭原则( OCP )
当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。也就是对扩展开放,对修改关闭。
开闭原则实例
定义接口 Cup
/**
* 可以通过“抽象约束、封装变化”来实现开闭原则,
* 即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
* 列:
* 我们需要一个保温杯和一个茶杯。
* 保温杯的特点是保温。
* 茶杯的特点是可以过滤茶叶。
* 而他们相同的属性是可以装水。
*/
public interface Cup {
void water();
}
定义保温杯 ThermsCup,实现 Cup 接口
/**
* 定义茶杯,实现Cup接口,新增功能过滤茶叶
*/
public class TeaCup implements Cup{
@Override
public void water() {
System.out.println("接一杯热水");
}
/**
* 新增的过滤茶叶功能
*/
public void filterTea(){
System.out.println("过滤茶叶");
}
}
定义茶杯接口 TeaCup ,实现接口 Cup
/**
* 定义保温杯,实现Cup接口,并且新增保温功能。
*/
public class ThermsCup implements Cup{
@Override
public void water() {
System.out.println("接一杯热水");
}
/**
* 新增的保温功能
*/
public void therm(){
System.out.println("新增保温功能");
}
}
首先定义了杯子的公共属性,找到公共属性,然后在原有的公共属性上进行扩展而不修改原有属性。
开闭原则总结:
1. 对扩展开放,对修改关闭。满足新需求时通过扩展而不修改原来的代码。
2. 里氏替换原则( LSP )
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。子类可以扩展父类的方法,但不能修改父类原有的功能。在继承父类时,除了新添加的方法,尽量不要重写父类的方法。
如果通过重写父类方法来新增功能,这样的复用性会变得特别差,而且程序出错的概率也会增加。
实例
分析:鸟基本上都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是鸵鸟不会飞。假如要设计一个实例,计算这两种鸟飞行300千米需要多长时间。拿燕子测试结果正确,可以测试出飞行时间。拿鸵鸟结果会发生“除零异常”或是“无穷大”,明显不符合预期。
public class LSPTest {
public static void main(String[] args) {
Swallow swallow = new Swallow();
Ostrich ostrich = new Ostrich();
swallow.setFlySpeed(120);
ostrich.setFlySpeed(120);
System.out.println("如果飞行300公里");
try {
System.out.println("燕子飞行"+swallow.getFlyTime(300)+"小时");
System.out.println("鸵鸟飞行"+ostrich.getFlyTime(300)+"小时");
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 定义鸟类,可以设置鸟的速度和飞行距离。
*/
public class Bird {
double flySpeed;
/**
* 设置鸟的飞行速度
* @param flySpeed 飞行速度
*/
public void setFlySpeed(double flySpeed) {
this.flySpeed = flySpeed;
}
/**
* 获取鸟的飞行时间
* @param distance 飞行距离
* @return 所需时间
*/
public double getFlyTime(double distance){
return (distance / flySpeed);
}
}
/**
* 定义燕子类,继承鸟类Bird计算飞行时间。
*/
public class Swallow extends Bird{
}
/**
* 定义鸵鸟类,继承鸟类Bird。计算飞行时间
*/
public class Ostrich extends Bird{
/**
* 鸵鸟不会飞,设置每小时飞行速度为0
* @param flySpeed 飞行速度
*/
@Override
public void setFlySpeed(double flySpeed) {
super.flySpeed = 0;
}
}
打印结果
如果飞行300公里
燕子飞行2.5小时
鸵鸟飞行Infinity小时
程序运行错误的原因是:鸵鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。正确的做法是:取消鸵鸟原来的继承关系,定义鸟和鸵鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。
public class LSPTest {
public static void main(String[] args) {
Swallow swallow = new Swallow();
Ostrich ostrich = new Ostrich();
swallow.setFlySpeed(120);
ostrich.setRunSpeed(120);
System.out.println("如果飞行300公里");
try {
System.out.println("燕子飞行"+swallow.getFlyTime(300)+"小时");
System.out.println("鸵鸟奔跑"+ostrich.getRunTime(300)+"小时");
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 动物类
*/
public class Animal {
public double runSpeed;
/**
* 设置奔跑速度
* @param speed 速度
*/
public void setRunSpeed(double speed){
runSpeed = speed;
}
/**
* 获取奔跑时间
* @param distance 距离
*/
public double getRunTime(double distance){
return (distance / runSpeed);
}
}
/**
* 定义鸟类,可以设置鸟的速度和飞行距离。
*/
public class Bird {
double flySpeed;
/**
* 设置鸟的飞行速度
* @param flySpeed 飞行速度
*/
public void setFlySpeed(double flySpeed) {
this.flySpeed = flySpeed;
}
/**
* 获取鸟的飞行时间
* @param distance 飞行距离
* @return 所需时间
*/
public double getFlyTime(double distance){
return (distance / flySpeed);
}
}
/**
* 定义燕子类,继承鸟类Bird计算飞行时间。
*/
public class Swallow extends Bird{
}
/**
* 定义鸵鸟类,继承动物类Animal。计算飞行时间
*/
public class Ostrich extends Animal{
}
程序运行结果如下:
如果飞行300公里
燕子飞行2.5小时
鸵鸟奔跑2.5小时
里氏替换原则总结:
- 对开闭原则的补充。开闭原则的关键步骤是抽象化,而基类与子类的继承就是抽象化具体实现。
- 基类出现的地方其子类也一定可以出现,而子类出现的地方父类不一定可以出现。
3.依赖倒置原则( DIP )
依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。
使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。
作用:
- 降低类间的耦合
- 提高系统的稳定性
- 提高代码的可读性和维护性
实例
分析:本程序反映了 “顾客类”与“商店类”的关系。商店类中有 sell() 方法,顾客类通过该方法购物以下代码定义了顾客类通过淘宝网店 TaobaoShop 购物
public class Customer {
/**
* 错误写法
* @param taobaoShop 顾客要在哪个商店购物
*/
public void shopping(TaobaoShop taobaoShop){
System.out.println(taobaoShop.sell());
}
}
这种写法的缺点是顾客想从另外一家商店购物的话需要修改shopping()的形参。
public class Customer {
/**
* 修改形参
* @param taobaoShop 顾客要在哪个商店购物
*/
public void shopping(JingdongShop jingdongShop){
System.out.println(jingdongShop.sell());
}
}
顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。解决方法是:定义“淘宝网店”和“京东网店”的共同接口 Shop。
public class Customer {
/**
* 正确写法
* @param shop 商店的共同接口
*/
public void shopping(Shop shop){
System.out.println(shop.sell());
}
}
完整代码
public class Customer {
/**
* 正确写法
* @param shop 商店的共同接口
*/
public void shopping(Shop shop){
System.out.println(shop.sell());
}
}
public interface Shop {
String sell();
}
public class JingdongShop implements Shop{
@Override
public String sell() {
return "京东网店:电子产品";
}
}
public class TaobaoShop implements Shop{
@Override
public String sell() {
return "淘宝网店:衣服";
}
}
/**
* 依赖倒置原则测试类
*/
public class DIPTest {
public static void main(String[] args) {
Customer customer = new Customer();
System.out.println("顾客购买了以下商品");
customer.shopping(new JingdongShop());
customer.shopping(new TaobaoShop());
}
}
运行结果:
顾客购买了以下商品
京东网店:电子产品
淘宝网店:衣服`
总结:
- 针对接口编程,高层模块不依赖于底层模块,他们都应该依赖于抽象。Customer就属于高层模块,方法shopping的形参不应该依赖于Jingdong或者Taobao。而Jingdong和Taobao应该依赖于接口或抽象,shopping的形参也是这样。
4.单一职责( SRP )
单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
单一职责的优点
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。
- 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
- 提高类的可读性。复杂性降低,自然其可读性会提高。
- 提高系统的可维护性。可读性提高,那自然更容易维护了。
- 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
实例:
分析:生产一个App主要工作包括产品和程序员两种,其中产品包括需求、原型图、设计图。程序员是生产代码的主要包括代码编写、Bug修复。如果将这些工作都交给一个人干明显不合理,正确的做法就是产品的活交给产品经理,代码的活交给程序员。
/**
* 单一职责实例
*/
public class App {
/**
* 错误写法
* 造一个App只干有关的事情,不可以在造App的时候睡觉
*/
public void sleep(){
System.out.println("我要去睡觉");
}
public void demand(){
System.out.println("我写了一个需求文档");
}
public void prototype(){
System.out.println("我写了一个原型图");
}
public void degin(){
System.out.println("我写了一个设计图");
}
public void produceCode(){
System.out.println("我开始生产代码了");
}
public void fixBug(){
System.out.println("我要修复自己写的BUG");
}
}
我们在造一个App时候不可以睡觉,而且这些工作都交给一个人明显不合理。正确应该删掉睡觉,而且把工作分给程序员和产品。单一原则不只适用类,还适用方法。
完整代码
/**
* 单一职责实例
*/
public class App {
/**
* 正确写法
* 单一原则同样适用于方法
*/
public void programmer(Programmer programmer){
programmer.produceCode();
programmer.fixBug();
}
public void pm(PM pm){
pm.demand();
pm.prototype();
pm.degin();
}
}
public class PM {
public void demand(){
System.out.println("我写了一个需求文档");
}
public void prototype(){
System.out.println("我写了一个原型图");
}
public void degin(){
System.out.println("我写了一个设计图");
}
}
public class Programmer {
public void produceCode(){
System.out.println("我开始生产代码了");
}
public void fixBug(){
System.out.println("我要修复自己写的BUG");
}
}
public class SRPTest {
public static void main(String[] args) {
App app = new App();
System.out.println("生产App需要产品干这些事情");
app.pm(new PM());
System.out.println("产品干完这些事情就可以开始造App了");
app.programmer(new Programmer());
}
}
运行结果:
生产App需要产品干这些事情
我写了一个需求文档
我写了一个原型图
我写了一个设计图
产品干完这些事情就可以开始造App了
我开始生产代码了
我要修复自己写的BUG
总结:
- 一个类只做一件事情,不做与其无关的事情。
- 单一职责同样适用于方法。
5.接口隔离原则( ISP )
要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
接口隔离的优点:
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
实例:
分析:学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能,如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中。
public interface InputModule {
void insert();
void delete();
void modify();
}
public interface CountModule {
void countTotalScore();
void countAverage();
}
interface PrintModule {
void printStuInfo();
void queryStuInfo();
}
public class StuScoreList implements InputModule,CountModule,PrintModule{
public static InputModule getInputModule()
{
return (InputModule)new StuScoreList();
}
public static CountModule getCountModule()
{
return (CountModule)new StuScoreList();
}
public static PrintModule getPrintModule()
{
return (PrintModule)new StuScoreList();
}
@Override
public void countTotalScore() {
System.out.println("统计模块的countTotalScore()方法被调用!");
}
@Override
public void countAverage() {
System.out.println("统计模块的countAverage()方法被调用!");
}
@Override
public void insert() {
System.out.println("输入模块的insert()方法被调用!");
}
@Override
public void delete() {
System.out.println("输入模块的delete()方法被调用!");
}
@Override
public void modify() {
System.out.println("输入模块的modify()方法被调用!");
}
@Override
public void printStuInfo() {
System.out.println("打印模块的printStuInfo()方法被调用!");
}
@Override
public void queryStuInfo() {
System.out.println("打印模块的queryStuInfo()方法被调用!");
}
}
/**
* 接口隔离原则测试类
*/
public class ISPTest {
public static void main(String[] args) {
InputModule inputModule = StuScoreList.getInputModule();
CountModule countModule = StuScoreList.getCountModule();
PrintModule printModule = StuScoreList.getPrintModule();
inputModule.insert();
countModule.countTotalScore();
printModule.printStuInfo();
}
}
运行结果
输入模块的insert()方法被调用!
统计模块的countTotalScore()方法被调用!
打印模块的printStuInfo()方法被调用!
总结:
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
6.迪米特法则( LoD )
只与你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
实例:
分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则
public class LoDtest{
public static void main(String[] args){
Agent agent=new Agent();
agent.setStar(new Star("林心如"));
agent.setFans(new Fans("粉丝韩丞"));
agent.setCompany(new Company("中国传媒有限公司"));
agent.meeting();
agent.business();
}
}
//经纪人
class Agent{
private Star myStar;
private Fans myFans;
private Company myCompany;
public void setStar(Star myStar){
this.myStar=myStar;
}
public void setFans(Fans myFans){
this.myFans=myFans;
}
public void setCompany(Company myCompany){
this.myCompany=myCompany;
}
public void meeting(){
System.out.println(myFans.getName()+"与明星"+myStar.getName()+"见面了。");
}
public void business(){
System.out.println(myCompany.getName()+"与明星"+myStar.getName()+"洽淡业务。");
}
}
//明星
class Star{
private String name;
Star(String name){
this.name=name;
}
public String getName(){
return name;
}
}
//粉丝
class Fans{
private String name;
Fans(String name){
this.name=name;
}
public String getName(){
return name;
}
}
//媒体公司
class Company{
private String name;
Company(String name){
this.name=name;
}
public String getName(){
return name;
}
}
运行结果:
粉丝韩丞与明星林心如见面了。
中国传媒有限公司与明星林心如洽淡业务。