设计模式概述和七大软件设计原则

1. 设计模式概述

1.1 什么是设计模式

设计模式是基于面向对象的软件设计经验总结,是针对软件开发中常见问题和模式的通用解决方案。

1.2 常见的设计模式

① GoF设计模式

《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为四人组(Gang of Four)。

这种模式包括了23种,常用的为单例模式、工厂模式、代理模式、模板方法、适配器模式、观察者模式、策略模式。

② 架构设计模式(Architectural Pattern)

主要用于软件系统的整体架构设计,包括多层架构、MVC架构、微服务架构、REST架构和大数据架构等。

③ 企业级设计模式(Enterprise Pattern)

主要用于企业级应用程序设计,包括基于服务的架构(SOA)、企业集成模式(EIP)、业务流程建模(BPM)和企业规则引擎(BRE)等。

④ 领域驱动设计模式(Domain Driven Design Pattern)

主要用于领域建模和开发,包括聚合、实体、值对象、领域事件和领域服务等。

⑤ 并发设计模式(Concurrency Pattern)

主要用于处理并发性问题,包括互斥、线程池、管道、多线程算法和Actor模型等。

⑥ 数据访问模式(Data Access Pattern)

主要用于处理数据访问层次结构,包括数据访问对象(DAO)、仓库模式和活动记录模式等。

1.3 GoF设计模式的分类

  1. 创建型:主要解决对象的创建问题,GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者 5 种创建型模式。

  2. 结构型:通过设计和构建对象之间的关系,以达到更好的重用性、扩展性和灵活性。GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合 7 种结构型模式。

  3. 行为型:主要用于处理对象之间的算法和责任分配。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器 11 种行为型模式。

2. 软件开发七大设计原则

  1. 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
  2. 里氏替换原则,告诉我们不要破坏继承体系。
  3. 依赖倒置原则,告诉我们要面向接口编程。
  4. 单一职责原则,告诉我们实现类要职责单一。
  5. 接口隔离原则,告诉我们在设计接口的时候要精简单一。
  6. 迪米特法则,告诉我们要降低耦合度。
  7. 合成复用原则,告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。

2.1 开闭原则

对扩展开放,对修改关闭。该选择可以使程序的扩展性好,易于维护和升级。

对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。

对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

实现方式

可以通过“抽象约束、封装变化”来实现开闭原则。

即通过接口或者抽象类为实体,定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

2.2 里氏代换原则

子类可以扩展父类的功能,但不能改变父类原有的功能。

实现方式

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法**。**// 可以扩展,但不能改变

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。// 违背了里氏代换原则

如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。例如重新抽取一个公共的接口,使其实现该接口。

关于里氏替换原则的例子,最有名的是“正方形不是长方形”。当然,生活中也有很多类似的例子,例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。

例子–正方形不是长方形

在数学领域内,正方形可以被视为一种特殊的长方形,即长宽相等。所以我们在设计代码结构时,可以设计长方形为父类,正方形为子类

在这里插入图片描述

上述类图种,Square继承自Rectangle,如果我们尝试在代码中将Square对象替换为Rectangle对象,可能会引起问题。比如:

Rectangle rect = new Square();
rect.setLength(5);
rect.setWidth(10);

这段代码违反了正方形的定义,因为正方形的长和宽应该始终相等。但是由于Square继承自Rectangle,使得Square的行为无法完全符合Rectangle的预期,这就违反了里氏代换原则。

改进
  1. 引入了一个Quadrilateral(四边形)接口,Square类和Rectangle类都实现了这个接口。

  2. RectangleDemo类依赖Quadrilateral接口,而不是具体的Rectangle类。

  3. Square类不直接继承Rectangle类,而是独立实现Quadrilateral接口。

在这里插入图片描述

通过引入Quadrilateral四边形接口,SquareRectangle独立实现接口方法,确保了每个类都有各自的行为,实现了更好的设计。这样,无论是Square还是Rectangle对象,都可以作为Quadrilateral对象使用,这遵循了里氏代换原则。

