写点什么

如何学好设计,做好架构? 设计原则才是关键,精通 android 游戏开发

用户头像
Android架构
关注
发布于: 10 小时前

public String getColor() {return "red";}}


IFruit fruit = new Apple();fruit.getColor();


通过声明的IFruit类型可以对其实现类Apple进行编程,好处就是扩展性强,当需要替换具体实现Apple时,对IFruit`的操作完全不用改


合理运用多态可以写出易扩展的代码,基于接口而非实现编程开闭原则的核心


抽象


抽象的目的是为了隐藏方法的具体实现,让调用者只需要关心方法提供了哪些方法(功能),并不需要知道这些功能是如何实现的。在Java中体现方式是接口抽象类


接口和抽象类的区别


  • 接口更侧重于功能的设计,并且能将具体实现与调用者隔离,一般要以接口隔离原则设计接口既粒度越细越好

  • 抽象类更侧重于提升复用性,在原有的基础上预留扩展点供开发者灵活实现

  • 区别:接口可以降低模块间耦合性,抽象类可提升复用性。

  • 相同点:均有较好的扩展性,符合开闭原则


tips


面向对象的四大特性相信大家都很熟悉,本小结只是帮大家做一次简单的回忆,关于其背景职责下半问会详细描述

1.2?诞生背景

谈及面向对象必定磨不开面向过程,毕竟它就是由面向过程衍变而来,吸收其大部分优点并解决其痛点。那什么是面向过程呢?基本定义如下:


分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,更侧重于功能的设计。代表语言 C


用代码体现就是下面这样:


#java 版面向过程


public?class?Wallet {/**?余额/int?balance;/?存钱/void?saveMoney(int?money){balance?+=?money;}/*?花钱*/void?spendMoney(int?money){balance?-=?money;}}


无权限修饰符将内部信息全部暴露,简单粗暴很符合初级程序员的思维,但带来的问题很明显,外部可直接访问balance修改钱包内余额,现象就是"我钱包都没掏出来但里面钱却变少/多了"面向过程在开发中带来的问题远不止这些,所以在此背景下诞生了面向对象 通过面向对象封装特性将面向过程代码做个改进,如下:


#java 版面向对象


public?class?Wallet {/**?余额/private?int?balance;/?存钱/void?saveMoney(int?money){balance?+=?money;}/*?花钱*/void?spendMoney(int?money){balance?-=?money;}}


通过封装特性将balance通过private修饰,这样外部就没有权限直接修改金额,避免误操作带来的未知风险,满足松耦合特性 面向过程编程偏向于功能的开发,简单粗暴难以维护。而面向对象在编程之前需要基于四大特性对功能做建模设计,可以提高代码安全性、复用性、扩展性,更易于维护 既然面向对象这么智能为什么面向过程语言还没有被淘汰?其实面向对象语言的智能是针对我们开发者的,为了能让我们能写出易于维护的代码会多做一步设计,虽然离开发者更了?但离机器确了,毕竟机器只认识 0 和 1 而已。C 语言规则简单易于形成机器码,所以执行效率高,这也是其没有被淘汰的原因。


小提示:


不要以为用了面向对象语言写出的就是面向对象代码,如果没有利用其特性那可能还是面向过程,比如没有利用权限修饰符、一个类一把梭等等....

2.?六大设计原则才是一切设计的基石

设计原则是基于面向对象思想衍变出来的一些规则,用来解决实际开发中的一些痛点,是所有设计的底层思想,也是我个人认为是设计/架构领域最重要的知识,所以请大家务必掌握好

2.1?单一设计原则

单一原则很好理解,指一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性。


案例:本地获取用户信息,提交到网络


fun post(){//创建数据库访问对象 Daoval userDao = ...(这一过程很复杂)


//从本地获取 val age = dao.getAge()val name = dao.getName()//....省略大量字段


//将个人信息提交至网络 http.request(age,name,....)}


以上案例将创建、获取、提交三步操作写到同一个函数中,很显然违背了单一设计原则,面临的问题也很明显,当修改创建、获取、提交任一过程时都会影响到其他二者,千万不要说"我注意一点就不会出错"这种话,因为人不是机器改动就可能出错,此时可以通过单一设计原则做一次重构,代码如下:


fun getUserDao():UserDao{...return dao}


fun getUserInfo():UserInfo{val dao = getUserDao()val userInfo = UserInfo()userInfo.age = dao.getAge()userInfo.name = dao.getName()...return userInfo}


fun post(){val userInfo = getUserInfo()//将个人信息提交至网络 http.request(userInfo.age,userInfo.name,....)}


三步操作被拆至三个函数 互不影响,从根本上杜绝因改动带来的一系列问题。所以使用面向对象语言开发时,不要急着写代码,要优先考虑下模块、类、函数...的设计是否


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


足够单一

2.2?开闭原则

一句话概括开闭原则:对扩展开放,修改关闭。它即充分诠释抽象、多态特性,又是多数行为型设计模式的基础,遍布于各大优秀框架之中,是最重要的一条设计原则,仅这一条原则就能把你的设计能力提高40%


举个例子让大家感受一下:


需求:通过 SQLite 做CRUD操作


class SQLiteDao{public void insert() {//通过 SQLite 做 insert}public void delete() {//通过 SQLite 做 insert}}


SQLiteDao dao = new SQLiteDao();dao.insert();...


以上是最简单粗暴的写法,但存在一个致命问题,如果某一天想替换SQLite业务层基本要动一遍,改动就存在出错的可能,并且需要做大量的重复操作


面对以上问题可以利用抽象、多态特性基于开闭原则做出重构,代码如下:


interface IDao{void insert();void delete();}


class SQLiteDao implements IDao{@Overridepublic void insert() {//通过 SQLite 做 insert}@Overridepublic void delete() {//通过 SQLite 做 insert}}


class RoomDao implements IDao{@Overridepublic void insert() {//通过 Room 做 insert}@Overridepublic void delete() {//通过 Room 做 delete}}


//扩展点 IDao dao = new SQLiteDao();dao.insert();


  • 定义功能接口IDao

  • 定义类SQLiteDao、RoomDao并实现 IDao 的功能

  • 业务层基于接口IDao进行编程


重构后,当需要将SQLite替换至Room时,只需将注释扩展点SQLiteDao替换成RoomDao即可,其他地方完全不用改动。这就是所谓的扩展开放,修改关闭


业务不断迭代情况下,唯一不变的就是改变,这种背景下我们能做的只有在代码中基于开闭原则多留扩展点以不变应万变。

2.3?迪米特法则

基本概念:不该有直接依赖关系的模块不要有依赖。有依赖关系的模块之间,尽量只依赖必要的接口。


迪米特法则很好理解并且非常实用,违背迪米特法则会产生什么问题?还以2.1面向过程代码举例:


class Wallet{/**


  • 余额*/int balance;


/**


  • 存钱*/void saveMoney(int money){balance += money;}


/**


  • 花钱*/void spendMoney(int money){balance -= money;}}


Wallet的设计违背了迪米特法则,毕竟外部只需要savespend功能,将balance暴漏使用者就有权限直接修改其值,可能会对整个Wallet功能造成影响。此时应基于迪米特法则Wallet进行改造,将balance通过封装特性增加private修饰符


迪米特法则单一设计原则很像,前者符合松耦合后者符合高内聚

2.4?接口隔离原则

基本概念:接口的调用者不应该依赖它不需要的接口。


乍一看与迪米特法则很相似。先来看下什么样的接口违背接口隔离原则


interface Callback{/**


  • 点击事件回调方法/void clickCallback();/*

  • 滚动事件回调方法*/void scrollCallback();}


接口Callback包含点击、滚动两个回调方法,面临的问题有两个:


  • 某些特定场景使用者只需要依赖点击回调,那滚动回调便成了多余,把外部不需要的功能暴露出来就存在误操作的可能。

  • 点击滚动本来就是两种特性,强行揉到一块只能让接口更臃肿,进而降低其复用性


根据接口隔离原则改造后如下:


interface ClickCallback{/**


  • 点击事件回调方法*/void clickCallback();}


interface ScrollCallback{/**


  • 滚动事件回调方法*/void scrollCallback();}


基于单一设计原则点击滚动拆分成两个接口,将模块间隔离的更彻底。并且由于粒度更细,所以复用性也更高


接口隔离原则迪米特法则目的很相似,都可以降低模块间依赖关系。但接口隔离更侧重于设计单一接口,提升复用性并间接降低模块间依赖关系,而迪米特法则是直接降低模块间依赖关

2.5?里氏替换原则

基本概念:


设计子类的时候,要遵守父类的行为约定。父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。


里氏替换非常简单并且很容易遵守,在使用继承时,允许复写父类方法,但不要改变其功能。比如自定义View,子类的onMeasure中一定要调用setMeasureaDimission()方法(或者直接使用 super),否则会影响父类方法功能(会抛异常),也既违背了里氏替换原则。

2.6?依赖倒置原则

控制反转: 提及依赖倒置便不得不提控制反转,一句话概括:将复杂的程序操作控制权由程序员交给成熟的框架处理,程序员->成熟的框架为反转,框架应暴露出扩展点由程序员实现 想详细了解可至 [关于 Android 架构,你是否还在生搬硬套?](


) 2.1章节查看


什么是依赖倒置?


高层模块(使用者)不应依赖低层模块(被使用者),它们共同依赖同一个抽象,抽象不要依赖具体实现细节,具体实现细节依赖抽象。


其实核心点就是基于接口而非实现编程2.2数据库案例也符合依赖倒置原则,高层模块(业务层)不依赖于低层模块(SQLiteDao/RoomDao),而是依赖于抽象(IDao),可见依赖倒置也是开闭原则扩展而来。 区别是依赖倒置更侧重于指导框架的设计,框架层应该尽量将更多的细节隐藏在内部,对外只暴露抽象(抽象类/接口),指导框架设计这方面核心就是控制反转

3.?设计模式只是设计原则的产物而已

设计模式共有 23 种,详细描述都能出一本书出来。本小结仅会分享一些通用的思路,个人认为还是比较硬核的,毕竟设计主要还是思想,而非生搬硬套

3.1?设计模式该怎么去学?

本小节会分析几个常见的设计模式核心思想以及设计背景,用于抛砖引玉


工厂模式


基本概念:用于创建复杂对象


创建复杂对象常规写法如下:


class B{...}


class D{void test(){B b = ....(创建 B 的过程很复杂)...}}


在使用的地方直接创建,如果直接new倒也没啥问题,但如果创建过程过于复杂,当修改创建过程时就会影响到test(),进而存在一些未知的隐患。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
如何学好设计,做好架构? 设计原则才是关键,精通android游戏开发