欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > 设计模式-UML建模语言面向对象的SOLIDLC

设计模式-UML建模语言面向对象的SOLIDLC

2024/11/30 8:52:14 来源:https://blog.csdn.net/m0_74380601/article/details/141721762  浏览:    关键词:设计模式-UML建模语言面向对象的SOLIDLC

一、设计模式概述和UML

1.设计模式概述

1.序言

把软件开发当做修炼武功的话,那么可以分为招式和内功心法。

招式:C++,C, java, python等编程语言;jsp等开发技术,qt, spring等开发框架。这些是可以通过学习,较快理解并掌握的。

内功心法:算法,设计模式等;内功的修炼是一个漫长的过程。但内功短时间内提升也是有诀窍的,就是把别人的总结好的内功秘籍拿来,消化吸收,不要自己去摸索。

2.设计模式的概念

设计模式是在特定环境中解决某类重复出现问题的一套成功的有效的解决方案。在各行各业都有设计模式,每个行业每个岗位有自己解决问题的经验和模式。最早设计模式是在建筑行业提出的。

3.软件设计模式从何而来

1994年四人组(Gang of Four)简称GoF,在一本书《设计模式——可复用的面向对象软件元素》中提到的。其实软件模式并非仅限于设计模式,还包括架构模式,分析模式和过程模式等。实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。

4.软件设计模式的种类

GoF提出了24个设计模式,共分为以下三类:

创建型模式:如何创建对象

结构型模式:如何实现类或对象的组合

行为型模式:类或对象怎样交互以及怎样分配职责

5.为什么要学习设计模式

学习设计模式,可以让我们站在巨人的肩膀上,在前人的经验上,非常快速有效地解决特定场景的某些重复性问题,而不需要自己伤脑筋探索。同时更加深入将面向对象思想和具体场景结合。好比说你是个刚学会开车的新手,有个老司机将几十年的经验传授给你。

学了设计模式,可以做到:1)如何将代码分散在多个不同的类中;2)为什么要有“接口”;3)什么叫做抽象编程?4)什么时候用继承?5)如何做到不改变源代码的情况下增加新功能;6)更好的理解现有类库中的源代码;... ...让我们脱离“菜鸟期”。

2.UML统一建模语言

UML(united modeling language)是统一建模语言:是一种面向对象的可视化建模语言;采用了一组形象的图形和符号作为建模语言,通过图形之间的各种关系来描述模型。虽然名字带着语言,但是不用敲代码,只需要画图。

1.什么是建模

1.1 就是建立模型框架。

任何事情都需要先想清楚了才能做,盖房子有图纸,装修有设计图纸,软件开发更是如此。软件开发过程不能一上来就开始写代码,写代码之前必须搞清楚下面这些问题?

  • 要做什么?
  • 做成什么样?
  • 怎么去做?
  • 当然以上问题必须以用户(甲方)的需求作为基准。
1.2 软件建模的实现过程

软件建模的作用——把现实世界的问题转化为计算机可以理解和实现的过程。

软件建模的实现过程——从需求入手,用模型表达分析设计过程,最终搭建模型映射成软件实现。具体——抽象——具体的过程。

2.UML有哪些图

2.1 UML 各种图

UML图分为结构图和行为图。

结构图分为类图、轮廓图、组件图、组合结构图、对象图、部署图、包图。

行为图又分活动图、用例图、状态机图和交互图。

        交互图又分为序列图、时序图、通讯图、交互概览图。

        用例图:由参与者(用户),用例以及他们之间的关系构成的用于描述系统功能的视图。

        类图:描述了系统中类,以及他们之间存在的各种静态关系。

2.2 UML画图工具:StarUML

StarUML 是一个开源的 UML 工具列表软件。

破解版:https://www.cnblogs.com/qiuyutingyyds/p/16650504.html

还可以使用在线画图工具:http://processon.com/

2.3 用例图

【概念】

