面向对象编程之SOLID原则

Android面试技术要点汇总
2021-05-17 14:29 · 阅读时长11分钟
小课

SOLID是由五个原则的首字母组成,这五个原则分别是单一职责原则(Single Responsibility),开闭原则(Open-Closed),里氏替换原则(Liskov Substitution),接口隔离原则(Interface Segregation)和依赖反转原则(Dependency Inversion)。

遵循SOLID原则进行开发,可以让代码有更好的扩展性,更高的可读性,更容易维护,当你需要维护一个长期项目时,这些优点就会变得极其明显。

单一职责原则(Single Responsibility)

一个类Class应该专注一件事,它也只会因为这件事而进行修改。如果一个类负责有很多件事/功能,将会增加代码的复杂度,因为不同的功能而进行修改这个类很可能会影响到其它的功能。

1class ViewModel {
2
3    fun getShopList() {
4        val res = queryShop()
5        if (res.success) {
6            updateUI(res.data)
7        } else {
8            showToast(res.msg)
9        }
10    }
11
12    //发起http请求获取数据
13    fun queryShop(): Response { }
14
15    //更新列表页面
16    fun updateUI(shopList: List<Shop>) { }
17
18    //展示toast
19    fun showToast(msg: String) { }
20}

ViewModel这个类负责的功能太多,既负责请求接口,处理数据,还负责更新用户界面,很明显违背了单一职责原则。下面我们对其进行改造,将接口请求、数据处理和界面更新这几个不同的功能拆分出去,使得每个接口都满足单一职责原则。

1interface ApiService {
2
3    //发起http请求获取数据
4    fun queryShop(): Response { }
5}
6
7interface FeedUI {
8
9    //更新列表页面
10    fun updateUI(shopList: List<Shop>) { }
11
12    //展示toast
13    fun showToast(msg: String) { }
14}
15
16class ViewModel {
17
18    val apiService: ApiService = ...
19    val ui: FeedUI = ...
20
21    fun getShopList() {
22        val res = apiService.queryShop()
23        if (res.success) {
24            ui.updateUI(res.data)
25        } else {
26            ui.showToast(res.msg)
27        }
28    }
29}
开闭原则(Open-Closed)

一个类Class应该对扩展开放,但是对修改关闭,也就是说在设计类时,要考虑到后续扩展的问题,应该可以在不改变原有类的源码的前提下,也能很方便地扩展功能,这样能够避免因为直接修改源码导致原有已经在使用该类的地方出现问题。

1class ViewModel {
2
3    val apiService: ApiService = ...
4
5    fun getShopList(sortType: SortType) {
6        val res = apiService.queryFeed()
7        when (sortType) {
8            SortType.Price -> sortByPrice(res.data)
9            SortType.Volume -> sortByVolume(res.data)
10        }
11        ...
12    }
13}

ViewModel中,在获取到数据之后,需要对数据进行排序,目前存在两种排序方式,一种是按价格排序,另外一种是按销量排序,如果后续还需要新增排序方式的话,那就只能修改ViewModel源码来完成,如果是以下这种设计,就可以在不修改源码的前提下,扩展新的排序方式。

1interface ISorter {
2    fun sort(shopList: List<Shop>)
3}
4
5class PriceSorter : ISorter {
6    override fun sort(shopList: List<Shop>) {
7        ...
8    }
9}
10
11class ViewModel {
12
13    val apiService: ApiService = ...
14
15    fun getShopList(sorter: ISorter) {
16        val res = apiService.queryFeed()
17        sorter.sort(res.data)
18        ...
19    }
20}
里氏替换原则(Liskov Substitution)

所有父类实例出现的地方,都应该可以使用子类实例替换,且不会产生问题,也就是说子类应该是扩展父类,而不应该修改或者要慎重修改父类原有的实例方法。这里的继承分两种情况

  • 如果父类是抽象类,那么就不会有父类的直接实例,也自然不会违反原则。
  • 如果父类不是抽象类,那么就会存在父类的实例,如果子类对父类的实例方法进行了重写,那么就有可能违反原则。