Quadrilateral quad = new Square();
// 由于接口中没有设置宽度的方法,避免了违反正方形定义的情况
quad.setLength(5);

2.3 依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

面向接口编程,不要面向实现编程。

依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。

实现方式

  1. 每个类尽量提供接口或抽象类,或者两者都具备。
  2. 变量的声明类型尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则。

举例说明

以顾客去不同商店购物为例。

  1. 在顾客类中,定义一个shopping方法,参数列表传入对应的商店对象,然后完成购物。

    该场景中,每当顾客换一家商店,都需要改变参数对象,这很显然违背了开闭原则。

  2. 改进

    将不同的商店抽象为一个shop接口,使每个商店实现该接口。顾客在进入不同商店购买时,传入接口对象,在测试实现类中,再传入具体的new商店对象即可。

    public class DIPtest{
        public static void main(String[] args){
            Customer wang = new Customer();
            System.out.println("顾客购买以下商品:"); 
            wang.shopping(new Shop1()); 
            wang.shopping(new Shop2());
        }
    }
    //商店
    interface Shop{
        public String sell(); //卖
    }
    //网店1
    class Shop1 implements Shop{
        public String sell(){
            return "111--土特产:香菇、木耳……"; 
        } 
    }
    //网店2
    class Shop2 implements Shop{
        public String sell(){
            return "222--土特产:绿茶、酒糟鱼……"; 
        }
    } 
    //顾客
    class Customer{
        public void shopping(Shop shop){
            //购物
            System.out.println(shop.sell()); 
        }
    }
    

2.4 单一职责原则

控制类的粒度大小、将对象解耦、提高其内聚性。

实现方式

设计人员需要根据类的不同职责并将其分离,再封装到不同的类或模块中。

注意

单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。

如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。

举例说明

大学生工作主要包括学生生活辅导、学生学业指导

生活辅导——班委建设、出勤统计、心理辅导、费用催缴、班级管理等

学业指导——专业引导、学习辅导、科研指导、学习总结等

如果将这些工作交给一位老师负责显然不合理,正确的做法是生活辅导由辅导员负责,学业指导由学业导师负责

2.5 接口隔离原则

为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

面试题

接口隔离原则和单一职责原则的区别?

接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

  1. 单一职责原则注重的是职责;接口隔离原则注重的是对接口依赖的隔离。

  2. 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

实现方式

接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。

提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

为依赖接口的类只提供调用者需要的方法,屏蔽不需要的方法。

举例说明

学生成绩管理——插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等

如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个接口中

2.6 迪米特法则

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。

过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。

所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。

实现方式

只依赖应该依赖的对象

只暴露应该暴露的方法

举例说明

角色描述:

  • 明星:专注于艺术创作和表演,不直接处理日常事务。
  • 经纪人:负责处理明星的日常事务,与粉丝和媒体公司进行沟通。
  • 粉丝:希望与明星见面。
  • 媒体公司:希望与明星进行业务洽谈。

迪米特法则的应用:

  • 明星粉丝媒体公司实体间不直接通信)之间不直接交流,而是通过经纪人第三方调用)来处理相关事务。
  • 经纪人明星的朋友,可以直接与明星交流。
  • 粉丝媒体公司明星的陌生人,他们的请求和交流必须通过经纪人来完成。
package principle;
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;
    }
}

2.7 合成复用原则

尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

① 继承复用的缺点(优点是简单、易实现)

  1. ==继承复用破坏了类的封装性。==继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. ==子类与父类的耦合度高。==父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. ==它限制了复用的灵活性。==从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

② 组合或聚合复用优点

​ 该方式将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能

  1. ==它维持了类的封装性。==因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. ==新旧类之间的耦合度低。==这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. ==复用的灵活性高。==这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

③ 实现方式

通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。

④ 举例说明

汽车分类——