用例图(use case diagram):也称为用户模型图,是从软件需求分析到最终实现的第一步,是从客户的的角度来描述系统功能。

【组件】

参与者(Actor):使用该系统的人。

用例(use case):系统中某项完整的功能

关系:定义用例和参与者之间的关系(关联关系,泛化关系,扩展关系,包含关系)

2.3.1用例图——关联关系

表示用户和功能之间的使用关系,哪些用户可以使用哪些功能。

2.3.2用例图——泛化关系

表示同一业务目的(父用例)的不同技术实现(各个子用例)在UML中用泛化关系表示,其实就是面向对象的继承(在UML中用空心箭头,箭头指向父)。

2.3.3用例图——扩展关系

如果在完成某个功能的时候偶尔会执行另外一个功能,则用扩展关系。在UML中扩展关系用虚线箭头加 <<extend>>箭头指向被扩展的用例。

2.3.4 用例图——包含关系

一个用例可以包含其他用例具有的行为并把它作为自己行为的一部分,在UML中包含关系用虚线箭头加 <<include>>箭头指向被包含的用例。

2.4 类图

类图主要是用来显示系统中的类、接口以及它们之间的关系(泛化关系,实现关系, 依赖关系,关联关系,聚合关系,组合关系等),是面向对象系统建模中最常见的图 。

类由3部分组成:类名,属性和方法。在UML中,类用矩形来表示,顶端部分存放类的名称,中间部分存放类的属性,属性的类型。底部部分存放类的方法,方法的参数和返回类型。(其中属性和方法可以选择性的列出部分代表,不用全部列出)。

2.4.1 类图——泛化关系(继承)

在UML中 泛化关系用来表示类与类,接口与接口之间的继承关系。在UML中泛化关系用一条实心线空箭头,由子类指向父类。

2.4.2 类图——实现关系

在UML中实现关系用来表示类与接口之间的实现关系,类实现接口。实现关系用一条虚线空心箭头由子类指向父类。

2.4.3 类图——依赖关系

依赖关系是一种使用关系,即一个类的实现需要另一个类的协助。比如:某人上班,需要一辆车,这个车可以不属于人这个类(不是人的属性),人与车的关系就是依赖关系。依赖的类作为成员函数的参数出现,用虚线箭头表示,箭头指向要依赖的类。

2.4.4 类图——关联关系

对于两个相对独立的系统,当一个系统的实例与另一个系统的一些特定实例存在固定的对应关系时,这两个系统之间为关联关系。例如:公司和员工,每个员工肯定属于一个公司。车和主人,每辆车属于特定的主人。关联的类作为属性出现,用实线箭头表示,箭头指向要关联的类。

2.4.5 类图——聚合关系

聚合是整体和部分之间的关系,例如汽车由引擎、轮胎以及其他零件组成。电脑由CPU,硬盘,内存组成。聚合关系也是通过成员变量来实现的,但是跟关联关系不一样,关联关系所涉及的两个类处在同一层面上,而聚合关系中两个类处于不同的层次上,一个代表整体,一个代表部分。用空心菱形箭头表示,箭头指向整体类。

2.4.6 类图—组合关系

组合关系也是关联关系的一种 ,比聚合关系还要强。代表整体的对象负责部分对象的生命周期。整体如果不存在了,部分也就不存在了。如:鸟和翅膀的关系。用实心菱形箭头表示,箭头指向整体类。

二、面向对象的设计原则SOLIDLC

软件设计的目标:高内聚低耦合

软件设计原则蕴含在很多设计模式中,是从众多设计方案中总结出的指导性原则。提高代码的重用性,可读性,可扩展性,可靠性,还能降低系统中类与类之间的耦合度,使程序呈现高内聚低耦合的特性。

面向对象设计原则如下:

什么是高内聚低耦合?

高内聚、低耦合讲的是程序单位协作的问题。内聚和耦合的的评判标准是强度,内聚越强越好,耦合越弱越好。最弱的耦合就是通过一个主控模快来协调n个模块进行运作,最强的内聚就是功能不能拆分,也就是原子化。

