软件设计原则
Posted 妈妈爱编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件设计原则相关的知识,希望对你有一定的参考价值。
软件设计为什么要强调分层?
这是为了应对软件需求的变化来考虑的,软件需求总是在变的,但变化是有规律的,不易变化的需求叫稳定需求,而易变的需求叫不稳定的需求。
在不同层次上应对稳定性不同的需求,在上层设计中响应不稳定的需求,在下层设计中实现稳定的需求,而在分层后的设计中使得上层依赖下层,而不允许下层依赖上层,则可以应对大部分需求变化时对系统的修改量最少。
在一般的MIS系统中,通常数据库结构是最稳定的,轻易不会修改,扩充是有可能的(除非在设计数据库时对用户的业务分析有重大误解),所以通常数据层放在最下层,而业务逻辑也相对稳定但会有变化,所以放在中间层;而最易变的则是表现层(展示层、UI交互界面,每个人的审美不同所致),所以放在最上层。
软件设计为什么要强调耦合度?
关于耦合,这个词在结构化设计中用得很多,在面向对象中,通常讲依赖关系。上层对下层的依赖(或者说强耦合)是理所当然的。比如说,通常应用程序是上层,而操作系统是下层,你能说你编一个应用程序不对操作系统耦合吗?但下层对上层则绝对不能有依赖(或者说零耦合)。没听说过谁为了自己的应用程序能运行而要求微软修改操作系统的。做到这样,你的系统就达到目的了,而完全没有必要为上层对下层的依赖(或者说强耦合)而耿耿于怀。但下层对上层依赖则是一定要避免的,对这要极其重视。
当然你可能会觉得有时会无法避免,如MVC模式(在RUP中叫Entity-Control-Interface模式)中,实体变化后,不是要调用界面层来改变显示吗,这不就是下层依赖上层了吗?
是的,对于这样的逆向依赖,在你设计用例实现过程中没有必要考虑,在进行完初步的设计以后,再统一进行逆向依赖的反转。在面向对象设计中有几个反转这种依赖的手段,在下层定义事件(或消息)由上层来订阅是一种手段,在下层定义接口或抽象类,上层来实现该接口或继承这个抽象类也是一种手段。只要你做到不存在下层对上层的依赖,那么你的系统分层设计的目的就达到了。
软件设计为什么要强调划分业务逻辑和界面的问题?
我觉得这是一个对客户需求的理解和分解的问题,以及对应这些分解后的需求放在哪一层实现的问题,也就是业务逻辑层的职责和界面层的职责划分问题,业务逻辑层的职责应该严格限制在对业务原语的实现上,而界面层的职责则是控制数据的展现形式和将用户的操作翻译或映射到业务原语上。但客户提出的直接需求往往会将这两方面一并提出,并且其中的业务原语往往是隐含的和不明确的,则需要我们在理解用户需求的情况下去分解出哪些是业务原语,哪些是界面操作。
呵呵,太抽象了,举个例吧,简单点,我们常见的用户管理问题,客户说:用户密码都用“*”显示,用户改密码时,输入一遍老密码,输入两遍新密码,当老密码正确且两个新密码相同则修改成功。
显然在这个需求中,用*显示密码是界面层的职责,但业务原语是什么呢?是判断老密码正确且新密码相同吗?不对,业务原语只是用户可以改密码,于是业务逻辑层只要开放一个方法认证当前用户并更改密码就可以了,至于输入两遍并校对相同则是界面层的事。
总之对需求要分析,从中提取出真正的业务原语,当然有时这条界限是很模糊的,是否能正确的识别出业务原语,就看每个人的分析能力和经验了。
设计模式-软件设计的7个原则
设计模式-软件设计的7个原则
概述
在软件开发时为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,通常要遵守一定的设计原则:
- 开闭原则
- 里式替换原则
- 依赖倒置原则
- 单一职责原则
- 接口隔离原则
- 迪米特原则
- 合成复用原则
1. 开闭原则
软件实体应当对扩展开放,对修改关闭。
开闭原则是软件设计的终极目标,对扩展开放可以使软件具有一定的灵活性,同时对修改关闭又可以保证软件的稳定性。使用开闭原则设计的软件有如下优势:
- 测试方便。由于开闭原则对修改关闭,因此软件实体是拥有稳定性的,测试时只需要对扩展代码进行测试即可;
- 更好地提高代码复用性。开闭原则通常采用抽象接口的方式来组织代码结构,抽象的编程本身就是对代码的复用性提高有很大的帮助;
- 提高软件的维护性和扩展性。由于开闭原则对扩展开放,因此当软件需要升级时,可以很容易地通过扩展来实现新功能,开发效率更高,代码也更易于维护。
在面向对象开发中,实现开闭原则可以通过继承父类和实现接口两种方式。
在开闭原则中,一个类只应该因为错误而修改,新加入的功能都不应该修改原始代码。
重构前的代码
enum Color: String
case unknown
case black
case white
case gray
case blue
case red
class Style
var backgroudColor = Color.black
var textColor = Color.white
func apply()
print("皮肤 - 背景色: \\(self.backgroudColor), 文字颜色: \\(self.textColor)")
let baseStyle = Style()
baseStyle.apply()
此时,如果有一个新需求增加一个背景色为灰色,文字颜色为蓝并且按钮颜色为红的主题,如何修改?并且需要遵守开闭原则
继承
class Custom1Style: Style
var buttonColor = Color.red
override init()
super.init()
backgroudColor = .gray
textColor = .blue
override func apply()
print("皮肤 - 背景色: \\(self.backgroudColor), 文字颜色: \\(self.textColor), 按钮颜色: \\(self.buttonColor)")
let custom1Style = Custom1Style()
custom1Style.apply()
通过继承方式实现开闭原则并不彻底,通过接口可以更好的实现开闭原则。
接口
protocol StyleInterface
var backgroudColor: Color get
var textColor: Color get
var buttonColor: Color get
func apply()
extension StyleInterface
var buttonColor: Color
get
return .unknown
class BaseStyle: StyleInterface
var backgroudColor: Color = .black
var textColor: Color = .white
func apply()
print("皮肤 - 背景色: \\(self.backgroudColor), 文字颜色: \\(self.textColor)")
class Custom2Style: StyleInterface
var backgroudColor: Color = .gray
var textColor: Color = .blue
var buttonColor: Color = .red
func apply()
print("皮肤 - 背景色: \\(self.backgroudColor), 文字颜色: \\(self.textColor), 按钮颜色: \\(self.buttonColor)")
let baseStyle2 = BaseStyle()
let custom2Style = Custom2Style()
baseStyle2.apply()
custom2Style.apply()
StyleInterface 协议定义了与主题相关的属性和方法,方法需要扩展多个主题时,需要对接口进行不同的实现即可。
2. 里式替换原则
继承必须保证超类所拥有的性质在子类中依然成立。即:在进行类的继承时,要保证子类不对父类的属性或方法进行重写,只是扩展父类的功能。
如果在设计时发现子类不得不重写父类的方法,则表明类的组织结构有问题,需要重新设计类的继承关系,比如将被重写的方法从父类抽离,仅在需要的子类声明。
3. 单一职责原则
一个类只应该承担一项责任,在实际设计中,可以以是否只有一个引起类变化的原因作为准则如果不止一个原因会引起类的变化,则需要对类重新进行拆分。
如果一个类或对象承担了太多的责任,则其中一个责任的变化可以带来对其他责任的影响,且不利于代码的复用性,容易造成代码的冗余,遵守单一职责设计的程序有以下几个特点:
- 降低类的复杂度,一个类承担单一的职责,逻辑清晰,提高内聚,降低耦合
- 提高代码可读性和可复用性
- 增强代码可维护性和可扩展性
- 类的变更是必然的,功能的增加必然会产生类的变更,单一职责可以使变更带来的影响最小。
4. 接口隔离原则
将庞大的接口定义拆分为更小的和更具体的接口,其“隔离”的主要是指对接口依赖的隔离。例如 UITableView
的 UITableViewDataSource
和 UITableViewDelegate
。定义的各个接口各司其职,尽量少耦合其他业务逻辑。
5. 依赖倒置原则
高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。与 开闭原则 的核心思路相同,都是要尽量减少对已有代码的修改,同时又易于进行扩展。优势:
- 由于都对接口进行依赖,减少了类之间的耦合
- 封闭了对类实现的修改,增强了程序的稳定性
- 核心是面向接口开发,减少了并行开发的依赖于风险
- 提高代码可读性和可维护性
重构前的代码:
如下代码即上层依赖下层:
class FoodStore
func sell(count: Int)
print("食品商店卖了\\(count)食物")
class Customer
func buy(store: FoodStore, count: Int)
print("购物--")
store.sell(count: count)
let customer = Customer()
customer.buy(store: FoodStore(), count: 4)
当有新的商店出现时就需要更改上层的 Customer
类。
使用依赖倒置的原则进行重构,使 Customer
只对抽象的接口进行依赖。
protocol Store
func sell(count: Int)
class FoodStore: Store
func sell(count: Int)
print("食品商店卖了\\(count)食物")
class ClothStore: Store
func sell(count: Int)
print("服装商店卖了\\(count)服装")
class Customer
func buy(store: Store, count: Int)
print("购物--")
store.sell(count: count)
let customer = Customer()
customer.buy(store: FoodStore(), count: 4)
customer.buy(store: ClothStore(), count: 2)
重构后的 Customer
不再依赖具体的 Store
,扩展也不需要更改其内部实现。
6. 迪米特原则
又叫 “最小知识原则”。核心为一个类或对象尽可能少地与其他实体发生交互作用。通常,我们不会对单独的类使用迪米特原则,这样做的解耦效果并不明显,但是如果是模块之间的交互通过一个中介类来统一处理,那就可以大大减少模块间的耦合程度,例如在iOS组件化开发中的路由器,可以将模块之间的耦合通过路由进行隔离,降低模块间的耦合。
7. 合成复用原则
在设计类的复用时,要尽量先使用组合或聚合的方式设计,尽量少使用继承。合成复用原则通过组合和聚合的方式实现复用,实现上通常使用属性、参数的方式引入其他实体进行通信。
重构前的代码:
class Teacher
var name: String
init(_ name: String)
self.name = name
func teach()
print("讲课")
class MathTeacher: Teacher
override func teach()
print("\\(name)讲数学课")
class EnglishTeacher: Teacher
override func teach()
print("\\(name)讲英语课")
let james = MathTeacher("james")
james.teach()
let davis = EnglishTeacher("davis")
davis.teach()
根据合成复用原则,不使用继承,把学科封装为Teacher的一个属性。
class Suject
var name: String
init(_ name: String)
self.name = name
class Teacher
var name: String
var subject: Suject
init(_ name: String, subject: String)
self.name = name
self.subject = Suject(subject)
func teach()
print("\\(name)讲\\(subject.name)课")
let james = Teacher("james", subject: "数学")
james.teach()
let davis = Teacher("davis", subject: "英语")
davis.teach()
总结
- 开闭原则是核心,在设计软件时保持扩展的开放性和修改的封闭性
- 里式替换原则要求在继承时不要破坏父类的实现
- 单一职责原则要求类的功能要单一
- 接口隔离原则要求接口的设计要精简
- 依赖倒置原则要求面向抽象编程,即面向接口编程
- 迪米特原则提供一种降低系统耦合性的方式
- 合成复用原则要求组织类的关系时谨慎使用继承
以上是关于软件设计原则的主要内容,如果未能解决你的问题,请参考以下文章