产品的一些架构问题

一,系统中的SOLID 原则,指的是开发者设计软件系统的时候,需遵循:
高内聚、低耦合、可维护、可扩展、健壮五个原则,现有一个文件处理模块,通常的写法如下:

// 低层模块 - 具体的数据存储实现
class FileDataStore {
    func save(data: String, to filename: String) {
        system.manager.save(path.filename, data);
        // 将数据保存到文件
        print("Saving data to file: \(filename)")
    }
}

// 高层模块 - 业务逻辑
class UserManager {
    private let fileStore = FileDataStore()  // 直接依赖于具体实现
    
    func saveUser(_ user: String) {
        fileStore.save(data: user, to: "user_data.txt")
    }
}

在这个设计中,UserManager是业务逻辑模块,直接依赖于 FileDataStore实现模块,将数据保存到文件中。
如果业务层的保存方式修改为保存到数据库中,那么必须去修改UserManager中的代码,例如修改FileDataStore为DataBaseStore,然后执行save方法,这样的实现耦合性高,也就是说代码不够健壮,如果遵循SOLID 原则,写法如下:

// 抽象层 - 定义协议
protocol DataStoreProtocol {
    func save(data: String, to location: String)
}

// 低层模块 - 具体实现
class FileDataStore: DataStoreProtocol {
    func save(data: String, to location: String) {
        system.manager.save(path.filename, data);
        print("Saving data to file: \(location)")
    }
}

// 低层模块 - 另一种具体实现
class DatabaseDataStore: DataStoreProtocol {
    func save(data: String, to location: String) {
        database.table.insert(("filename","fileData"),(path.filename, data));
        print("Saving data to database: \(location)")
    }
}

// 高层模块 - 业务逻辑
class UserManager {
    private let dataStore: DataStoreProtocol  // 依赖于抽象而非具体实现
    
    init(dataStore: DataStoreProtocol) {
        self.dataStore = dataStore
    }
    
    func saveUser(_ user: String) {
        dataStore.save(data: user, to: "users")
    }
}

// 使用文件
let fileStore = FileDataStore()
let userManager = UserManager(dataStore: fileStore)
userManager.saveUser("John Doe")

// 或者数据库
let dbStore = DatabaseDataStore()
let anotherUserManager = UserManager(dataStore: dbStore)
anotherUserManager.saveUser("Jane Doe")

以上代码,底层具体实现模块遵循了数据存储协议,协议中有一个保存接口,然后分别写了文件保存、数据库保存两个具体实现类,在UserManager业务模块中进需要传递一个集成DataStoreProtocol协议的实现类,就可以实现保存,只要实现类遵循了DataStoreProtocol协议,任意的保存方式都可以实现,遵循了SOLID 原则,增加了扩展性。具体好处如下:
1. 解耦:高层模块不再依赖于低层模块的具体实现,依赖的是底层接口(UserManager.init(dataStore: DataStoreProtocol) );
2,可测试性:可以使用模拟对象进行测试(下面会进行测试用例);
3,灵活性:可以轻松更换不同的实现(文件或数据库亦或其他保存方式);
4,可维护性:修改实现不会影响依赖于协议的高层模块,例如DatabaseDataStore内部修改,不影响UserManager业务层,因为UserManager中只是调用了DataStoreProtocol的接口。
还有,就是通过查看DataStoreProtocol,立马就能知道其职责是什么。

可测试性,如果需要对业务进行测试,那么测试用例:

// 测试时可以创建模拟对象
class MockStoreManager: DataStoreProtocol {
    // 用于记录方法调用的属性
    var saveCallCount = 0
    var lastSavedData: String?
    var lastSaveLocation: String?
    var shouldThrowError = false
    func save(data: String, to location: String) {
        saveCallCount += 1
        lastSavedData = data
        lastSaveLocation = location
        if lastSavedData != nil || lastSaveLocation != nil {
                print("保存成功");
                return;
        } else {
                shouldThrowError = true
                print("保存失败");
                return;
        }
    }
    
    // ... 其他协议方法的模拟实现
}

在测试类中,遵循了DataStoreProtocol协议,只是打印了是否保存成功,可以直接方便进行测试,不需要具体实现如何保存。
遵循了倒置原则中的可测试性

倒置原则中的接口隔离原则的设计:
如果以上的DataStoreProtocol协议中,增加了一个数据查询方法,然后将查询出来的数据进行打印:

protocol DataStoreProtocol {
    func save(data: String, to location: String)
    func get(location: String) -> String(Data);
    func print(data: String);
}

当一个某一个具体实现模块(DatabaseDataStore)要对数据进行增删改查,但并不想进行打印,那么DatabaseDataStore遵循DataStoreProtocol协议,我们就强迫DatabaseDataStore实现它们不需要的方法print,如果不去实现则会报错,这样就违反了接口隔离的原则。而且整个DataStoreProtocol协议中的接口范围会更大,对其具体的职责不够清晰,范围太广导致继承类需要实现不需要的方法。接口隔离的原则具体指的是:
1,类只需要实现与其职责相关的接口;
2,修改接口时影响范围更小;
3,接口职责更加明确;
4,更容易为特定功能创建测试;
5,减少不必要的依赖,避免实现和使用不需要的方法

正确的实现方式如下:

// 数据增删改查接口
protocol DataStoreProtocol {
    func save(data: String, to location: String)
    func get(location: String) -> String(Data);
    func update(location: String, data: String) -> bool;
    func delete(location: String, data: String) -> bool;
}

// 数据打印接口
protocol DataPrinterProtocol {
    func print(data: String)
}

// 业务层
class dataDealManager: DataStoreProtocol, DataPrinterProtocol {

    private let dataStore: DataStoreProtocol
    private let printer: DataPrinterProtocol

    func saveUser(_ user: String) {
        dataStore.save(data: user, to: "users")
    }

    func printData(_ data: String) {
        printer.print(data)
    }
}

在实际的设计中,应当:
1,设计小而专注的接口;
2,让类遵循多个专门的接口,而不是一个庞大的接口;
3,遵循单一职责原则,确保每个接口只负责一个方面的职责;
4,在你的分析会话管理器中,可以根据实际使用场景来决定是否将协议拆分得更细;

针对以上这个软件的设计思想,不仅仅是为了能够设计出更健全的软件架构,最重要的其实是可以通过这个思维,延伸到各个工种,更甚者可以延伸到生活中。
例如产品经理:对于一些设计思想的低层记录,最终实现到延展;

Leave a Reply

Required fields are marked *