可以这样理解,一个企业的管理,最理想的情况就是各个部门各司其职,井然有序,互不干涉,但是需要沟通交流的时候,各个部门都可以找到接口人专门负责部门沟通。在软件里呢, 就是说各个模块要职能明确, 一个功能尽量由一个模块实现,同样,一个模块最好只实现一个功能。这个是所谓的“内聚”; 同时,模块与模块之间、系统与系统之间的交互,是不可避免的, 但是我们要尽量减少由于交互引起的问题,比如单个模块无法独立使用或者无法移植的情况发生, 尽可能多的单独提供接口用于对外操作, 这个就是所谓的“低耦合”。 但是实际的设计开发过程中,总会发生这样那样的问题与情况, 真正做到高内聚、低耦合是很难的。

高内聚:

高内聚是一个评判软件设计质量的标准。内聚更为专业的说法叫做功能内聚,是对系统中元素职责的相关性和集中度的度量。如果元素有高度的相关职责,除了这些职责在没有其他的工作,那么该元素就有高内聚。

例如:这就好像,如果我是一个项目经理,我的职责是监控和协调我的项目各个阶段的工作。当我的项目进入需求分析阶段,我会请求需求分析人员来完成;当我的项目进入开发阶段,我会请求软件开发人员来完成;当我的项目需要测试的时候,我会请求测试人员完成。需求分析人员、软件开发人员、软件测试人员他们都是高内聚的,因为他们只负责自己的本职工作,但是如果我这个项目经理参与了开发工作,我就不是一个高内聚的元素,因为开发不是我的职责。当然如果开发人员参与了测试工作,那么开发人员也不是高内聚的了。

为什么要高内聚?为了可读性、复用性、可维护性和易变更性。

低耦合:

耦合就是元素与元素之间的连接,感知和依赖度量。这里说的元素即是功能,方法,模块,对象,系统。

        例如:现在有元素A和元素B。我们让A元素去调用B元素,当B元素有问题或者不存在的时候,A元素就不能正常的工作,那么就说元素A和元素B耦合。

耦合带来的问题:

        当元素B变更或者不存在时,都将影响元素A的正常运作,影响系统的可维护性和易变更性。同时这也大大的降低了A元素的可复用性。正因为耦合的种种弊端,我们才需要在软件设计上追求低耦合。

低耦合如何做:

元素A不能过度依赖元素B

合理的职责划分:让系统中的对象各司其职,不仅是提高内聚的要求,同时也可以有效地降低耦合

使用接口而不是继承:我们不难发现。继承就是一种耦合,假如子类A继承了父类B,不论是直接继承或者间接继承,一但父类B不存在或者发生任何变更,都将导致子类A不得不修改或者重写。假如父类B的子类数十上百的,这就是灾难性的变更。

2.1. 单一职责原则S—Single Responsibility

单一职责原则:一个类应该只包含单一的职责,承担的职责越多,被复用的可能性越小。

chef:厨师 gardener园丁 painter油漆工 driver司机

1.实例:

C/S是客户端/服务器模式;B/S是浏览器/服务器模式。

2.现有功能分析:

(1)init() 的作用是在登录时,对登录界面进行初始化

(2)display() 负责显示登录界面

(3)validate() 对当前界面用户输入的信息进行语法合法性检验,这个是语法验证,比如说用户的密码输入了没有,是否为空;年龄是否是负数…而不是对用户进行合法性验证

(4)getConnection() ,findUser() ,用户的信息保存在数据库中,要进行用户信息的验证,要连接数据库,并且在数据库中寻找输入的用户

(5)mian() ,是整个系统的入口

3.存在的问题

根据单一职责原则,一个类应该只有一个职责。现在的登录类大体来讲至少有3种职责。

(1)前面三个方法都是与登录界面有关系的。初始化登录界面,展示登录界面,对界面输入的信息进行语法性检验。

