一,什么是设计
- 按哪一种思路或者标准来实现功能
- 功能相同,可以有不同的设计方式
- 需求如果不断变化,设计的作用才能体现出来
二,SOLID 五大设计原则
首字母 | 指代 | 概念 |
---|---|---|
S | 单一职责原则 | 单一功能原则认为对象应该仅具有单一功能 的概念 |
O | 开放封闭原则 | 开闭原则认为 软件体应该是对外开放的,但是对于修改封闭的概念 |
L | 里式替换原则 | 里式替换原则认为程序中的对象应该是可以在不改变程序正确性的前提下 被它的子类替换 的概念 |
I | 接口隔离原则 | 接口隔离原则认为 多个特定的客户端接口要好于一个宽泛用途的接口 的概念 |
D | 依赖反转原则 | 依赖反转原则认为一个方法应该遵循 依赖于抽象而不是一个实例 的概念 依赖注入是该原则实现的一种方式 |
1.O开放封闭原则
- Open Closed Principle
- 对扩展开放,对修改关闭
- 增加需求时,扩展新代码,而非修改已有代码
- 开闭原则是设计模式中的总原则
- 对近期可能会变化并且如果有变化但改动量巨大的地方要增加扩展点,扩展点过多会降低可读性
不好的设计
class Product{
constructor(name,price){
this.name=name
this.price=price
}
cost(customer){
switch(customer.rank){
case 'member':
return this.price*.8
case 'vip':
return this.price*.6
default:
return this.price
}
}
}
class Customer{
constructor(rank){
this.rank=rank
}
}
let product=new Product('笔记本电脑',1000)
let member=new Customer('member')
let vip=new Customer('vip')
let guest=new Customer('guest')
console.log(product.cost(member))
console.log(product.cost(vip))
console.log(product.cost(guest))
好的设计
class Product{
constructor(name,price){
this.name=name
this.price=price
}
cost(customer){
return this.price*customer.discount
}
}
class Customer{
constructor(rank,discount=1){
this.rank=rank
this.discount=discount
}
}
let product=new Product('笔记本电脑',1000)
let member=new Customer('member',.8)
let vip=new Customer('vip',.6)
let guest=new Customer('guest')
let superVip=new Customer('superVip',.4)
console.log(product.cost(member))
console.log(product.cost(vip))
console.log(product.cost(guest))
console.log(product.cost(superVip))
- 多态是一个功能,它的实现是要靠继承的,没有继承就没有多态
2. S 单一职责原则
- Single responsibility principle
- 一个类或者模块只负责完成一个职责,如果功能特别复杂就进行拆分
- 单一职责可以降低类的复杂性,提高代码可读性、可维护性
- 当类代码行数过多、方法过多、功能太多、职责复杂的时候就要对类进行拆分了
- 拆分不能过度,如果拆分过度会损失内聚性和维护性
3. L里氏替换原则
- Liskkov Substitution Principle
- 所有引用基类的地方必须能透明的使用其他子类的对象
- 子类能替换父类,使用者可能根本就不需要知道父类还是子类,反之则不行
- 里式替换原则是开闭原则的实现基础,程序设计的时候尽量使用基类定义及引用,运行时再决定使用哪个子类
- 里式替换原则可以提高代码的复用性,提高代码的可扩展性,也增加了耦合性
-
相对于多态,这个原则讲的是类如何设计,子类如果违反了父类的功能则表示违反了里式替换原则
里氏替换原则
好的设计
class Drink{
//抽象类abstract
getPrice(){}
}
class CocaCola extends Drink{
getPrice(){
return 3
}
}
class Sprite extends Drink{
getPrice(){
return 3
}
}
class Fanta extends Drink{
getPrice(){
return 5
}
}
class Customer{
drink(AbstrackDrink){
console.log(`花费${AbstrackDrink.getPrice()}`)
}
}
let c1=new Customer()
c1.drink(new CocaCola())
不好的设计
class Drink{
//抽象类abstract
getPrice(){}
}
class CocaCola extends Drink{
getPrice(){
return '我是一瓶可口可乐' //子类违反了父类的功能和规定
}
}
class Sprite extends Drink{
getPrice(){
return 3
}
}
class Fanta extends Drink{
getPrice(){
return 5
}
}
class Customer{
drink(AbstrackDrink){
console.log(`花费${AbstrackDrink.getPrice()}`)
}
}
let c1=new Customer()
c1.drink(new CocaCola())
4.D 依赖倒置原则
- Dependence Inversion Principle
- 依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。
- 核心思想是:要面向接口编程,不要面向实现编程
依赖倒置原则的主要作用如下:
- 依赖倒置原则可以降低类间的耦合性。
- 依赖倒置原则可以提高系统的稳定性。
- 依赖倒置原则可以减少并行开发引起的风险。
- 依赖倒置原则可以提高代码的可读性和可维护性
依赖倒置原则的实现方法
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则
依赖倒置描述链接
package principle;
public class DIPtest
{
public static void main(String[] args)
{
Customer wang=new Customer();
System.out.println("顾客购买以下商品:");
wang.shopping(new ShaoguanShop());
wang.shopping(new WuyuanShop());
}
}
//商店
interface Shop
{
public String sell(); //卖
}
//韶关网店
class ShaoguanShop implements Shop
{
public String sell()
{
return "韶关土特产:香菇、木耳……";
}
}
//婺源网店
class WuyuanShop implements Shop
{
public String sell()
{
return "婺源土特产:绿茶、酒糟鱼……";
}
}
//顾客
class Customer
{
public void shopping(Shop shop)
{
//购物
System.out.println(shop.sell());
}
}
依赖倒置原则
interface Girlfriend{
age:number
height:number
cook():void
}
class LinChiling implements Girlfriend{
age:number=35
height:number=178
cook(){
console.log('泡面')
}
}
class HanMeimei implements Girlfriend{
age:number=35
height:number=178
cook(){
console.log('泡面')
}
}
class SingleDog{
constructor(public girlfriend:Girlfriend){
}
}
let dog1=new SingleDog(new LinChiling())
let dog2=new SingleDog(new HanMeimei())
5 接口隔离原则
- Interface Segregation Principle
- 保持接口的单一独立,避免出现胖接口
- 客户端不应该依赖它不需要的接口,类之间的依赖关系应该建立在最小的接口上
- 接口尽量细化,而且接口中的方法尽量的少
-
类似于单一职责原则,更关注接口
image.png
interface Runing{
run():void
}
interface Flying{
fly():void
}
interface Swimming{
swim():void
}
class Automobile implements Runing,Flying,Swimming{
run(){}
fly(){}
swim(){}
}
6 迪米特法则
- Law of Demeter, LOD
- 有时候也叫做最少知识原则
- 一个软件实体应当尽少地与其他实体发生相互作用
- 迪米特法则的初衷在于降低类之间的耦合
- 类定义时尽量要实现内聚,少用public修饰符,尽量使用private、protected等
- 迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
- 迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法
迪米特法则的优点
- 迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
缺点
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
迪米特法则的实现方法
从迪米特法则的定义和特点可知,它强调以下两点:
1.从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下 6 点。
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
示例1
class Salesman{
constructor(public name:string){
}
sale(){
console.log(this.name+'销售中。。。。。。。。。。')
}
}
class SaleManager{
public salesmen:Array<Salesman>=[new Salesman('张三'),new Salesman('李四')]
sale(){
this.salesmen.forEach(salesman=>salesman.sale())
}
}
class CEO{
private saleManager:SaleManager=new SaleManager()
sale(){
this.saleManager.sale()
}
}
let ceo=new CEO()
ceo.sale()
示例2 明星与经纪人的关系实例
分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则
image.png
package principle;
public class LoDtest
{
public static void main(String[] args)
{
Agent agent=new Agent();
agent.setStar(new Star("林心如"));
agent.setFans(new Fans("粉丝韩丞"));
agent.setCompany(new Company("中国传媒有限公司"));
agent.meeting();
agent.business();
}
}
//经纪人
class Agent
{
private Star myStar;
private Fans myFans;
private Company myCompany;
public void setStar(Star myStar)
{
this.myStar=myStar;
}
public void setFans(Fans myFans)
{
this.myFans=myFans;
}
public void setCompany(Company myCompany)
{
this.myCompany=myCompany;
}
public void meeting()
{
System.out.println(myFans.getName()+"与明星"+myStar.getName()+"见面了。");
}
public void business()
{
System.out.println(myCompany.getName()+"与明星"+myStar.getName()+"洽淡业务。");
}
}
//明星
class Star
{
private String name;
Star(String name)
{
this.name=name;
}
public String getName()
{
return name;
}
}
//粉丝
class Fans
{
private String name;
Fans(String name)
{
this.name=name;
}
public String getName()
{
return name;
}
}
//媒体公司
class Company
{
private String name;
Company(String name)
{
this.name=name;
}
public String getName()
{
return name;
}
}
7.合成复用原则
1.类的关系
- 类之间有三种基本关系,分别是关联(聚合和组合)、泛化和依赖
- 如果一个类单向依赖另一个类,那么它们之间就是单向关联。如果彼此依赖,则为相互依赖,即双向关联
- 关联关系包括两种特例:聚合和组合
- 聚合,用来表示整体和部分的关系或者拥有关系,代表部分的对象可能会被整体拥有,但不一定会随着整体的消亡而销毁,比如班级和学生
-
合成或者说组合要比聚合关系强的多,部分和整体的生命周期是一致的,比如人和器官之间
图示
2.合成复用原则
- 合成复用原则是通过将已有的对象纳入到新对象中,作为新对象的成员对象来实现的
- 新对象可以调用已有的对象的功能,从而达到复用
- 原则是尽量首先使用组合/聚合方式,而不是继承(继承耦合性太强)
- 专业人做专业事
- 合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现
- 如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范
合成复用原则的重要性
通常类的复用分为继承复用和合成复用两种
继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
1.它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
2.新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
3.复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
合成复用原则的实现方法
合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用
//尽量使用组合或者聚合,而不是使用继承
class Cooker{
cook(){
}
}
class Person{
private cooker:Cooker
cook(){
this.cooker.cook()
}
}
【例1】汽车分类管理程序。
分析:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。图 1 所示是用继淨:关系实现的汽车分类的类图
图1
从图 1 可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,其类图如图 2 所示。
图2
8.总结
- 开闭原则是核心,对修改关闭对扩展开放是软件设计的基石
- 单一职责要求我们设计接口和模块功能的时间尽量保证单一性和原子性,修改一条不影响全局和其他模块
- 里氏替换原则和依赖倒置原则要求面向接口和抽象编程,不要依赖具体实现,否则实现一改,上层调用者就要对应修改
tips:如何写出好代码
- 可维护性,bug是否好改
- 可读性,是否容易看懂
- 可扩展性 是否可以添加新功能
- 灵活性 添加新功能是否容易,老方法和接口是否容易复用
- 简洁性,代码是否简单清晰
- 可复用性 相同的代码不要写两遍
- 可测试性 是否方便写单元测试和集成测试
23种设计模式
设计模式之间的关系.jpg一. 创建型
- 工厂模式(工厂方法模式、抽象工厂模式、简单工厂模式)、建造者模式、单例模式
- 原型模式
二. 结构型
- 代理模式、桥接模式、装饰器模式、适配器模式
- 外观模式、组合模式、享元模式
三.行为型
-观察者模式、模版方法模式、策略模式、职责链模式、迭代器模式、状态模式
- 访问者模式、 备忘录模式、命令模式、解释器模式、中介者模式
网友评论