后端开发面试题3(附答案)

发布于:2024-05-24 ⋅ 阅读:(27) ⋅ 点赞:(0)

前言

        在下首语言是golang,所以会用他作为示例。

原文参见 @arialdomartini的: Back-End Developer Interview Questions

设计模式相关问题

 1. 请用一个例子表明,全局对象是邪恶的存在。

        在Go语言中,虽然没有传统意义上的全局变量(全局对象),但可以通过包级别的变量来模拟全局对象的效果。全局变量在多个函数或包之间共享,有可能导致数据竞争和同步问题,进而引发程序的复杂性和潜在的bug。下面是一个简化的例子,展示全局对象在并发环境下的潜在问题:

package main

import (
	"fmt"
	"sync"
)

// 全局对象模拟
var globalData = make(map[int]int)
var mutex = &sync.Mutex{}

func increment(key int) {
	mutex.Lock()
	defer mutex.Unlock()

	globalData[key]++
	fmt.Printf("Incremented key %d to value %d\n", key, globalData[key])
}

func decrement(key int) {
	mutex.Lock()
	defer mutex.Unlock()

	globalData[key]--
	if globalData[key] < 0 {
		globalData[key] = 0
	}
	fmt.Printf("Decremented key %d to value %d\n", key, globalData[key])
}

func main() {
	go increment(1)
	go increment(1)
	go decrement(1)

	// 这里我们并没有等待goroutine完成,因此输出结果可能并不一致
	// 事实上,即使使用了互斥锁,由于全局对象的并发访问,仍然可能出现竞态条件
}

        在上述代码中,全局对象 globalData 是一个 map,我们在两个并发的goroutine中对其进行增减操作。尽管我们试图通过 mutex 来保护全局对象的并发访问,但在实际运行时,由于goroutine的调度不确定性,依然可能出现竞态条件,使得map中的值并不能保证总是预期的结果。这就是全局对象在并发编程中可能带来的问题。

        在实践中,应尽量避免过度使用全局对象,可以通过依赖注入、局部变量、闭包等方式传递数据,确保数据访问的明确性和可控性。如果确实需要全局状态,也应尽量采用线程安全的数据结构,并确保对其访问的同步控制恰当有效。

2. 假设你工作的系统不支持事务性,你会如何从头开始实现它?

        在不支持事务性的系统中实现事务性支持,通常需要从以下几个方面着手:

事务管理设计

        设计一套事务管理器,用于跟踪事务的状态(开始、进行中、提交、回滚)。

        为每个事务定义原子操作,确保每个操作要么全部执行成功,要么全部失败。

事务边界

        确定事务的开始和结束点,例如在开始事务时记录当前系统状态,在结束时根据事务的结果决定提交或回滚。

数据存储

        为支持事务,数据存储层需要有某种形式的版本控制或者影子副本机制,以便在事务失败时能够还原到事务开始前的状态。

补偿操作

        对于每个事务操作,定义相应的补偿操作(Rollback action),当事务需要回滚时,执行这些补偿操作以撤销已完成的更改。

一致性检查

        在提交事务前,进行一致性检查(如检查数据完整性约束),确保事务执行后的状态满足业务逻辑。

锁定和并发控制

        为了在并发环境下实现事务,需要实现锁定机制,如乐观锁、悲观锁等,以避免脏读、幻读、不可重复读等问题。

日志记录

        事务执行过程中,记录详细的日志信息,以便在出现问题时能够追踪事务的执行历史,以及用于在失败后进行事务恢复。

分布式事务支持

        如果系统是分布式环境,可能需要实现两阶段提交(2PC)、三阶段提交(3PC)或其他分布式事务协议来保证跨节点的一致性。

具体实现时,可以根据系统的具体技术和架构选择合适的方法,例如在数据库层面、应用服务层面或消息中间件层面实现事务控制。对于没有内置事务支持的数据库或数据存储系统,可能需要开发者手动实现以上的事务管理逻辑。

3. 什么是好莱坞原则(Hollywood Principles)?

        好莱坞原则(Hollywood Principle)来源于好莱坞的电影行业,但在软件工程中,这一原则被用来指导组件之间解耦和通信的设计哲学。在计算机编程和面向对象设计(OOD)中,好莱坞原则的主要表述是“Don't call us, we'll call you”(别找我们,我们会去找你)。

        在具体实践中,这意味着高层次的组件或框架(如好莱坞制片人)决定何时以及如何使用低层次的组件或服务(如演员)。低层次组件不需要知道或关心高层次组件的内部运作,只需提供必要的接口或回调函数,等待被调用。这样可以极大地减少组件间的耦合,提高系统的灵活性和可扩展性。

        在依赖注入(Dependency Injection,DI)和 inversion of control(IoC)容器的概念中,好莱坞原则得到了广泛应用。例如,一个对象不直接创建它的依赖对象,而是通过外部容器或框架传递进来,对象只需要声明它需要什么样的服务,而不必关心这些服务的具体实现和生命周期管理。当服务准备好时,容器会按照对象的要求提供服务,实现了“你不用来找我,我会去找你”的设计模式。

4. 关于迪米特法则(最少知识原则): 写一段代码违反它, 然后修复它。

        迪米特法则:the Law of Demeter, 最少知识原则: the Principle of Least Knowledge

        迪米特法则(Law of Demeter, LoD),又称最少知识原则,其基本思想是:一个对象应当对其他对象有最少的了解,也就是说,一个对象应当尽量少地与其他对象发生相互作用。当直接与一个对象互动时,尽量通过其提供的公共接口操作,而不是通过其内部的细节或间接访问其他对象。

        下面是一段违反迪米特法则的Go语言代码示例,以及如何修复它:

违反迪米特法则的代码示例:

package main

type Employee struct {
	Department *Department
}

type Department struct {
	Manager *Manager
}

type Manager struct {
	Name string
}

func (e *Employee) GetManagerName() string {
	return e.Department.Manager.Name // 违反了迪米特法则,Employee直接访问了Department的Manager属性,然后又访问了Manager的Name属性
}

func main() {
	dep := &Department{Manager: &Manager{Name: "John Doe"}}
	emp := &Employee{D