(2)getConnection() 和 findUser() 这两个方法是与业务逻辑无关的,是和数据库打交道的。

(3)mian() 方法与登录类没有关系,它是整个系统的入口。只不过是恰好存在系统入口是先进入登录界面的情况。

4.根据单一职责重构后

(1)MainClass: 里有一个 main(),负责系统的入口

(2)LoginForm :是一个界面视图,界面表单。里面有界面的初始化,界面展示,语法检验方法。

(3)UserDAO:是数据访问对象,是用于访问用户对象的,与用户对象打交道的,里面有 findUser()

(4)DBUtil:数据库连接工具,负责与 数据库进行连接

2.2开闭原则O —Open-Closed

开闭原则:对扩展开放,对修改关闭。不修改源代码的情况下,改变模块的行为。

实例

1.存在的问题

(1)原始的设计方案就是,在登录界面希望用一个圆形按钮。 处理方法就是在 LoginForm 类里面包含了一个 CircleButton 类;然后 LoginForm 类用一个 display() 显示出来。

(2)但是这样处理问题就出现了,如果用户要求使用其他类型的按钮,比如把圆形的改为矩形的,那要如何处理。如果按照之前的思路就是直接将 CircleButton 类改为 RectangleButton 类,如下图。

2.开闭原则重构后

关闭修改的方式,打开拓展的方式,改造成一个不确定的抽象的按钮类,abstraction是抽象的意思,对外提供服务,然后等我们使用的时候根据需要,把这个抽象按钮类用具体的类代替即可,这个代替的过程可以放在配置文件中实现,软件可以根据配置文件的不同而启动不同的状态,不必修改源代码。如果再增加一个三角形按钮,可以在不修改源代码的情况下,直接写一个三角形类,让他继承抽象按钮类。

2.3 LSP里氏替换原则L — Liskov Substitution

里氏替换原则:

子类对象能够替换程序中的父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。 从定义描述和代码实现上来看,多态和里氏替换原则有点类似,但它们关注的角度是不一样的。多态是面向对象一大特性,是一种语法,是一种代码实现的思路。 里氏替换是一种设计原则,用来指导继承关系中子类如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

LSP里氏替换原则:

其本质就是用子类去实例化父类声明的对象(即父类的指针指向子类开辟内存),然后调用子类内部的方法等。里氏替换是实现开闭原则的方式之一。因为我们可以用不同的子类拓展不同的功能,这样就打开了拓展。但父类不变,所以关闭了修改。

实例代码演示,见vs

#include <iostream>
using namespace std;//里氏替换原则
//父类,动物类
class Animal
{
public://吃东西的方法virtual void eat() { cout << "不同的动物吃不同的东西" << endl; }
};
//子类猫:继承父类同时对eat进行重定义
class Cat :public Animal
{
public:void eat() { cout << "猫吃鱼" << endl; }
};
//子类狗:继承父类同时对eat进行重定义
class Dog :public Animal
{
public:void eat() { cout << "狗吃骨头" << endl; }
};
//不使用里氏替换原则
//定义一个学生类,学生类有喂猫和喂狗的方法
class Student
{
public:void feedCat(Cat& cat) { cout << "学生喂猫,"; cat.eat(); }void feedDog(Dog& dog) { cout << "学生喂狗,"; dog.eat(); }
};
void test01()
{Student s;Cat c;Dog d;s.feedCat(c);s.feedDog(d);
}
//实现开闭原则和里氏替换原则
class Person
{
public:void feedAnimal(Animal& a) { cout << "人喂动物,"; a.eat(); }
};
void test02()
{Person p;Cat c;Dog d;//人喂动物的时候,通过一个具体的动物子类替换掉父类p.feedAnimal(c);p.feedAnimal(d);
}

2.4接口隔离原则I —Interface Segregation

一旦接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。满足单一职责原则。使用多个专门的接口,每一个接口应该承担一种相对独立的角色,在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护。接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差。

