分享议程
-
问题痛点:我们曾经如何管理MQTT主题
-
解决方案:主题工厂模式的演进之路
-
架构优势:四大核心价值深度解析
-
实战扩展:生产环境进阶用法
-
最佳实践:总结与落地建议
一、 问题痛点:字符串拼接的困境
曾经的代码现状
Kotlin
// 场景1:直接拼接 - 最原始的方式
val topic1 = "robots/control/" + deviceId
// 场景2:模板字符串 - 稍好但仍存在问题
val topic2 = "robots/screen/${model}/response/${screenId}"
// 场景3:String.format - 格式复杂时难以维护
val topic3 = String.format("robots/notification/%d/status", deviceId)
面临的四大痛点
Kotlin
// 1. 格式不统一 - 不同开发者写法各异
val topicA = "robots/control/" + id
val topicB = "robots/control/${id}"
val topicC = "robots/control/%d".format(id)
// 2. 修改困难 - 主题格式变更需要全局搜索替换
// 从 "robots/control/123" 改为 "v2/robots/control/123"
// 😱 需要修改所有拼接处!
// 3. 没有编译保障 - 拼写错误到运行时才发现
val topic = "robots/controll/123" // 少了个r,直到运行时才报错
// 4. 重复代码 - 相同逻辑散落各处
fun buildControlTopic(id: Int): String {
return "robots/control/$id"
}
// 多个文件中都有类似的构建函数
二、解决方案:主题工厂模式的演进
第一版:常量集中管理
Kotlin
object MqttTopics {
// 基础主题常量
const val ROBOT_CONTROL = "robots/control"
const val ROBOT_RESPONSE = "robots/response"
const val ROBOT_NOTIFICATION = "robots/notification"
// 使用方式
val topic = "$ROBOT_CONTROL/$deviceId"
}
进步 :统一了基础路径
不足:拼接逻辑仍然分散
第二版:基础构建方法
Kotlin
object MqttCommandConstants {
private const val TOPIC_ROBOT_CONTROL = "robots/control"
fun getControlTopic(sourceId: Int): String {
return "$TOPIC_ROBOT_CONTROL/$sourceId"
}
}
进步 :封装了构建逻辑
不足:缺乏业务语义
第三版:完整工厂模式
Kotlin
object MqttCommandConstants {
// 🏗️ 分层架构设计
// 1. 模板常量层 - 私有化保护
private const val TOPIC_ROBOT_CONTROL = "robots/control"
private const val TOPIC_ROBOT_RESPONSE = "robots/response"
private const val TOPIC_SCREEN_BASE = "robots/screen"
// 2. 基础构建层 - 核心构建逻辑
fun getControlTopic(sourceId: Int): String {
return "$TOPIC_ROBOT_CONTROL/$sourceId"
}
// 3. 领域专用层 - 业务语义化
fun getScreenControlTopic(model: String, sourceId: Int): String {
return "$TOPIC_SCREEN_BASE/$model/control/$sourceId"
}
fun getScreenResponseTopic(model: String, sourceId: Int): String {
return "$TOPIC_SCREEN_BASE/$model/response/$sourceId"
}
}
三、架构优势:四大核心价值
优势1: 运行时灵活性
Kotlin
// 型号作为参数传入,支持动态配置
class DeviceManager {
fun publishCommand(deviceModel: String, deviceId: Int, command: Command) {
val topic = MqttCommandConstants.getScreenControlTopic(deviceModel, deviceId)
mqttClient.publish(topic, command)
}
}
// 支持运行时决定的型号
val userSelectedModel = getUserPreference().screenModel // "m1" 或 "m2"
val topic = MqttCommandConstants.getScreenControlTopic(userSelectedModel, 444)
// 支持配置化的型号管理
val configuredModels = listOf("m1", "m2", "m3-pro")
configuredModels.forEach { model ->
val topic = MqttCommandConstants.getScreenControlTopic(model, 444)
// 为所有型号创建主题
}
优势2: 编译时类型安全
Kotlin
// ✅ 编译时安全保障
val topic1 = MqttCommandConstants.getControlTopic(123) // 正确
val topic2 = MqttCommandConstants.getScreenControlTopic("m1", 444) // 正确
// ❌ IDE即时报错 - 错误的参数类型
// val topic3 = MqttCommandConstants.getControlTopic("123") // 编译错误
// val topic4 = MqttCommandConstants.getScreenControlTopic(444, "m1") // 编译错误
// 🎯 IDE智能支持
// • 自动补全:输入 "MqttCommandConstants.get" 显示所有可用方法
// • 参数提示:明确显示参数名称和类型
// • 引用查找:快速找到所有使用处
优势3: 便于测试和维护
Kotlin
class MqttCommandConstantsTest {
@Test
fun `should build correct control topic`() {
// When
val topic = MqttCommandConstants.getControlTopic(123)
// Then
assertThat(topic).isEqualTo("robots/control/123")
}
@ParameterizedTest
@CsvSource("m1,444", "m2,555", "m3-pro,666")
fun `should build screen topics for all models`(model: String, sourceId: Int) {
// When
val controlTopic = MqttCommandConstants.getScreenControlTopic(model, sourceId)
val responseTopic = MqttCommandConstants.getScreenResponseTopic(model, sourceId)
// Then
assertThat(controlTopic).isEqualTo("robots/screen/$model/control/$sourceId")
assertThat(responseTopic).isEqualTo("robots/screen/$model/response/$sourceId")
}
@Test
fun `should maintain topic consistency`() {
// 验证所有主题遵循相同模式
val topics = listOf(
MqttCommandConstants.getControlTopic(123),
MqttCommandConstants.getResponseTopic(123),
MqttCommandConstants.getScreenControlTopic("m1", 444)
)
// 所有主题都应该符合基本格式要求
topics.forEach { topic ->
assertThat(topic).contains("/")
assertThat(topic).doesNotContain(" ") // 无多余空格
}
}
}
优势4: 轻松切换不同型号
Kotlin
// 业务代码无需关心具体型号
class MessageRouter {
fun routeToScreen(message: Message, screenModel: String) {
val topic = MqttCommandConstants.getScreenControlTopic(screenModel, message.deviceId)
publish(topic, message)
}
}
// 型号升级无缝衔接
class ScreenModelUpgrader {
fun upgradeDevice(oldModel: String, newModel: String, deviceId: Int) {
// 停止旧型号主题
val oldTopic = MqttCommandConstants.getScreenControlTopic(oldModel, deviceId)
unsubscribe(oldTopic)
// 开启新型号主题
val newTopic = MqttCommandConstants.getScreenControlTopic(newModel, deviceId)
subscribe(newTopic)
logger.info("设备升级: $oldTopic -> $newTopic")
}
}
四、 实战扩展:生产环境进阶用法
扩展1:主题验证与安全
Kotlin
object MqttCommandConstants {
fun getScreenControlTopic(model: String, sourceId: Int): String {
require(model.isNotBlank()) { "型号不能为空" }
require(model.matches(Regex("[a-z0-9-]+"))) { "型号格式不正确: $model" }
require(sourceId > 0) { "设备ID必须大于0" }
return "$TOPIC_SCREEN_BASE/$model/control/$sourceId"
}
}
扩展2:主题解析与反向操作
Kotlin
// 从主题中提取参数信息
data class TopicInfo(
val model: String,
val action: String,
val sourceId: Int,
val fullTopic: String
)
object MqttTopicParser {
fun parseScreenTopic(topic: String): TopicInfo? {
val pattern = Regex("$TOPIC_SCREEN_BASE/([^/]+)/([^/]+)/(\\d+)")
return pattern.matchEntire(topic)?.let { match ->
TopicInfo(
model = match.groupValues[1],
action = match.groupValues[2],
sourceId = match.groupValues[3].toInt(),
fullTopic = topic
)
}
}
}
// 使用示例
val topic = "robots/screen/m1/control/444"
val info = MqttTopicParser.parseScreenTopic(topic)
// info: TopicInfo(model="m1", action="control", sourceId=444)
扩展3:多租户支持
Kotlin
object AdvancedMqttTopics {
// 支持多租户隔离
fun getTenantControlTopic(tenantId: String, sourceId: Int): String {
return "tenants/$tenantId/robots/control/$sourceId"
}
// 支持环境隔离
fun getEnvControlTopic(environment: String, sourceId: Int): String {
return "$environment/robots/control/$sourceId"
}
}
五、最佳实践总结
✅ 推荐做法
Kotlin
// 1. 主题模板私有化 - 防止外部误用
private const val TOPIC_BASE = "robots"
// 2. 构建方法语义化 - 方法名体现业务含义
fun getRobotStatusTopic(robotId: Int) = "$TOPIC_BASE/status/$robotId"
// 3. 参数验证前置 - 尽早发现问题
fun getValidatedTopic(deviceId: Int): String {
require(deviceId in 1..9999) { "设备ID范围错误" }
return "$TOPIC_BASE/control/$deviceId"
}
// 4. 提供便捷方法 - 常用场景快捷方式
fun getM1ScreenControl(sourceId: Int) = getScreenControlTopic("m1", sourceId)
❌ 避免做法
Kotlin
// 1. 避免业务代码直接拼接
// ❌ val topic = "base/" + type + "/" + id
// ✅ val topic = TopicFactory.getTopic(type, id)
// 2. 避免主题格式硬编码
// ❌ fun getTopic(id: Int) = "fixed/path/$id"
// ✅ fun getTopic(id: Int) = "$CONFIGURABLE_BASE/$id"
// 3. 避免重复构建逻辑
// ❌ 在每个Service中重复写构建逻辑
// ✅ 统一在TopicFactory中管理
落地实施步骤
-
第一步:识别现有代码中的主题拼接点
-
第二步:创建基础的主题工厂类
-
第三步:逐步替换字符串拼接为工厂方法
-
第四步:添加单元测试保障正确性
-
第五步:团队推广和代码规范约束
六、总结升华
核心思想
"把变化封装在工厂里,把稳定留给业务代码"
技术成长的体现
-
初级:会写代码实现功能
-
中级:会设计可维护的代码结构
-
高级:会构建适应变化的架构
最终收益
Kotlin
// 从散落的字符串拼接
val topic = "robots/screen/" + model + "/control/" + id
// 到统一的设计模式
val topic = MqttCommandConstants.getScreenControlTopic(model, id)
// 收获的是:
// 🎯 更好的可维护性
// 🛡️ 更高的代码可靠性
// 🔧 更强的适应变化能力
// 🚀 更快的开发效率