为什么说重写父类实例方法可能会违反里氏替换原则?看看下面这个例子。

1open class Parent {
2
3    open fun getBaseNumber(): Int {
4        return 10
5    }
6
7    fun calculate(value: Int): Int {
8        return value / getBaseNumber()
9    }
10}
11
12class Child : Parent() {
13
14    override fun getBaseNumber(): Int {
15        return 0
16    }
17}

很明显子类重写父类实例方法,将会导致所有调用父类calculate方法的地方抛出异常,当然这个例子错得很离谱,实际开发中因为重新父类实例方法导致的问题,往往难以发现。遵循里氏替换原则能够避免一些未知的问题。

接口隔离原则(Interface Segregation)

不应该让外部依赖它们用不到的方法,也就是说我们应该根据业务,尽可能地细分我们的接口。接口隔离原则和单一职责原则有些相似,但是侧重点不同,单一职责原则更侧重对内,要求类内部不要有太多、太杂,只应该关注一件事,而接口隔离原则更侧重对外,要求细分接口,对外提供的接口应该是其所能用到的

比如我们有一个配置服务,提供给用户模块和订单模块使用,如果不考虑接口隔离,我们可以这样设计。

1interface IPreference {
2    fun getOrderConfig(): String
3    fun getUserConfig(): String
4}
5
6class PreferenceImpl : IPreference {
7    override fun getOrderConfig() = "order"
8    override fun getUserConfig() = "user"
9}
10
11class UserModule {
12    private val prefs: IPreference = PreferenceImpl()
13
14    fun init() {
15        val config = prefs.getUserConfig()
16    }
17}
18
19class OrderModule {
20    private val prefs: IPreference = PreferenceImpl()
21
22    fun init() {
23        val config = prefs.getOrderConfig()
24    }
25}

对于订单模块来说,它不需要关心用户配置,用不到getUserConfig方法,而对于用户模块来说,它不需要关心订单配置,用不到getOrderConfig方法,这就不太符合接口隔离原则。我们可以这样改造配置服务接口来遵循接口隔离原则。

interface IUserPreference {
    fun getUserConfig(): String
}

interface IOrderPreference {
    fun getOrderConfig(): String
}

至于接口实现类,我们可以根据代码复杂度,来考虑拆分或者仍然共用一个实现类。

1class PreferenceImpl : IUserPreference, IOrderPreference {
2    override fun getOrderConfig() = "order"
3    override fun getUserConfig() = "user"
4}
5//或者分别实现
6class UserPreferenceImpl : IUserPreference {
7    override fun getUserConfig() = "user"
8}
9
10class OrderPreferenceImpl : IOrderPreference {
11    override fun getOrderConfig() = "order"
12}
依赖反转原则(Dependency Inversion)

抽象不依赖具体实现,具体实现应该依赖抽象。不遵循依赖反转原则的开发模式一般是高层模块是依赖低层模块,而遵循依赖反转原则是将高层模块对低层模块的依赖抽象成接口,而底层模块依赖并实现该接口,从而实现依赖反转。

比如说,在开发登录模块时,它依赖低层的http模块,一般实现如下。

1class HttpModule {
2    fun post(body: Body): Response {
3        //http call
4    }
5}
6
7class LoginModule {
8    private val httpModule = HttpModule()
9
10    fun login(username: String, password: String) {
11        val res = httpModule.post(Body.from(username, password))
12    }
13}

为了遵循依赖反转原则,我们将登陆需要抽象为接口ApiService,然后让http模块实现该接口。

1//http module
2class ApiServiceImpl : ApiService {
3
4    override fun login(username: String, password: String): Response {
5        //http call
6        return post(Body.from(username, password))
7    }
8}
9
10//login interface module
11interface ApiService {
12    fun login(username: String, password: String)
13}
14
15//login module
16class LoginModule {
17    private val apiService: ApiService = ApiServiceImpl()
18
19    fun login(username: String, password: String) {
20        val res = apiService.login(username, password)
21    }
22}
SOLID原则设计模式原则