1.实例

开发人员针对某CRM系统的客户数据显示模块设计了如图所示接口

dataRead()用于从文件中读取数据,

transformToXML()用于将数据转换成XML格式,

createChart()用于创建图表,

displayChart()用于显示图表,

createReport()用于创建文字报表,

displayReport()用于显示文字报表。

2.存在的问题

接口承担了太多的职责,导致该接口的实现类很庞大,而且用户根本用不到这么多接口。

3.按照接口隔离原则和单一职责原则重构

把一个大的接口拆成了四个接口,1)从文件中读数据;2)转换为XML格式数据;3)图表操作;4)文字报表操作。

用户只用到了1、3这两个接口

2.5依赖倒置原则D —Dependency Inversion

抽象不应该依赖于具体类,具体类应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。高级模块不应依赖于低级模块,两者都应取决于抽象abstraction;抽象不应依赖细节,细节应取决于抽象。

在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码。

开闭原则是面向对象设计的目标,那么依赖倒置原则就是主要手段。

1.实例需求:

某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是 XLS文件(XLSTransformer) 等。

2.原始设计

3.存在的问题

现在 MainClass 依赖的四个模块都是具体的。

由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类MainClass都需要修改源代码,以便使用新的类,违背了开闭原则。

4.依赖倒置重构

现使用依赖倒置原则对其进行重构,存在的问题就是 MainClass 依赖的四个模块都是具体的,具体的东西是不稳定的。解决的思路就是将具体的模块抽象化,让 MainClass 依赖的是抽象的东西。

2.6迪米特法则L—Law of Demeter

又叫做最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

迪米特法则是希望减少类之间的耦合,让类越独立越好。所以需要实现高内聚、低耦合。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。

1.实例

上图,当DAO 类发生变化就会引起多个界面发生变化,用迪米特法则解耦,增加了中间的控制层,中间的控制层就起到了隔离的作用,降低类之间的耦合关系。

2.7合成复用原则C—Composite Reuse Principle, CRP

如果要实现类之间的复用,优先使用合成组合的方式来复用,而不是继承的方式来复用。简言之:要尽量多使用依赖/组合/聚合关系,提高拓展性,减少耦合度。少用继承,因为继承会增加耦合度,不便于拓展。继承适合用在相似的类之间,有相似特征的类之间。下面这个例子,学生类和教师类跟数据库连接类之间没有相似的特征,不能用继承。应该用组合的方式复用。这也违背了里氏替换原则。

1.实例

每次对学生或者教师的信息进行访问前,都要连接数据库,再进行操作。所以两个DAO类为了复用 getConnection(),就通过继承的方式去复用。(1)getConnection () 连接数据库(2)StudentDAO :对学生进行增删查改(3)TeacherDAO :对教师进行增删查改

2.问题

如果需要更换数据库连接方式,如原来采用JDBC连接数据库,现在采用数据库连接池连接,则需要修改DBUtil类源代码。

如果StudentDAO采用JDBC连接,但是TeacherDAO采用连接池连接,则需要增加一个新的DBUtil类,并修改StudentDAO或TeacherDAO的源代码,使之继承新的数据库连接类,这将违背开闭原则,系统扩展性较差。

3.按照合成复用原则重构

舍弃继承,直接在学生类和教师类中设置数据库连接方式。数据库连接方式是父类是个抽象类,作为参数使用,同时可以有多种具体的数据库连接方式继承抽象的数据库连接方式,这样就可以不修改源代码的情况下,通过改变参数对象即可使用不同的连接方式,满足了开闭原则,也满足了里氏替换原则,系统的拓展性更好,降低耦合度,这是组合的思路。

2.8 七大原则总结

目的:提高代码的重用性,可读性,可扩展性,可靠性,还能降低系统中类与类之间的耦合度,使程序呈现高内聚低耦合的特性。

七大原则就是讲一个类如何设计,两个类关系如何设计。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com