​ 按“动力源”划分可分为汽油汽车、电动汽车等;

​ 按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。

如果同时考虑这两种分类,其组合就很多。

使用继承

用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。

使用组合

在这里插入图片描述

// 颜色接口
interface Color {
    String getColor();
}

// 具体颜色实现类
class White implements Color {
    @Override
    public String getColor() {
        return "White";
    }
}

class Black implements Color {
    @Override
    public String getColor() {
        return "Black";
    }
}

class Red implements Color {
    @Override
    public String getColor() {
        return "Red";
    }
}

// 汽车基类
abstract class Car {
    protected Color color;

    public Car(Color color) {
        this.color = color;
    }

    public abstract void move();
}

// 汽油汽车类
class GasolineCar extends Car {
    public GasolineCar(Color color) {
        super(color);
    }

    @Override
    public void move() {
        System.out.println("Gasoline car with color " + color.getColor() + " is moving.");
    }
}

// 电动汽车类
class ElectricCar extends Car {
    public ElectricCar(Color color) {
        super(color);
    }

    @Override
    public void move() {
        System.out.println("Electric car with color " + color.getColor() + " is moving.");
    }
}

// 主类
public class Main {
    public static void main(String[] args) {
        Color white = new White();
        Color black = new Black();
        Color red = new Red();

        Car whiteGasolineCar = new GasolineCar(white);
        Car blackElectricCar = new ElectricCar(black);
        Car redGasolineCar = new GasolineCar(red);

        whiteGasolineCar.move();  // 输出: 白色汽油汽车正在移动
        blackElectricCar.move();  // 输出: 黑色电动汽车正在移动
        redGasolineCar.move();    // 输出: 红色汽油汽车正在移动
    }
}
  • 40
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
软件设计方案 用户界面设计规范 用户界面:又称人机界面,实现用户与计算机之间的通信,以控制计算机或进行用户与计算机之间的数据传送的系统部件。 GUI:即图形用户界面,一种可视化的用户界面,它使用图形界面代替正文界面。 本系统坚持图形用户界面(GUI)设计原则,界面直观、对用户透明。用户接触软件后对界面上对应的功能一目了然、不需要多少培训就可以方便地使用本应用系统。 1、界面设计介绍 界面设计是为了满足软件专业化标准化的需求而产生的对软件的使用界面进行美化优化规范化的设计分支。 1)软件启动封面设计 应使软件启动封面最终为高清晰度的图像,选用的色彩不宜超过256色,大小多为主流显示器分辨率的1/6大。启动封面上应该醒目地标注制作或支持的公司标志、产品商标、软件名称、版本号、网址、版权声明、序列号等信息,以树立软件形象,方便使用者或购买者在软件启动的时候得到提示。插图宜使用具有独立版权的、象征性强的、识别性高的、视觉传达效果好的图形,若使用摄影也应该进行数位处理,以形成该软件的个性化特征。如果是系列软件还将考虑整体设计的统一和延续性。 2)软件框架设计 软件的框架设计要复杂得多。软件框架设计应该简洁明快,尽量少用无谓的装饰,应该考虑节省屏幕空间,各种分辨率的大小,缩放时的状态和原则,并且为将来设计的按钮、菜单、标签、滚动条及状态栏预留位置。设计中将整体色彩组合进行合理搭配,将软件商标放在显著位置,主菜单应放在左边或上边,滚动条放在右边,状态栏放在下边,以符合视觉流程和用户使用心理。 3)软件按钮设计 软件按钮设计应该具有交互性,即应该有3到6种状态效果:点击前鼠标未放在上面时的状态;鼠标放在上面但未点击的状态;点击时状态;点击后鼠标未放在上面时的状态;不能点击时状态;独立自动变化的状态。按钮应具备简洁的图示效果,名称易懂,用词准确,能望文知意最好,让使用者产生功能关联反应,群组内按钮应该风格统一,功能差异大的按钮应该有所区别。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值