1. 概述
在统一建模语言(UML)中,依赖关系是一种重要的模型元素,用来表示一个事物(比如类、组件或包)依赖于另一个事物的情况。依赖关系通常表示一个事物的定义或实现部分地或完全依赖于另一个事物。
2. 特点
- 方向性:依赖关系是有方向的,表示一个元素使用或依赖另一个元素。
- 动态性:依赖通常表示在运行时或在编译时的关系,不像关联那样通常表示更长久的关系。
- 弱关系:相比于类的关联、聚合或组合,依赖是一种相对较弱的关系,通常用于表示非永久性的使用或交互。
3. 依赖关系的类型
UML 依赖关系可以细分为以下几种类型:
3.1 使用依赖
一个类(客户端)在其方法中使用另一个类(供应商)的实例。例如:
- 一个
Car
类使用Engine
类来提供动力。
cpp
#include <iostream>
class Engine {
public:
void start() {
std::cout << "Engine started" << std::endl;
}
void stop() {
std::cout << "Engine stopped" << std::endl;
}
};
class Car {
public:
Car() {
engine = new Engine();
}
void start() {
engine->start();
}
void stop() {
engine->stop();
}
private:
Engine* engine;
};
int main() {
Car car;
car.start();
car.stop();
return 0;
}
- 一个
ReportGenerator
类使用DataSource
类来获取数据。
cpp
#include <iostream>
#include <vector>
class DataSource {
public:
virtual ~DataSource() {}
virtual std::vector<int> getData() = 0;
};
class JdbcDataSource : public DataSource {
public:
std::vector<int> getData() override {
std::cout << "Getting data from JDBC data source" << std::endl;
return {1, 2, 3};
}
};
class CsvDataSource : public DataSource {
public:
std::vector<int> getData() override {
std::cout << "Getting data from CSV data source" << std::endl;
return {4, 5, 6};
}
};
class ReportGenerator {
public:
ReportGenerator(DataSource* dataSource) {
this->dataSource = dataSource;
}
void generateReport() {
std::vector<int> data = dataSource->getData();
// ... 生成报表 ...
}
private:
DataSource* dataSource;
};
int main() {
// 使用 JDBC 数据源
JdbcDataSource* jdbcDataSource = new JdbcDataSource();
ReportGenerator reportGenerator1(jdbcDataSource);
reportGenerator1.generateReport();
// 使用 CSV 数据源
CsvDataSource* csvDataSource = new CsvDataSource();
ReportGenerator reportGenerator2(csvDataSource);
reportGenerator2.generateReport();
return 0;
}
3.2 创建依赖
一个类负责创建另一个类的实例。例如:
- 一个
Factory
类负责创建Product
类的实例。
cpp
#include <iostream>
class Product {
public:
virtual ~Product() {}
virtual void doSomething() = 0;
};
class ConcreteProductA : public Product {
public:
void doSomething() override {
std::cout << "ConcreteProductA do something" << std::endl;
}
};
class ConcreteProductB : public Product {
public:
void doSomething() override {
std::cout << "ConcreteProductB do something" << std::endl;
}
};
class Factory {
public:
virtual ~Factory() {}
virtual Product* createProduct() = 0;
};
class ConcreteFactoryA : public Factory {
public:
Product* createProduct() override {
return new ConcreteProductA();
}
};
class ConcreteFactoryB : public Factory {
public:
Product* createProduct() override {
return new ConcreteProductB();
}
};
int main() {
Factory* factory = new ConcreteFactoryA();
Product* productA = factory->createProduct();
productA->doSomething();
factory = new ConcreteFactoryB();
Product* productB = factory->createProduct();
productB->doSomething();
return 0;
}
- 一个
Document
类负责创建Paragraph
类的实例。
cpp
#include <iostream>
#include <vector>
class Section {
public:
Section(const std::string& text) {
this->text = text;
}
void print() {
std::cout << text << std::endl;
}
private:
std::string text;
};
class Document {
public:
void addSection(const std::string& text) {
sections.push_back(new Section(text));
}
void print() {
for (Section* section : sections) {
section->print();
}
}
private:
std::vector<Section*> sections;
};
int main() {
Document document;
document.addSection("This is the first section.");
document.addSection("This is the second section.");
document.print();
return 0;
}
3.3 参数依赖
一个类的方法接受另一个类的实例作为参数。例如:
- 一个
FileReader
类的方法接受一个File
类的实例作为参数。
cpp
#include <iostream>
#include <fstream>
class File {
public:
File(const std::string& path) {
this->path = path;
}
const std::string& getPath() const {
return path;
}
private:
std::string path;
};
class FileReader {
public:
void readFile(const File& file) {
std::ifstream ifs(file.getPath());
if (ifs.is_open()) {
std::string line;
while (getline(ifs, line)) {
std::cout << line << std::endl;
}
ifs.close();
} else {
std::cout << "Error opening file" << std::endl;
}
}
};
int main() {
File file("C:/test.txt");
FileReader fileReader;
fileReader.readFile(file);
return 0;
}
- 一个
List
类的Add
方法接受一个要添加的元素作为参数。
cpp
#include <iostream>
#include <vector>
template <typename T>
class List {
public:
void add(const T& element) {
elements.push_back(element);
}
void print() {
for (const T& element : elements) {
std::cout << element << " ";
}
std::cout << std::endl;
}
private:
std::vector<T> elements;
};
int main() {
List<int> list;
list.add(1);
list.add(2);
list.add(3);
list.print();
return 0;
}
3.4 返回值依赖
一个类的方法返回另一个类的实例。例如:
- 一个
Database
类的方法返回一个Connection
类的实例。
cpp
#include <iostream>
class Connection {
public:
Connection() {
std::cout << "Creating connection" << std::endl;
}
~Connection() {
std::cout << "Closing connection" << std::endl;
}
};
class Database {
public:
Connection* getConnection() {
return new Connection();
}
};
int main() {
Database database;
Connection* connection = database.getConnection();
// 使用连接...
delete connection;
return 0;
}
- 一个
Parser
类的方法返回一个Document
类的实例。
cpp
#include <iostream>
#include <fstream>
class Document {
public:
Document(const std::string& text) {
this->text = text;
}
void print() {
std::cout << text << std::endl;
}
private:
std::string text;
};
class Parser {
public:
Document* parse(const std::string& filePath) {
std::ifstream ifs(filePath);
if (ifs.is_open()) {
std::string text;
while (getline(ifs, text)) {
// ... 解析文本 ...
}
ifs.close();
return new Document(text);
} else {
std::cout << "Error opening file" << std::endl;
return nullptr;
}
}
};
int main() {
Parser parser;
Document* document = parser.parse("C:/test.txt");
if (document != nullptr) {
document->print();
delete document;
}
return 0;
}
3.6 绑定依赖
指模板实例化时,将模板类绑定到具体的参数上。
类模板是指可以生成不同类型对象的类。类模板的实例化需要指定具体的类型参数。如果一个类的模板实例化依赖于另一个类,那么就表示该类模板依赖于另一个类。例如:
- 一个
List
类模板可以生成不同类型元素的列表。
cpp
#include <iostream>
#include <vector>
template <typename T>
class List {
public:
void add(const T& element) {
elements.push_back(element);
}
void print() {
for (const T& element : elements) {
std::cout << element << " ";
}
std::cout << std::endl;
}
private:
std::vector<T> elements;
};
int main() {
// 生成 int 型元素的列表
List<int> list1;
list1.add(1);
list1.add(2);
list1.add(3);
list1.print();
// 生成 string 型元素的列表
List<std::string> list2;
list2.add("Hello");
list2.add("World");
list2.add("!");
list2.print();
return 0;
}
- 一个
Map
类模板可以生成不同类型键值对的映射。
cpp
#include <iostream>
#include <map>
template <typename K, typename V>
class Map {
public:
void add(const K& key, const V& value) {
elements[key] = value;
}
void print() {
for (const auto& pair : elements) {
std::cout << pair.first << " -> " << pair.second << std::endl;
}
}
private:
std::map<K, V> elements;
};
int main() {
// 生成 int 型键值对的映射
Map<int, int> map1;
map1.add(1, 10);
map1.add(2, 20);
map1.add(3, 30);
map1.print();
// 生成 string 型键值对的映射
Map<std::string, std::string> map2;
map2.add("Hello", "World");
map2.add("Goodbye", "Cruel World");
map2.print();
return 0;
}
3.7 实现依赖
实现关系 是指一个类(实现类 )承诺实现另一个类(接口)定义的契约。接口定义了一组方法和属性,但并不提供具体的实现,而是描述了一种行为规范。实现类则提供了这些方法和属性的具体实现。例如:
- 一个
Animal
类定义一个Speak
接口。 - 一个
Dog
类实现Animal
接口,并提供具体的Speak
方法实现。
cpp
#include <iostream>
class Animal {
public:
virtual void speak() = 0; // 纯虚函数,没有具体实现
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
int main() {
Dog dog;
dog.speak();
return 0;
}
实现关系的特点:
- 契约: 接口定义了一种行为规范,实现类必须遵守该规范。
- 实现: 实现类提供接口定义的方法和属性的具体实现。
- 依赖: 实现类依赖于接口,因为需要根据接口定义来实现其功能。
- 多态: 接口可以被多个类实现,从而提供不同的实现方式。
3.8 扩展依赖
扩展关系是一种 UML 关系类型,表示一个用例(扩展用例)在某些条件下扩展另一个用例(基本用例)的功能。扩展关系通常用于表示可选的、非必须的功能。
扩展关系的关键点:
- 扩展点: 基本用例中定义的点,扩展用例可以在该点插入其行为。
- 可选性: 扩展用例不是基本用例必需的,只有在满足特定条件时才会执行。
- 行为插入: 扩展用例的行为插入到基本用例的流程中,修改或补充基本用例的行为。
- 类型: 扩展关系可以是包含 或扩展两种类型。
扩展关系的应用场景:
- 可选功能: 为基本用例添加可选功能,例如日志记录、错误处理等。
- 条件性功能: 仅在满足特定条件时才执行的功能,例如权限控制、数据验证等。
- 行为变异: 在不同情况下对基本用例行为进行不同的变异,例如不同的用户界面、不同的处理流程等。
扩展关系的优点
- 灵活性: 扩展关系可以使代码更加灵活,可以根据需要动态添加或删除功能。
- 可扩展性: 扩展关系可以使代码更加可扩展,可以方便地添加新的功能。
- 可维护性: 扩展关系可以使代码更加模块化和可维护性,可以将可选功能或条件性功能从基本用例中分离出来。
扩展关系与其他关系的比较:
- 包含关系: 包含关系表示一个用例包含另一个用例的所有功能,扩展关系则表示扩展用例只在某些情况下扩展基本用例的部分功能。
- 继承关系: 继承关系表示一个类继承另一个类的所有属性和方法,扩展关系则表示两个用例之间是一种依赖关系。
扩展关系示例:
- 一个 登录 用例可以扩展一个 安全检查 用例,在需要进行安全检查时才执行安全检查功能。
- 一个 下单 用例可以扩展一个 优惠计算 用例,在满足优惠条件时才计算优惠价格。
- 一个文件上传用例可以扩展一个病毒扫描用例,在上传文件之前进行病毒扫描。
cpp
#include <iostream>
class IControl {
public:
virtual void Click() = 0;
protected:
virtual void DoClick() = 0;
};
class Button : public IControl {
public:
void Click() {
std::cout << "按钮被点击了!" << std::endl;
DoClick();
}
protected:
void DoClick() override {
std::cout << "按钮执行点击操作!" << std::endl;
}
};
class Security {
public:
virtual bool VerifyUser() = 0;
protected:
virtual void DoVerifyUser() = 0;
};
class LoginUseCase {
public:
void Execute() {
std::cout << "登录用例正在执行..." << std::endl;
// ...
// 检查是否需要安全检查。
if (isSecurityCheckNeeded) {
Security security;
if (!security.VerifyUser()) {
std::cout << "登录失败:身份验证失败!" << std::endl;
return;
}
}
// ...
}
private:
bool isSecurityCheckNeeded = true;
};
int main() {
LoginUseCase loginUseCase;
loginUseCase.Execute();
return 0;
}
Control 类
- Control 类是一个抽象类,代表一种控件。
- Control 类定义了一个虚方法 Click(),用于触发控件的点击操作。
Button 类
- Button 类继承自 Control 类,代表一种按钮控件。
- Button 类实现了 Click() 方法,用于执行按钮的点击操作。
- 当用户点击按钮时,Button 类会执行 Click() 方法,并触发相应的业务逻辑。
Security 类
- Security 类继承自 Control 类,代表一种安全控制。
- Security 类没有实现 Click() 方法,因为它不是用于点击操作的控件。
- Security 类可以提供其他方法,用于执行安全检查操作。
LoginUseCase 类
- LoginUseCase 类是一个用例,代表登录用例。
- LoginUseCase 类使用 Button 类和 Security 类来实现其功能。
- LoginUseCase 类根据需要动态添加或删除 Button 类和 Security 类。
cpp
#include <iostream>
class Order {
public:
virtual double calculatePrice() = 0; // 纯虚函数,没有具体实现
protected:
double originalPrice;
public:
Order(double originalPrice) {
this->originalPrice = originalPrice;
}
};
class DiscountCalculator {
public:
virtual bool isEligibleForDiscount(const Order& order) = 0; // 纯虚函数,没有具体实现
virtual double calculateDiscount(const Order& order) = 0; // 纯虚函数,没有具体实现
};
class OrderWithDiscount : public Order {
public:
OrderWithDiscount(double originalPrice) : Order(originalPrice) {}
double calculatePrice() override {
DiscountCalculator calculator;
if (calculator.isEligibleForDiscount(*this)) {
return originalPrice - calculator.calculateDiscount(*this);
} else {
return originalPrice;
}
}
};
class SimpleDiscountCalculator : public DiscountCalculator {
public:
bool isEligibleForDiscount(const Order& order) override {
return order.originalPrice >= 100;
}
double calculateDiscount(const Order& order) override {
return order.originalPrice * 0.1;
}
};
int main() {
Order* order = new OrderWithDiscount(120);
std::cout << "原价:" << order->originalPrice << std::endl;
std::cout << "优惠价:" << order->calculatePrice() << std::endl;
return 0;
}
在这个例子中,OrderWithDiscount 类扩展了 Order 类,并使用了 DiscountCalculator 类来计算优惠价格。
cpp
#include <iostream>
#include <fstream>
class File {
public:
virtual bool isInfected() = 0; // 纯虚函数,没有具体实现
protected:
std::string path;
public:
File(const std::string& path) {
this->path = path;
}
};
class VirusScanner {
public:
virtual bool scan(const File& file) = 0; // 纯虚函数,没有具体实现
};
class FileUpload {
public:
virtual void upload(const File& file) = 0; // 纯虚函数,没有具体实现
};
class FileUploadWithVirusScan : public FileUpload {
public:
FileUploadWithVirusScan() {}
void upload(const File& file) override {
VirusScanner scanner;
if (!scanner.scan(file)) {
std::cout << "文件 " << file.path << " 含有病毒,无法上传" << std::endl;
return;
}
// 上传文件...
std::cout << "文件 " << file.path << " 上传成功" << std::endl;
}
};
class SimpleVirusScanner : public VirusScanner {
public:
bool scan(const File& file) override {
std::ifstream ifs(file.path);
if (ifs.is_open()) {
// 扫描文件内容...
return true;
} else {
return false;
}
}
};
int main() {
File* file = new File("C:/test.txt");
FileUploadWithVirusScan uploader;
uploader.upload(*file);
return 0;
}
在这个例子中,FileUploadWithVirusScan 类扩展了 FileUpload 类,并使用了 VirusScanner 类来扫描文件是否含有病毒。
4. 依赖关系的表示
假设有一个ReportGenerator类,它负责生成报告,并使用DataSource类来获取所需的数据。在这种情况下,ReportGenerator依赖于DataSource,因为它需要DataSource提供的数据来完成其功能。在UML图中,这种依赖关系会用一条从ReportGenerator指向DataSource的带有开放箭头的虚线来表示。
5. 注意事项
- 依赖关系应该尽可能地弱,以便提高代码的模块性和可维护性。
- 循环依赖关系应该避免,因为它会导致代码难以理解和维护。
- 依赖关系应该在设计阶段仔细考虑,并进行必要的调整和优化。