文章目录
- 软件建模详细设计概述
- 软件建模详细设计目标
- 软件建模详细设计原则
- 开闭原则
- 里氏 (Liskov) 替换原则
- 依赖倒置原则
- 接口分离原则
- 单一职责原则
- 最少知识原则(迪米特法则)
- 高内聚原则
- 松耦合原则
- 可重用原则
- 软件建模详细设计内容
- UML 软件静态结构视图建模
- 软件静态结构细化设计
- 类的聚合关系细分
- 专属聚合 (Exclusive0wns)
- 从属聚合 (0wns)
- 拥有聚合 (Has)
- 成员聚合 (Member)
- 泛化与继承
- 实现继承
- 扩展继承
- 限制继承
- 方便继承
- 高级类图建模
- 可见性
- 导出信息
- 限定关联
- 关联类与具体化类
- 接口与抽象类
- 接口
- 抽象类
- 接口与抽象类的区别
- 类内聚与耦合
- 类内聚与耦合的启发规则
- 类耦合的种类
- 类之间 6 种关系的耦合强度依次增强
- 迪米特法则应用
- 设计类图建模实践
- UML软件动态交互视图建模
- 顺序图
- 通信图
- 类操作
- 发现操作
- 类操作的表示
- 顺序图高级交互技术
- 创建与销毁临时对象
- 片段
- 交互引用
- UML软件状态机视图建模
- UML 软件的实现视图建模
- 实现视图
- 构件图
- 构件和构件之间的依赖关系
- 构件与接口之间的关系
- 端口
- 端口连接
- 部署图
- 包图
- 包
- 包的可见性
- 包的嵌套
- 包图
- 图书借阅管理系统软件建模设计实践
- 系统需求
- 系统静态模型设计
- 系统动态模型设计
- 图书搜索功能逻辑设计
- 图书借阅功能逻辑设计
- 图书归还功能逻辑设计
- Book对象的状态机图设计
- Borrower对象的状态机图设计
- 静态结构的类图模型细化设计
软件建模详细设计概述
软件建模详细设计目标
软件建模详细设计是在软件架构模型设计基础上,对系统中各模型进一步细化设计,给出软件程序实现的设计方案。
软件建模详细设计原则
开闭原则
- 软件实体(类、构件等)应该对功能扩展具有开放性,对代码修改具有封闭性。
- 当应用需求改变时,在不修改软件实体源代码的前提下,就可以扩展模块的功能,使其满足新的需求。
案例:“环境监测器”功能构件设计
开闭原则设计的好处:
- 在不修改原有代码基础上,可以扩展新功能。
- 提高代码的可复用性及可维护性。
里氏 (Liskov) 替换原则
- 子类可以扩展基类的功能,但不能改变基类原有的功能。
- 子类在继承基类时,除了添加新的方法且完成新增功能外,不要重写基类的方法代码。
- 子类必须遵守基类与外部类之间的隐含约定。
里氏替换原则设计的好处:
- 解决了继承重写父类带来的可复用性变差问题。
- 提高代码的可复用性及可维护性。
案例:鸟类飞行时间计算模块程序设计
//鸟类
class Bird {
protected double flySpeed;
public void setSpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//燕子类
class Swallow extends Bird {
}
//几维鸟类
class BrownKiwi extends Bird {
public void setSpeed(double speed) {
flySpeed = 0;
}
}
package principle;
public class LSPtest {
public static void main(String[] args) {
Bird bird1 = new Swallow();
Bird bird2 = new BrownKiwi();
bird1.setSpeed(120);
bird2.setSpeed(120);
System.out.println("如果飞行300公里: ");
try {
System.out.println("燕子将飞行" +
bird1.getFlyTime(300) + "小时.");
System.out.println("几维鸟将飞行" +
bird2.getFlyTime(300) + "小时.");
} catch (Exception err) {
System.out.println("发生错误了!");
}
}
}
按照里氏替换原则重新设计该模块
完整代码:
// 动物类
class Animal {
protected double runSpeed;
public void setRunSpeed(double speed) {
runSpeed = speed;
}
public double getRunTime(double distance) {
return (distance / runSpeed);
}
}
// 鸟类
class Bird extends Animal {
protected double flySpeed;
public void setFlySpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
// 燕子类
class Swallow extends Bird {
// 燕子类可以继承 Bird 类的飞行行为
}
// 几维鸟类
class BrownKiwi extends Animal {
// 几维鸟不能飞,所以我们不实现飞行相关的方法
// 而是使用 Animal 类的跑步行为
// 如果需要,这里可以覆盖跑步速度设置方法或行为
}
依赖倒置原则
- 基于抽象类编程,不依赖具体类编程。
- 面向接口编程,不要面向实现编程。
依赖倒置原则设计的好处:
- 降低客户类与实现类之间的耦合性,容易实现代码可扩展性。
- 降低并行开发可能出现的风险性。
案例:顾客购物模块程序设计
顾客类 (Customer) 通过韶关网店(ShaoguanShop)类实现购物,其代码如下:
class Customer {
public void shopping(ShaoguanShop shop) {
//购物
System.out.println(shop.sell());
}
}
顾客若想从婺源网店 (WuyuanShop) 购物,需要对顾客类 (Customer )代码修改:
class Customer {
public void shopping(WuyuanShop shop) {
//购物
System.out.println(shop.sell());
}
}
解决方法:定义“婺源网店”和“韶关网店”的共同接口 Shop ,顾客类 (Customer )面向该接口编程,其代码修改如下:
class Customer {
public void shopping(Shop shop) {
//购物
System.out.println(shop.sell());
}
}
完整代码:
// 定义 Shop 接口
interface Shop {
String sell();
}
// 韶关网店实现 Shop 接口
class ShaoguanShop implements Shop {
@Override
public String sell() {
// 实现卖出商品的方法,返回结果
return "韶关网店销售的商品";
}
}
// 婺源网店实现 Shop 接口
class WuyuanShop implements Shop {
@Override
public String sell() {
// 实现卖出商品的方法,返回结果
return "婺源网店销售的商品";
}
}
// 顾客类,面向 Shop 接口编程
class Customer {
public void shopping(Shop shop) {
// 购物
System.out.println(shop.sell());
}
}
接口分离原则
- 一个构件对外应提供不同服务的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的构件去调用,避免客户端依赖不必要的接口方法
- 提供者构件应该为每个不同访问构件类型提供一个特定的接口
接口分离原则设计的好处:
- 符合高内聚低耦合设计思想,提高代码可扩展性。
- 使得类具有很好的可读性、可扩展性和可维护性。
案例:学生成绩管理程序
- 学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、查询成绩信息等功能。
- 如果将这些功能全部放到一个接口中对外提供访问服务,这显然不太合理。
- 合理的做法是对外提供不同的功能服务访问接口。
单一职责原则
- 一个类应该有且仅有一个功能职责,否则该类应该被拆分。
- 单一职责原则的核心就是控制类的粒度大小,将对象解耦,提高其内聚性。
单一职责原则设计的好处:
- 降低类的复杂度。一个类只负责一项职责,其代码逻辑肯定要比负责多项职责简单得多。
- 提高类代码的可读性。复杂性降低,自然其可读性会提高。
- 提高类代码的可维护性。可读性提高,那自然更容易维护了。
案例:学生工作管理
学生工作主要包括学生生活辅导和学生学业指导两个方面的工作:
- 生活辅导主要包括班委建设、出勤统计、心理辅导、费用催缴、班级管理等工学业指导主要包括专业引导、学习辅导、科研指导、学习总结等工作。
- 如果将这些工作交给一位老师负责显然不合理,好的做法是生活辅导由辅导员负责,学业指导由学业导师负责。
最少知识原则(迪米特法则)
迪米特法则的定义是:只与你的朋友交谈,不跟“陌生人”说话。在程序设计中,如果两个软件实体没有直接关系,那么就不直接调用,可以通过第三方转发该调用。
迪米特法则具有以下好处:
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于耦合度降低,从而提高了类的可复用性和系统的扩展性。
案例:明星与经纪人的关系
- 明星需全身心投入艺术,所以许多日常事务交给经纪人去处理,如与粉丝的见面会,与媒体公司的业务洽淡等。
- 经纪人是明星的朋友,而粉丝和媒体公司对明星来讲是陌生人。
高内聚原则
内聚性意味着构件或者类只封装那些密切关联的属性和操作。
按照内聚性的级别,内聚性分为以下三种,它们的内聚性依次变弱:
- 功能内聚:模块内的所有元素紧密配合完成同一个功能,即满足单一职责原则。
- 分层内聚:系统采用分层结构,高层能够访问低层服务,但低层不能访问高层服务。
- 通信内聚:模块内的所有元素都访问相同数据。通信内聚通常用于数据的查询、访问和存储。
一般来说,内聚性越高,构件越易于实现、测试和维护。
松耦合原则
耦合性指不同对象之间相互关联的程度。如果一个对象过多地依赖于其它对象来完成自己的工作,这会增加测试、修改的难度,同时降低了对象的可重用性和可移植性。
在详细设计中要尽可能保持对象之间松耦合。但是软件必须进行内部和外部的通信,因此耦合是必然存在的。当两个对象必须相互联系时,应该通过类的公共接口实现耦合,不应该直接依赖于类的实例。
补充例子:
// 定义一个通信接口
interface Communicator {
void sendMessage(String message);
String receiveMessage();
}
// 实现Communicator接口的Email类
class EmailCommunicator implements Communicator {
public void sendMessage(String message) {
System.out.println("Sending email with message: " + message);
}
public String receiveMessage() {
// 假设接收到消息
return "Email message received";
}
}
// 实现Communicator接口的SMS类
class SMSCommunicator implements Communicator {
public void sendMessage(String message) {
System.out.println("Sending SMS with message: " + message);
}
public String receiveMessage() {
// 假设接收到消息
return "SMS message received";
}
}
// 使用Communicator接口的Client类
class Client {
private Communicator communicator;
public Client(Communicator communicator) {
this.communicator = communicator;
}
public void communicate() {
communicator.sendMessage("Hello World!");
System.out.println(communicator.receiveMessage());
}
}
// 在应用程序中使用Client和Communicator
public class CommunicationApp {
public static void main(String[] args) {
Communicator email = new EmailCommunicator();
Communicator sms = new SMSCommunicator();
Client emailClient = new Client(email);
Client smsClient = new Client(sms);
emailClient.communicate();
smsClient.communicate();
}
}
在这个例子中,Client
类不直接与 EmailCommunicator
或 SMSCommunicator
类耦合。相反,它通过 Communicator
接口与它们通信。这允许 Client
类在不知道通信细节的情况下发送和接收消息。因此,如果将来引入了一个新的 Communicator
实现,例如 WhatsAppCommunicator
,Client
类不需要任何修改就能使用它,只需在创建 Client
实例时传入新的通信器即可。这就是通过类的公共接口实现耦合的一个典型例子。
可重用原则
- 为了提高工作效率、减少错误、降低成本,要充分考虑软件元素的重用性。
- 重用性有两个方面的含义:尽量复用已有的类代码,如开发环境提供的类库;在设计新类时考虑将来的可重用性。
- 设计一个可重用的软件比设计一个普通软件的代价要高,但是随着这些软件被重用次数的增加,分摊到它的设计和实现成本就会降低。
补充例子:
import java.util.ArrayList;
import java.util.List;
// 一个通用的接口定义了操作的契约
interface Operable {
void operate();
}
// 一个具体的类实现了Operable接口
class Printer implements Operable {
@Override
public void operate() {
System.out.println("Printing a document...");
}
}
// 另一个具体的类实现了Operable接口
class Scanner implements Operable {
@Override
public void operate() {
System.out.println("Scanning a document...");
}
}
// 设计一个工作流程类,它可以利用Operable接口操作任何设备
class Workflow {
private List<Operable> operations = new ArrayList<>();
public void addOperation(Operable operable) {
operations.add(operable);
}
public void runAll() {
for (Operable operable : operations) {
operable.operate();
}
}
}
// 演示如何重用Workflow类来操作不同的设备
public class OfficeAutomationApp {
public static void main(String[] args) {
// 创建设备的实例
Printer printer = new Printer();
Scanner scanner = new Scanner();
// 创建工作流并添加操作
Workflow officeWorkflow = new Workflow();
officeWorkflow.addOperation(printer);
officeWorkflow.addOperation(scanner);
// 运行所有操作
officeWorkflow.runAll();
}
}
在这个例子中,Operable
接口允许我们定义一个可以操作任何设备的通用契约。Printer
和 Scanner
类都实现了这个接口,使得它们可以在 Workflow
类中互换使用。这样的设计不仅提高了类的重用性,而且在设计新类时也考虑到了将来的可重用性。例如,如果我们想添加一个新设备类如 FaxMachine
,我们只需要让它实现 Operable
接口,就可以轻松地添加到现有的工作流程中而不需要修改 Workflow
类的代码。
此外,这个例子还展示了如何复用Java开发环境提供的类库,例如 ArrayList
类,来管理 Operable
对象的集合。这种复用标准类库可以提高开发效率,减少错误,并降低开发成本。
以下客户功能模块存在什么设计问题?该如何完善?
接口分离原则, IClient
接口提供的方法太多或不是所有的客户端都需要这些方法,它违反了接口分离原则。客户端不应该被迫依赖于它们不使用的方法。 根据不同的职责将 IClient
接口拆分为更小的接口。详细代码如下:
// 接口负责连接管理
public interface IConnectionManager {
void connect();
void disconnect();
}
// 接口负责数据的发送和接收
public interface IDataTransfer {
void send();
void receive();
}
// 客户端类实现这两个接口
public class Client implements IConnectionManager, IDataTransfer {
@Override
public void connect() {
// 实现连接逻辑
}
@Override
public void disconnect() {
// 实现断开连接逻辑
}
@Override
public void send() {
// 实现发送数据逻辑
}
@Override
public void receive() {
// 实现接收数据逻辑
}
}
问题探讨:以下客户数据图形分析模块存在什么设计问题?该如何完善?
违背单一职责原则,应将该类进行拆分,CustomerDataChart
类负责图表的创建和显示,一个 CustomerDAO
类负责数据访问操作,以及一个 DBUtil
类负责数据库连接。
代码如下:
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
class DBUtil {
public Connection getConnection() {
// 实现数据库连接的逻辑
// 例如使用 JDBC 连接到数据库
return null; // 应返回实际的数据库连接对象
}
}
class CustomerDAO {
private DBUtil util;
public CustomerDAO(DBUtil util) {
this.util = util;
}
public List<Customer> findCustomers() {
// 使用 util.getConnection() 获取数据库连接
// 执行查询并返回客户列表
return new ArrayList<>(); // 应返回实际查询到的客户列表
}
}
class CustomerDataChart {
private CustomerDAO dao;
public CustomerDataChart(CustomerDAO dao) {
this.dao = dao;
}
public void createChart() {
// 使用 dao.findCustomers() 获取数据
// 根据获取的数据创建图表
}
public void displayChart() {
// 显示图表的逻辑
}
}
public class Main {
public static void main(String[] args) {
// 创建 DBUtil 实例
DBUtil dbUtil = new DBUtil();
// 使用 DBUtil 实例来创建 CustomerDAO 实例
CustomerDAO customerDAO = new CustomerDAO(dbUtil);
// 使用 CustomerDAO 实例来创建 CustomerDataChart 实例
CustomerDataChart dataChart = new CustomerDataChart(customerDAO);
// 创建图表
dataChart.createChart();
// 显示图表
dataChart.displayChart();
}
}
图形展示如下:
软件建模详细设计内容
- 在分析类基础上,对软件静态结构模型的类图进行详细设计。各个功能构件均需给出设计类模型。
- 针对构件功能,分析对象之间的交互关系,给出功能构件逻辑实现的动态行为交互图模型详细设计。
- 分析对象生命周期的状态变化,给出对象状态变迁的状态机图模型详细设计。
- 对反映软件物理实现模型的构件图和部署图进行详细设计。
UML 软件静态结构视图建模
软件静态结构细化设计
- 静态结构视图用来表示软件系统的类程序组成结构,同时也反映各个类中封装的数据与操作方法。
- 静态结构视图支撑面向对象程序编程。
类的聚合关系细分
专属聚合 (Exclusive0wns)
专属聚合是类之间语义最强的聚合,部分类与整体类之间满足 4 个条件:
- 部分类对整体类具有存在依赖性,删除整体类时,部分类也被删除。
- 具有传递性,如果对象 C1是 B1 的一部分,并且 B1 是 A1 的一部分, C1 也是 A1 的一部分。
- 具有非对称性,如果 B1 是 A1 的一部分,则 A1 不是 B1 的一部分。
- 具有固定性,如果 B1 是 A1 的一部分,则它绝不是 Ai (i!=1)的一部分。
从属聚合 (0wns)
从属聚合的语义比专属聚合稍弱,部分类与整体类之间满足 3 个条件:
- 部分类对整体类具有存在依赖性,删除整体类时,部分类也被删除。
- 具有传递性,如果对象 C1 是 B1 的一部分,并且 B1 是 A1 的一部分,则 C1 是 A1 的一部分。
- 具有非对称性,如果 B1 是 A1 的一部分,则 A1 不是 B1 的一部分。
拥有聚合 (Has)
拥有聚合的语义比从属聚合弱,部分类与整体类之间满足 2 个条件:
- 具有传递性,如果对象 C1 是 B1 的一部分,并且 B1 是 A1 的一部分,则 C1 是 A1 的一部分。
- 具有非对称性,如果 B1 是 A1 的一部分,则 A1 不是 B1 的一部分。
成员聚合 (Member)
成员聚合的语义最低,仅仅是将一组对象组合在一起。
- 不具有存在依赖性、传递性、非对称性、固定性。
- Member 聚合具有组合独立对象的特征。
- 表示一组对象作为一个高层复合对象来考虑。
- 一个对象可以同时属于一个以上的复合对象,因此 member 聚合的多重性可以是多对多。
泛化与继承
- 泛化是指多个类的公共特征抽象到一个更一般化的类中,它描述类之间的一般与具体关系。
- 继承是指子类复用基类(超类)属性与操作的机制。
实现继承
实现继承是指在子类中组合基类(超类)的特征,并允许在必要时以新的实现来重载这些特征。
扩展继承
在扩展继承中,子类组合来自超类的特征,并增加新的特征。
限制继承
限制继承是指子类组合来自超类的特征,并重载部分继承来的特征。
问题:重载导致子类没有包括超类的所有特征,会带来维护上的问题,可替换性原则失效。
方便继承
某些类之间具有一定相似的实现,但它们之间不存在泛化关系,即没有概念上的分类关系。方便继承是指将它们中一个类作为其它类的超类。
问题:子类没有包括超类的所有特征,子类与超类之间没有泛化关系,可替换性原则无效。尽量避免采用这种方式实现继承。
高级类图建模
软件开发的实际情况很复杂,复杂的问题不会有简单的解决方案。本节引入类图建模的高级概念,包括:
- 可见性
- 导出信息
- 限定关联
- 关联类与具体化类
可见性
通过设置可见性可以确定类内部的封装程度,决定其他类能否访问该类的元素。应用于类的属性和操作的可见性类型包括:
+ public
公共可见性: 该元素对任何类都可见;- private
私有可见性: 该元素只对本类可见;# protected
保护可见性: 该元素对本类及子类可见;~ package
包可见性: 该元素对同一包中的元素可见.
在设计良好的面向对象系统中,大多数操作是公共的,大多数属性是私有的。
public class Visibility {
private int privateAttribute;
public int publicAttribute;
protected int protectedAttribute;
int packageAttribute; // 默认访问修饰符,也称为包访问权限
private void privateOperation() {
}
public void publicOperation() {
}
protected void protectedOperation() {
}
void packageOperation() { // 默认访问修饰符,也称为包访问权限
}
}
( 1 )保护可见性
- 保护可见性指类的属性或操作对本类及其子类可见。
- 应用于继承的情况,允许子类对象访问父类对象中的属性和操作。
( 2 )包可见性
- 具有包可见性类的属性和操作能够被处于同一包中的其他类访问。
- 包可见性是 Java 语言默认的可见性。
( 3 )友元可见性友元可见性用于 c++语言。当一个类需要访问另一个类的私有属性或操作时,可以采用友元可见性,在授予友元关系的类中声明友元操作。
class Book {
private:
void putOnBookShelf();
};
class BookShelf {
public:
friend void Book::putOnBookShelf();
};
导出信息
导出信息是指从其他元素计算得到的信息。它作为一种冗余信息,其目的是为了增加可读性和信息存取速度。导出信息包括导出属性和导出关联。
- 导出属性是指可以由其他信息导出的属性。
- 导出关联是指由其它关联导出的关联。
- 导出信息的 UML 表示是在导出属性名或导出关联名前面加上一条斜线“/”。
限定关联
限定关联是一对多或多对多关联的约束条件表示。限定关联在二元关联的一端设定属性限定符,即在框中给出一个或多个属性用于约束条件。
关联类与具体化类
如果两个类之间是多对多的关联,且每个关联实例都具有属性值,这时可以使用关联类。关联类具有一个约束,要求对于每一对链接起来的类实例,只能作为关联类的一个实例。如果不能满足这个约束的话,就需要将关联具体化,即使用一个具体化类来代替关联类。这个具体化类与之前的两个类均具有二元关联。
接口与抽象类
接口
接口用于定义一个类或者构件的功能操作函数集。
- 接口定义一组操作,但不定义操作的实现。
- 接口一般不定义属性,只有定义操作,并且所有操作都隐含是公共的、抽象的可视性。
- 在实现接口的类中,这些操作都要被实现。
- 一个类可以实现多个接口,一个接口可以被多个类实现
接口是一种特殊的类,所有的接口都是构造型为<<interface>>
的类。在UML中,接口的表示方式有三种: 第一种是接口的图标表示,第二种是接口的修饰表示,第三种是接口的构造型标签表示。
- 如果接口使用 构造型标签表示,实现关系线是一条末端带有空心三角的虚线,箭头指向接口,虚线上可以加上构造型关键字《implement》。
- 如果接口使用图标表示,实现关系线则是一条实线,没有箭头,实线上也可以加上构造型关键字《implement》。
- 一个类使用接口,表明该类依赖该接口,用指向接口的一条虚线箭头表示,箭头上可以加上构造型关键字《use》。
案例: IElectricalEquipment接口定义了turnOn操作和turnoff操作,TV类和Player类均实现了该接口,即实现了turnOn操作和turnoff操作,RemoteControl类使用了IElectricalEquipment接口。
抽象类
抽象类指不具有实例的类,它通常作为父类,不创建实例对象。抽象类的子类可以实例化。
- 抽象类一般至少有一个抽象操作。
- 抽象类的作用是为其子类描述它们的公共属性和行为。
- UML 采用斜体表示抽象类的名称,也可以使用 {abstract) 约束来表示。
补充代码:
// 定义一个 Vehicle 基类
abstract class Vehicle {
private String color; // 车辆颜色属性
public Vehicle(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
// 定义一个抽象方法 drive
public abstract void drive();
}
// Plane 类继承自 Vehicle
class Plane extends Vehicle {
public Plane(String color) {
super(color);
}
// 实现 drive 方法
public void drive() {
System.out.println("Plane is flying.");
}
}
// Bus 类继承自 Vehicle
class Bus extends Vehicle {
public Bus(String color) {
super(color);
}
// 实现 drive 方法
public void drive() {
System.out.println("Bus is driving on the road.");
}
}
// Driver 类与 Vehicle 有关联关系
class Driver {
private Vehicle vehicle;
// Driver 可以驾驶任何类型的 Vehicle
public void driveVehicle(Vehicle vehicle) {
this.vehicle = vehicle;
vehicle.drive();
}
}
接口与抽象类的区别
- 抽象类是对一组具有相同属性和方法、逻辑上有关系的类抽象。
- 接口是对一组具有相同属性和方法、逻辑上不相关的类抽象。
- 抽象类可提供部分方法的代码实现,而接口不实现任何方法的代码实现。
- 抽象类体现一种泛化关系,而接口体现契约关系。
- 抽象是在问题域分析中发现,而是接口是在设计发现。
类内聚与耦合
类内聚是指类内部元素之间相关性的程度。类耦合是指类之间联系的程度。
-
内聚和耦合彼此相关联,更强的内聚会导致更弱的耦合,反之亦然。
-
良好的程序设计应确保在类的内聚与耦合之间达到良好平衡。
类内聚与耦合的启发规则
为了取得类内聚和类耦合之间的平衡, Riel提出了一些启发规则:
- 属性及其相关操作封装在一个类中;
- 类之间只通过公共接口产生依赖;
- 每个类应该只实现单一职责功能;
- 类中不存在一个操作子集只与一个属性子集相关的情况;
- 系统功能应尽可能在各个类中均匀分配。
类耦合的种类
( 1 ) x 类有属性指向 Y 类的实例
( 2 ) x 类有方法引用了 Y 类的实例,如x 使用 Y 类对象作为局部变量;或者返回 Y 类对象
( 3 ) x 类对象调用 Y 类对象方法发送消息
( 4 ) Y 类是 X 类的直接或间接子类
( 5 )X 类方法中输入参数为 Y 类的实例。
( 6 ) Y 类是一个接口,而 X 类实现了该接口。
类之间 6 种关系的耦合强度依次增强
1 )依赖关系一耦合度最弱
2 )关联关系一耦合度其次弱
3 )聚合关系一耦合度一般
4 )组合关系一耦合度较高
5 )继承关系一耦合度高
6 )实现关系一耦合度最高
迪米特法则应用
类耦合对于对象之间协作实现特定功能是必要的,但它应该尽量被限制在类的层次内,即层内耦合,层间耦合应该最小化。
为了限制类间任意通信,采用迪米特法则(最少知识原则)设计,指明在类操作中只允许将以下对象作为消息目标,即没有直接关系的类之间不交互通信,仅仅有直接关系类进行通信。
- 当前对象本身
- 当前操作中作为参数的对象
- 当前对象的属性指向的对象
- 当前操作创建的对象
- 全局变量引用的对象
设计类图建模实践
- 设计类图建模是在分析类图基础之上对系统分析类图进行细化与完善设计,从而在设计层面描述系统的静态结构。
- 在设计类图中除了详细定义各种类属性与方法外,还需定义类之间关系的设计约束。
Power Designer 建模实践:雇员职位管理设计类图建模
ChatGPT4:
- Person 类:
- 属性:
name
(String),sex
(char) - 方法:
getName()
返回 String,getSex()
返回 char - 这个类似乎代表了具有基本属性如姓名和性别的普通人。
- 属性:
- Employee 类(继承自 Person 类):
- 属性:
username
(String),password
(String) - 方法:
username
和password
的获取和设置方法,一个接受用户名和密码并返回布尔值的login()
方法,以及重写的getName()
和getSex()
方法。 Employee
类继承自Person
类,这意味着每个Employee
都是Person
,但具有特定于雇员的额外属性和行为,如登录系统的凭证。
- 属性:
- Post 类:
- 属性:
postname
(String) - 方法:
getPostname()
返回 String,setPostname()
接受一个 String,addPost()
返回一个布尔值。 - 这个类似乎代表
Employee
可以持有的职位或职称。1..1
表示每个Employee
有且仅有一个Post
。
- 属性:
- Department 类:
- 属性:
departmentname
(String) - 方法:
departmentname
的获取和设置方法,addDepartment()
返回布尔值。 - 代表组织中的一个部门。每个
Employee
与一个Department
关联,而一个Department
可以有多个雇员(1..*
关系)。
- 属性:
- Power 类:
- 属性:
powername
(String) - 方法:
powername
的获取和设置方法。 - 代表与
Post
关联的权力或权限。每个Post
有且仅有一个Power
,表示一对一的关系。
- 属性:
- Poweritem 类(继承自 Power 类):
- 属性:
itemname
(String),isvalue
(int) - 方法:
itemname
和isvalue
的获取和设置方法,addItem()
返回布尔值。 - 这个类继承自
Power
类,暗示每个权限可以有多个Poweritem
实例,这些是更具体的权限或许可。
- 属性:
类图还展示了类之间的关联关系:
- 一个
Employee
与一个Person
相关联(继承关系)。 - 一个
Employee
可以有一个或多个Post
(0..*
到1..1
的关系),但每个Post
都确切地与一个Employee
相关联。 - 每个
Post
与一个Power
关联,每个Power
与一个Post
关联(1..1
关系)。 - 一个
Department
可以有多个Employee
实例,但每个Employee
都与一个Department
相关联。
这个模型旨在管理组织中雇员的角色和责任。它捕捉了不同组织实体如人员、职位、部门以及与这些职位相关联的权限之间的层次结构和关系。该系统可用于跟踪员工的职位角色、所属部门及其在组织内的相应权限。使用继承和关联确保了清晰、可维护的结构,能够准确反映组织结构。
1 .分析说明如下模型中类之间设计该关系依据是什么?以及它们的耦合度高低。
2 .针对如下 UML 设计类图给出 Vehicle 类、 Car 类、 Bus 类的 Java 编程代码。
1)
2)
import java.util.ArrayList;
import java.util.List;
// Vehicle.java
public interface Vehicle {
void run();
}
// Car.java
public class Car implements Vehicle {
public String name;
public List<Driver> drivers;
public Car(String name) {
this.name = name;
this.drivers = new ArrayList<>();
}
@Override
public void run() {
// Car的run实现
}
public void beforeRun(Oil oil) {
// Car的beforeRun实现
}
public void addDriver(Driver driver) {
this.drivers.add(driver);
}
}
// Bus.java
public class Bus extends Car {
public Bus(String name) {
super(name);
}
// Bus可能有特定的行为和属性,这里只提供基础构造方法
}
完整代码:
import java.util.ArrayList;
import java.util.List;
// Vehicle.java
interface Vehicle {
void run();
}
// Car.java
class Car implements Vehicle {
public String name;
public List<Driver> drivers;
public Car(String name) {
this.name = name;
this.drivers = new ArrayList<>();
}
@Override
public void run() {
// Car的run实现
}
public void beforeRun(Oil oil) {
// Car的beforeRun实现
}
public void addDriver(Driver driver) {
this.drivers.add(driver);
}
}
// Bus.java
class Bus extends Car {
public Bus(String name) {
super(name);
}
// Bus可能有特定的行为和属性,这里只提供基础构造方法
}
// Driver.java
class Driver {
public String name;
public int age;
public List<Car> cars;
public Driver(String name, int age) {
this.name = name;
this.age = age;
this.cars = new ArrayList<>();
}
public void addCar(Car car) {
this.cars.add(car);
}
}
// Oil.java
class Oil {
public String type;
public Oil(String type) {
this.type = type;
}
public void add() {
// Oil的add方法实现
}
}
UML软件动态交互视图建模
软件动态交互视图用于描述系统功能的对象之间动态交互行为。顺序图和通信图均属于动态交互模型图。
顺序图
顺序图也称序列图,它按照从上至下顺序描述对象之间的消息交互,其目的是展示功能逻辑的设计实现。
- 顺序图由一组对象及它们之间的消息组成,强调消息的先后时间顺序。
- 顺序图也包括生命线、激活、片段等图形元素。
通信图
通信图是另一种类型的动态交互图,它用于表现对象之间的协作关系。通信图由对象以及其关联组成,它强调对象间的连接关系,对象之间消息通信顺序使用序列号表示。
类操作
类操作(方法)是指类所能提供的服务或功能函数。类将一组操作函数作为服务提供给系统中的其他类访问。
发现操作
- 可从顺序图或通信图的对象交互中发现类操作。顺序图和通信图中的每一条消息,都必须有目标对象的一个操作为其服务。
- 根据类的职责确定操作,如数据记录创建、读取、更新和删除操作。
类操作的表示
在 UML 中,类操作的表示语法为:
[可见性] 操作名称 [(参数表)] [:返回类型] [{属性字符串}]
操作名称是动词或动词短语,单词首字母小写。如果有多个单词,则将这些单词连接起来,第一个单词首字母小写,其他单词首字母大写。
ChatGPT4:
类图代表了使用数据传输对象(DTO)模式的用户注册系统设计。这种模式在面向对象设计中常用于软件应用子系统之间的数据传输。这里是类图的每个部分的详细解释:
UserDTO 类:
这是一个简单的 DTO 类,包含两个属性:
userAccount
:一个String
类型,用来存储用户的账户名。userPassword
:一个String
类型,用来存储用户的密码。
它还提供了两个属性的获取和设置方法,允许其他类访问和修改这些值:
getUserAccount()
:返回userAccount
字符串。setUserAccount(String userAccount)
:设置userAccount
字符串。getUserPassword()
:返回userPassword
字符串。setUserPassword(String userPassword)
:设置userPassword
字符串。
RegisterForm 类:
这个类似乎代表用户填写的注册表单。它包含:
user
:UserDTO
的一个实例,用来保存与用户相关的表单数据。userDao
:IUserDAO
接口的一个实例(下面会解释),可能用于将用户添加到某种类型的数据存储中。
IUserDAO 接口:
这是一个定义用户数据访问对象(DAO)契约的接口,包含以下方法:
addUser(UserDTO user)
:一个接受UserDTO
对象并返回一个表示添加用户到数据存储是否成功的boolean
值的方法。
OracleUserDAO 类:
这是 IUserDAO
接口的一个具体实现,专门设计用来与 Oracle 数据库交互。它实现了 IUserDAO
接口的 addUser
方法。
关系:
RegisterForm
与UserDTO
有一个组合关系,由实心菱形表示,意味着RegisterForm
包含并控制UserDTO
实例的生命周期。RegisterForm
与IUserDAO
有一个关联关系,由箭头表示,意味着它使用IUserDAO
接口来访问用户数据。IUserDAO
与OracleUserDAO
有一个实现关系,由虚线和空心箭头表示。这意味着OracleUserDAO
承诺实现IUserDAO
定义的契约。
案例:发现操作
顺序图高级交互技术
创建与销毁临时对象
- 当创建临时对象时,发送者发出一个实例化消息后将创建临时对象。
- 当销毁临时对象时,发送者发出消息销毁临时对象,并回收其拥有的资源。
- 在顺序图中,在对象的生命线上标记“×”符号,表示对象被销毁。
片段
在模块功能详细设计中,需要描述功能处理的控制逻辑,如展示条件分支、循环处理,以及并行执行。在顺序图中采用不同类型的片段来表示这些控制逻辑。
UML 2.x中预定义了如下片段:
- opt选项片段
- alt条件片段
- loop循环片段
- para并行片段
(1) loop循环片段
在模块功能逻辑设计中,使用loop片段表示重复执行一些操作。
(2)para并行片段
在模块功能逻辑设计中,使用para片段表示执行一些并行操作。
(3)alt条件片段
在模块功能逻辑设计中,使用alt片段表示根据条件执行操作。alt片段表达if-then-else逻辑。
(4) opt选项片段
在模块功能逻辑设计中,使用opt片段根据当条件执行选项操作。opt片段表达if…then逻辑。
交互引用
在设计有较多相同处理逻辑的模块时,可以采用交互引用复用已有处理逻辑,被引用的处理逻辑采用标签 ref 标注。
在如下订单提交模块序列图模型中,包含了哪些处理逻辑?并分别说明各交互对象的对象类型。
订单结账和信用卡支付。
Customer、Payment Processor为参与者, Order Checkout、CreditCard Payment为控制类对象, Checkout Page为边界类对象 ,Order、OrderItem、Item为实体类对象。
UML软件状态机视图建模
状态机图是对系统中对象的状态变化逻辑进行建模,它描述类对象随时间变化的行为状态,以及对象受事件影响如何进行状态变迁。
状态机图建模步骤:
- 发现对象状态
- 分析对象变迁的事件与条件
- 建立对象状态图
案例:微波炉工作的状态机图
ChatGPT4:
状态:
- Waiting(等待):微波炉的初始状态,显示时间,等待用户输入。
- Full Power(全功率):微波炉在全功率模式下工作,功率设置为600W。
- Half Power(半功率):微波炉在半功率模式下工作,功率设置为300W。
- Set Time(设置时间):用户设置微波炉的工作时间。
- Enabled(启用):微波炉门关闭,准备开始工作,显示“Ready”。
- Disabled(禁用):微波炉门开启,不允许操作,显示“Waiting”。
- Operation(操作):微波炉开始工作。
转换和触发事件:
- 用户选择全功率或半功率,微波炉进入相应的功率状态。
- 在任何功率状态下,设置时间后,微波炉进入设置时间的状态。
- 当时间被设置后,如果微波炉门关闭,微波炉变为启用状态。
- 如果微波炉门打开,微波炉将变为禁用状态。
- 从启用状态,开始操作将使微波炉进入操作状态并开始加热。
- 用户可以在任何时候取消操作,微波炉将返回到等待状态。
- 在操作结束后,微波炉将完成加热过程并返回到等待状态。
针对如下电梯对象的状态机图模型,如何编写其Java控制程序?
代码均为简化代码,参考思路即可:
1.定义电梯的状态和事件:
public enum ElevatorState {
IDLE,
MOVING_UP,
MOVING_DOWN,
OPEN_DOORS,
CLOSED_DOORS
}
public enum ElevatorEvent {
CALL_UP,
CALL_DOWN,
MOVE_UP,
MOVE_DOWN,
DOORS_OPEN,
DOORS_CLOSE,
TIMEOUT
}
2.创建电梯的控制类
public class ElevatorControl {
private ElevatorState currentState;
public ElevatorControl() {
this.currentState = ElevatorState.IDLE;
}
public void handleEvent(ElevatorEvent event) {
switch (currentState) {
case IDLE:
switch (event) {
case CALL_UP:
openDoors();
currentState = ElevatorState.OPEN_DOORS;
break;
case CALL_DOWN:
// Similar logic for CALL_DOWN
break;
// Handle other events
}
break;
case MOVING_UP:
switch (event) {
case MOVE_UP:
// Logic for moving up
break;
case TIMEOUT:
currentState = ElevatorState.IDLE;
break;
// Handle other events
}
break;
// Implement logic for other states
}
}
private void openDoors() {
// Logic to open elevator doors
System.out.println("Doors are opening.");
}
// Define other methods such as closeDoors(), moveUp(), moveDown(), etc.
}
3.使用控制类响应事件
public class ElevatorSystem {
public static void main(String[] args) {
ElevatorControl elevator = new ElevatorControl();
// Simulate events
elevator.handleEvent(ElevatorEvent.CALL_UP);
elevator.handleEvent(ElevatorEvent.DOORS_CLOSE);
elevator.handleEvent(ElevatorEvent.MOVE_UP);
// Add more events as needed
}
}
UML 软件的实现视图建模
实现视图
实现视图是对系统的实现结构建模,包括如下模型图:
- 构件图:建模构件和构件间的接口和依赖关系,表示系统构件组成结构;:
- 部署图:建模在系统运行时,构件在系统各节点上的物理部署关系及其运行环境;
- 子系统与包图:子系统可以用包来体现,可使用包图表示系统逻辑架构。
构件图
- 构件图是用来表示系统中构件与构件之间、构件与接口之间关系的图。
- 构件图可以从多种层次粒度描述系统实现架构,如系统分解哪几个子系统,子系统包括哪些构件,构件由哪些类封装组成。
构件和构件之间的依赖关系
构件与接口之间的关系
- 构件与构件之间通过定义良好的接口进行协作。
- 构件图中,构件之间的关系表现为依赖关系或实现关系。
- 实现关系指一个构件实现了一个接口,使用实线连接到小圆上。该接口被称为供给接口。
- 依赖关系指一个构件使用了一个接口,使用实线连接到半圆上。该接口被称为请求接口。
端口
端口是指构件一个对外交互点。端口显示为一个小正方形,放置在构件方框的边界上。
端口连接
在 UML 软件架构建模设计中,主要使用如下连接器将构件的端口进行连接。
- 装配连接器:用于连接构件之间的端口
- 委托连接器:用于连接构件供给端口与内部供给端口或内部请求端口与构件请求端口
作为一种选择, UML 还允许将连接器按如下方案连接构件端口。
若有多个构件使用相同接口访问另一构件接口, UML 允许将连接器按如下方案连接构件端口。
身份认证构件 (Authentication)端口与订单构件 (Orders) 端口均使用相同请求接口(lCustomers)连接客户构件(:Customers )端口的供给接口lCustomers。
委托连接器是将构件的端口连接到实现该行为的对象。它表示消息到达一个端口后,将被传递给内部目标对象进行处理。
案例:电子商务系统的构件图
部署图
- 部署图用于对系统的物理实现情况进行建模。
- 部署图用于表示软件构件如何部署到硬件环境中,它显示系统中的不同构件在何处物理地运行,以及彼此之间如何进行通信。
节点
- 节点是指人工制品和构件在系统中部署的硬件设备及其运行环境。
- 节点之间通过通信线路将连接起来实现信息传输。
构件在节点中部署
构件需要在节点设备中运行。在建模时,需要将构件在节点中进行部署
案例:电子商务系统部署图
包图
- 在 UML 模型图中包用来对模型元素进行分组,简化 UML 图,使其更容易理解。
- 包图用于对模型自身组织进行建模,它是由一系列模型元素(类、用例等)构成的包所组成的模型,描述包与包之间的关系。
包
UML 中包表示为左上方带有小矩形的矩形。包的名称采用字符串,简单名仅包含名称字符串,全限定名则以本包所处的外围包的名称作为前缀。
包的可见性
包可以控制protected所包含的内部元素的可见性,即外部元素访问包内元素的权限:
+ public
公共可见性: 对它所在包和内嵌包的对象可见,也对所有引入它的包及其子包中对象可见;- private
私有可见性: 只对它所在包中对象可见;# protected
保护可见性: 对它所在包和内嵌包中对象可见;
包的嵌套
- 包可以拥有其他包,子包又可以拥有自己的子包,从而构成嵌套结构。
- 包的嵌套可以清晰地表现模型元素之间的层次结构关系。
- 但是包的嵌套不宜过深,层数一般以 2到 3 层为宜
包图
- 包图描述包与包之间的关系,可用于描述软件系统的逻辑模型。
- 包与包之间可以存在依赖关系。两个包之间存在着依赖关系通常是指这两个包所包含的模型元素之间存在着一个或多个依赖关系。
- 包的依赖关系使用一根虚线箭线表示,箭头指向被依赖的包。依赖关系可以标上标签,在《》中包含依赖关系的类型。
案例:应用包图描述系统的软件架构
针对如下股票投资分析系统构件模型,分析构件之间关系如何设计?
ChatGPT4:
在这个股票投资分析系统的构件模型中,构件之间的关系是按照客户端-服务器架构设计的。客户端包含用户界面(GUI)、图形库(Graphing Lib)、客户端会话(Client Session)和股票组合(Stock Portfolio)构件。服务器端包含认证服务器(Authentication Server)、服务器会话(Server Session)、股票市场分析器(Stock Market Analyzer)和投资组合数据库(Portfolio DB)构件。
客户端的GUI用于展示信息和接收用户输入,图形库用于数据可视化。客户端会话管理用户与系统的交互,而股票组合构件负责管理用户的个人投资组合信息。这些客户端构件通过API与服务器端的构件交互。服务器端的认证服务器负责处理登录和安全认证,服务器会话维护与客户端的连接状态。股票市场分析器是系统的核心,负责分析股票市场数据和生成投资建议,而投资组合数据库存储用户的投资组合信息。
客户端与服务器之间的交互通过网络进行。例如,用户通过GUI发起请求,客户端会话将请求发送到服务器,服务器会话接收请求并与认证服务器进行认证,然后由股票市场分析器处理请求。结果通过服务器会话返回给客户端会话,并最终通过GUI展示给用户。
此外,系统还接入了第三方服务,如彭博数据库(Bloomberg DB),这提供了额外的市场数据,股票市场分析器可能会使用这些数据来增强分析结果的准确性。
针对如下房地产按揭交易系统部署模型,分析各要素之间关系如何设计?
针对如下电商系统软件架构模型,分析包之间关系如何设计?
《use》关系表示使用依赖,Web Shopping依赖Payment,Mobile Shopping依赖Payment…
《merge》关系表示合并,Web Shopping、Mobile Shopping、Phone Shopping、和Mail Shopping合并之后就拥有了Shopping Cart的全部功能 。也表明了它们共享一些在“Web Shopping”包中定义的通用功能。
《Access》关系表示私有引入,比如代码中的指定包名类名 。Payment可以访问Shopping Cart中的特定功能或类,但这种访问是受限的。
《Import》关系表示公共引入,比如Java中的import之后,就可以直接使用import包中的类了。一旦“Shopping Cart”公共导入了“Inventory”,它就可以直接使用“Inventory”中定义的公共类和接口。
图书借阅管理系统软件建模设计实践
系统需求
系统静态模型设计
建立系统静态模型类图。建立类图需要确定系统中的类,确定类的属性,确疋类之间的关系。根据系统动态模型确定类的操作并完善类图。
( 1 )确定系统中的类
- 系统用户有管理员 (Administrator) 、图书管理员 (Librarian) 和借阅者(Borrower) 三个参与者,可将他们抽取为的角色实体类。
- 系统需存储图书信息,可抽取出Book类;需要存储借阅信息,可抽取出Loan类;需要存储预定信息,可抽取出Reservation类;这些类是系统中的主要业务实体类。
( 2 )确定类的属性
( 3 )确定类之间的关系,创建系统类图
系统动态模型设计
系统详细设计阶段,针对用例图中的每一个用例,考虑如何通过对象之间的协作来完成用例的功能。采用顺序图或通信图对用例功能进行设计建模,采用状态机图对类的行为状态进行设计建模。
图书搜索功能逻辑设计
图书借阅功能逻辑设计
图书归还功能逻辑设计
Book对象的状态机图设计
Book 对象是图书管理系统中具有明显状态变化的对象,图书借阅业务都与Book对象状态有关。Book对象具有如下状态: 新书 (NewBook) 、可借阅预定(Available) 、被预定 (Reserved) 、被借阅 (Borrowed) 、被删除 (Deleted)。
Borrower对象的状态机图设计
Borrower对象也具有比较明显的状态,因此也采用状态机图对其建模。
Borrower 对象具 有 如下状态: 新建 (NewUser) 、未预定未借书 (NoLoanReserve) 、有预定 (HasReserve) 、有借书 (HasLoan) 、有预定有借书 (HasLoanReserve) 、被删除 (Deleted)。
静态结构的类图模型细化设计
-
Borrower类需要提供
- checkBorrower操作
- checkNumBooks操作
- addLoan操作
- deleteLoan操作
-
Book类需要提供
- searchBook操作
- checkBook操作
- setLoanStatus操作
-
Loan类需要提供
- addLoan操作
- deleteLoan操作
- checkOverdue操作
针对图书借阅管理系统如下构件,如何设计系统部署图模型,并在部署图中给出部署关系和运行环境(Apache、JBoss、PostgreSQL、Ubuntu)。
1)页面构件:borrowBook.jsp、returnBook.jsp、reserveBook.jsp、searchBook.jsp、viewBook.jsp、borrowerInfo.jsp,
2)控制构件:borrowBook.java、returnBook.java、reserveBook.java、searchBook.java、viewBook.java、borrowerInfo.java,
3)实体构件:book.java、reservation.java、loan.java、borrower.java,
4)数据库表:BookInfo、Reservation、Loan、Borrower、BookType
ChatGPT4:
我们可以按照三层架构来组织这些构件:
- 表示层(通过Apache和JBoss托管): 包括所有的JSP页面,如
borrowBook.jsp
,returnBook.jsp
等。 - 业务逻辑层(运行在JBoss上): 包括控制和实体构件,如
borrowBook.java
,book.java
等。 - 数据层(使用PostgreSQL): 包含所有数据库表,如
BookInfo
,Loan
等。
现在,我将详细设计一个系统部署图模型来展示这些构件是如何部署和相互作用的:
- Apache Web Server: 托管所有的JSP页面。这些页面通过HTTP请求与JBoss服务器通信。
- JBoss Application Server: 处理来自Apache服务器的请求,并运行Java控制和实体构件。JBoss可以部署这些Java构件,并提供必要的运行环境。
- PostgreSQL Database Server: 存储所有的数据库表,由JBoss服务器通过JDBC进行访问和操作。
- Apache Web Server托管JSP页面,并将请求转发给JBoss服务器。
- JBoss服务器处理业务逻辑,并与数据库服务器进行交互以存储或检索数据。
- PostgreSQL数据库服务器存储和管理数据,为业务逻辑层提供数据支持。
所有这些组件都部署在Ubuntu操作系统上,确保系统兼容性和稳定性。