一、设计原则
注:所有的模式或模型都为了高内聚低耦合,提高代码的复用和可读性,从而降低维护成本。
一)SOLID原则:
1)开放封闭原则(OCP):
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
1. 定义的解读:
- 用抽象构建框架,用实现扩展细节。
- 不以改动原有类的方式来实现新需求,而是应该以实现事先抽象出来的接口(或具体类继承抽象类)的方式来实现。
2. 优点:
增加了程序的可扩展性,降低了程序的维护成本。
3. 代码对比:
设计一个在线课程类:
由于教学资源有限,开始的时候只有类似于博客的,通过文字讲解的课程。 但是随着教学资源的增多,后来增加了视频课程,音频课程以及直播课程。
- 不好的设计:
public class Open_Close_Principle //开闭原则
{
public class Course
{
//课程名
public string CourseName { set; get; }
//课程介绍
public string CourseIntroduction { set; get; }
//教师名
public string TeacherName { set; get; }
//博客课程
public void ContentCourse(){}
public override string ToString()
{
return $"课程名:{CourseName}, 课程介绍:{CourseIntroduction}, 教师名称:{TeacherName}";
}
//=================新需求==================
//新需求:视频课程
public void VideoCourse(){}
//新需求:音频课程
public void AudioCourse(){}
//新需求:直播课程
public void LiveCourse(){}
}
}
- 错误总结:
- 未遵循修改关闭,扩展开放的原则。
- 随着需求增加,需反复修改之前创建的Course课程类。
- 每次实例化时产生冗余,创建了其他不需要的类数据。
- 较好的设计:
public class Open_Close_Principle //开闭原则
{
public abstract class Course //抽象类:对类抽象,提取共有的有参或无参的方法及属性,可含方法体
{
//课程名
public string CourseName { set; get; }
//课程介绍
public string CourseIntroduction { set; get; }
//教师名
public string TeacherName { set; get; }
//重写ToString()方法
public override string ToString()
{
return $"课程名:{CourseName}, 课程介绍:{CourseIntroduction}, 教师名称:{TeacherName}";
}
}
//接口:对行为抽象,提取共有方法,但具体实现交给子类
public interface CourseAction
{
void ClassBegin();
void ClassOver();
}
//博客课程,继承抽象类和接口
public class ContentCourse : Course,CourseAction
{
//无参构造
public ContentCourse() { }
//有参构造
public ContentCourse(string courseName,string courseIntroduction,string teacherName)
{
this.CourseName = courseName;
this.CourseIntroduction = courseIntroduction;
this.TeacherName = teacherName;
}
//接口实现
public void ClassBegin()
{
Console.WriteLine("博客课程上课");
}
public void ClassOver()
{
Console.WriteLine("博客课程下课");
}
//子类定义的方法
public string Content() { return "博客内容"; }
}
//视频课程
public class VideoCourse : Course { }
//音频课程
public class AudioCourse : Course { }
//直播课程
public class LiveCourse : Course { }
}
4. UML对比:
5. 总结:
- 以上体现了开闭原则,其实也就是一种标准,能提高可扩展性,降低维护成本的标准。
- 抽象类和接口:
– 抽象类是对类整体抽象,包括类成员,最终呈现为类别的方式。子类可以继承父类默认的有参或无参方法及属性,并能够进一步扩展。
– 接口是对其中的行为抽象,只在乎能干什么,最终实现交给子类进一步扩展。 - 更好的实际开闭原则,设计之初就要想清楚该场景哪些数据或行为是一定不变的(或很难改变),哪些容易变动的。 将后者抽象成接口或抽象类以便于未来改动。
2)单一职责原则(SRP):
定义: 一个类只允许有一个职责,即只有一个导致该类变更的原因。
1. 定义的解读:
- 如果一个类有多种职责,就会有多种原因导致这个类发生变化,从而导致维护困难。
- 在开发过程中如果发现当前类的职责不仅仅有一个,就应该将本不属于该类真正的职责分离出去。
- 不单指类,函数方法也要遵循。
2.优点:
- 让类和方法的职责划分更清晰,提高可读性。清晰的代码降低了维护的成本。
3.缺点:
- 过度的颗粒化方法进行单一职责会增加复杂性。
4.代码对比:
1.初始需求:创造一个员工类,这个类含有员工的一些基本信息。
2. 新需求:增添两个方法①判断员工今年是否升职;②计算员工薪水
public class Single_Responsibility_Principle //单一职责原则
{
public class Employee //员工类
{
//员工号
public long EmployeeID { set; get; }
//员工姓名
public string Name { set; get; }
//员工性别
public string Sex { set; get; }
//员工工龄
public int WorkAge { set; get; }
//员工薪水
public decimal Salary { set; get; }
//=============新需求==================
//计算薪水
public decimal CaculateSalary() { return 0; }
//今年是否升职
public bool GetPromotionThisYear() { return false; }
}
}
- 错误总结:
- 新需求直接加入员工类中看似没问题,但违反了单一职责原则。因为计算工资和是否晋升不是员工本身的职责。员工类的设计初衷只是保存员工的信息。
- 假若晋升机制有变化,或者薪资有变化仍需要反复修改此类。
- 较好的设计:
public class Single_Responsibility_Principle //单一职责原则
{
public class Employee //员工类
{
//员工号
public int EmployeeID { set; get; }
//员工姓名
public string Name { set; get; }
//员工性别
public string Sex { set; get; }
//员工工龄
public int WorkAge { set; get; }
//员工薪水
public decimal Salary { set; get; }
}
public class FinancialApartment //财务部门负责薪资计算
{
public decimal CaculateSalary(Employee employee) { return 0; }
}
public class HRApartment //人事部门负责员工的晋升
{
public bool GetPromotionThisYear(Employee employee) { return false; }
}
}
5.UML对比:
6.总结:
- 使类、方法的职责更加清晰。提高代码的可读性,减小了后期维护的成本。
- 在实际开发中,很容易将不同的职责糅杂在一起,开发者应时刻注意。
- 在真正开发中,细分职责的时机:
– 1、 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
– 2、类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
– 3、比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
– 4、类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。
3)依赖倒置原则(DIP):
定义:①依赖抽象而非依赖实现。②抽象不能依赖细节,细节要依赖抽象。③高级模块不应该依赖低级模块,二者都应该依赖抽象。
1.定义的解读:
- 针对接口编程,而不是针对实现编程。
- 尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现。
- 关于高层模块与低层模块的划分可以按照决策能力的高低进行划分。业务层自然就处于高层模块,逻辑层和数据层自然就归类为底层。
- 倒置的意思:
在开发者和程序角度:底层的依赖倒置,不再依赖开发者,而是依赖程序本身来实现。(IOC和DI)
2.优点:
- 依赖倒置原则可以降低类间的耦合性。
- 依赖倒置原则可以提高系统的稳定性。
- 依赖倒置原则可以减少并行开发引起的风险。
- 依赖倒置原则可以提高代码的可读性和可维护性。
3.代码对比:
以“顾客购物程序”为例:
设计顾客类和商店类,商店类中定义了Sell()方法,顾客通过不同的商店实现不同的购物。
public class Dependency_Inversion_Principle //依赖倒置原则
{
public class WandaShop
{
public string Sell() { return "这里是万达商场"; }
}
public class WoermaShop
{
public string Sell() { return "这里是沃尔玛商场"; }
}
/// <summary>
/// 顾客类:
/// 当商场变化时,总要反复修改此处商场类型,违反了开闭原则。
/// 具体原因是因为顾客类与具体的商店类绑定在了一起,违背了依赖倒置原则。
/// </summary>
public class Customer
{
public void Shopping(WandaShop shop)
{
shop.Sell();
}
}
}
- 在以上代码中,高层模块(Customer)都依赖了底层模块(WandaShop、WoermaShop),违反了依赖倒置原则。
- 改进:
– 将所有的Shop类抽象出来,让Customer类不再依赖于所有底层,而是去依赖抽象。
– 所有的底层WandaShop、WoermaShop类也都依赖这个抽象,通过实现这个抽象来实现自己的方法。
public class Dependency_Inversion_Principle //依赖倒置原则
{
//Shop商场接口
public interface Shop
{
string Sell();
}
//顾客类,定义Shopping方法
public class Customer
{
public void Shopping(Shop shop)
{
Console.WriteLine(shop.Sell());
}
}
//万达商场继承Shop接口
public class WandaShop : Shop
{
public string Sell()
{
return "这里是万达商场";
}
}
//沃尔玛商场继承Shop接口
public class WoermaShop : Shop
{
public string Sell()
{
return "这里是沃尔玛商场";
}
}
public static void Main()
{
Customer customer = new Customer();
customer.Shopping(new WandaShop()); //万达商场
customer.Shopping(new WoermaShop()); //沃尔玛商场
}
}
4.UML对比:
5.总结:
实现方法:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
4)接口隔离原则(ISP):
定义:多个特定的客户端接口要好于一个通用性的总接口。
1.定义的解读:
- 客户端不应该依赖它不需要实现的接口。
- 不建立庞大臃肿的接口,应尽量细化接口,接口中的方法应该尽量少。
- 需要注意的是:接口的粒度也不能太小。如果过小,则会造成接口数量过多,使设计复杂化。
2.优点:
- 避免同一个接口里面包含不同类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。
3.代码对比:
public class Interface_Segregation_Principle //接口隔离
{
public interface RestaurantProtocol //餐厅订单协议
{
void PlaceOnlineOrder(); //网上订餐方式
void PlaceTelePhoneOrder(); //电话订餐方式
void PlaceWalkInCustormerOrder(); //到店订餐方式
void PayInPerson(); //付款方式
}
public class OnlineClient : RestaurantProtocol
{
public void PayInPerson(){ Console.WriteLine("客户网上订餐付款方式"); }
public void PlaceOnlineOrder(){ Console.WriteLine("客户网上订餐"); }
public void PlaceTelePhoneOrder(){ } //无用
public void PlaceWalkInCustormerOrder(){ } //无用
}
public class TelePhoneClient : RestaurantProtocol
{
public void PayInPerson() { Console.WriteLine("客户电话订餐付款方式"); }
public void PlaceOnlineOrder() { } //无用
public void PlaceTelePhoneOrder() { Console.WriteLine("客户电话订餐"); }
public void PlaceWalkInCustormerOrder() { } //无用
}
public class WalkInClient : RestaurantProtocol
{
public void PayInPerson() { Console.WriteLine("客户到店订餐付款方式"); }
public void PlaceOnlineOrder() { } //无用
public void PlaceTelePhoneOrder() { }//无用
public void PlaceWalkInCustormerOrder() { Console.WriteLine("客户到店订餐");}
}
}
- 错误总结:
- 接口过大,使得子类实现出现了冗余,不符合接口隔离原则。
- 需要将不同类型的接口分离出来———>下单接口和支付接口,再由子类实现独有方法。
- 改进后设计:
public class Interface_Segregation_Principle //接口隔离
{
public interface RestaurantPlaceOrderProtocol //餐厅订单地点协议
{
void PlaceOrder(); //订餐地点
}
public interface RestaurantPaymentProtocol //餐厅订单方式协议
{
void PayOrder();
}
//网上客户
public class OnlineClient : RestaurantPaymentProtocol, RestaurantPlaceOrderProtocol
{
public void PayOrder(){ Console.WriteLine("客户网上订餐付款方式"); }
public void PlaceOrder(){ Console.WriteLine("客户网上订餐"); }
}
//电话客户
public class TelePhoneClient : RestaurantPaymentProtocol, RestaurantPlaceOrderProtocol
{
public void PayOrder() { Console.WriteLine("客户电话订餐付款方式"); }
public void PlaceOrder() { Console.WriteLine("客户电话订餐"); }
}
//到店客户
public class WalkInClient : RestaurantPaymentProtocol, RestaurantPlaceOrderProtocol
{
public void PayOrder() { Console.WriteLine("客户到店订餐付款方式"); }
public void PlaceOrder() { Console.WriteLine("客户到店订餐"); }
}
}
4.UML对比:
5.总结:
- 使接口责任划分更明确,符合高内聚,低耦合。清晰代码也减少后期维护成本。
- 开发时自己思考方法是否能进行抽象,能否归于同一类任务。如不是则拆分。
5)里氏替换原则(LSP):
定义:子类对象可以替换其父类对象,而程序执行效果不变。(父类有的方法子类都有)
1.定义的解读:
- 在继承体系中,子类中可以增加自己特有的方法,也可以实现父类的抽象方法。
- 但是不能重写父类的非抽象方法。
2.优点:
- 可以检验继承使用的正确性,约束继承在使用上的泛滥。
– 正方形不是长方形、几维鸟不是鸟这样的例子。 - 克服了继承中重写父类造成的可复用性变差的缺点。
- 实现开闭原则的重要方式之一。
– 对扩展开放,对修改关闭。
3.代码对比:
- 原设计:
public class Liskov_Sbstitution_Principle //里氏替换原则
{
public static void Main()
{
BrownKiwi brownKiwi = new BrownKiwi();
brownKiwi.FlySpeed = 120;
Console.WriteLine($"几维鸟飞行时间:{brownKiwi.GetFlyTime(300)}");
/*几维鸟飞行时间:∞*/
}
//子类:燕子类
public class Swallow : Birds { }
//子类:几维鸟类
public class BrownKiwi : Birds
{
private int flySpeed;
public int FlySpeed { set { this.flySpeed = 0; } get { return 0; } }
}
//父类:鸟类
public class Birds
{
//飞行速度
public int FlySpeed { set; get; }
//获得飞行时间
public double GetFlyTime(double distance)
{
return (distance / FlySpeed);
}
}
}
- 错误总结:
- 几维鸟子类对父类方法重写,违反了里氏替换和开闭原则。
- 所以正确做法应该取消继承鸟类,继承更一般的父类,如动物类,走路奔跑是可以的。
4.UML对比:
5.总结:
- 里氏替换原则只是对继承关系的一种检验,看它是否真正的继承关系。
- 反复思考和确认该继承关系是否正确,或者当前的继承体系是否还可以支持后续的需求变更,如果无法支持,则需要及时重构,采用更好的方式来设计程序。
二)LKP原则(迪米特原则:Talk only to your immediate friends and not to strangers)
定义:一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。
1.定义的解读:
- 迪米特法则也叫做最少知道原则(Least Know Principle), 一个类应该只和它的成员变量,方法的输入,返回参数中的类作交流,而不应该引入其他的类(间接交流)。
2.优点:
- 可以良好地降低类与类之间的耦合,减少类与类之间的关联程度,让类与类之间的协作更加直接。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
- 中介者模式
3.缺点:
- 但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。
4.代码对比:
- 原设计:
public class Person
{
// 使用洗衣机洗衣服的方法
public void washClothes(WashingMachine washingMachine)
{
Console.WriteLine("准备清洗。");
washingMachine.receiveClothes();
washingMachine.wash();
washingMachine.drying();
}
}
public class WashingMachine
{
// 接收衣服的方法
public void receiveClothes()
{
Console.WriteLine("洗衣机接收衣服");
}
// 洗涤的方法
public void wash()
{
Console.WriteLine("洗衣机开始洗衣服");
}
// 烘干的方法
public void drying()
{
Console.WriteLine("洗衣机开始烘干衣服");
}
}
- 错误总结:
Person类中一连调用了WashMachine类的三个方法,并且这三个方法都是需要洗衣机做的,跟Person类无关。这就造成Person类对WashMachine类知道的太多。违反迪米特原则。 - 改进后代码:
public class WashingMachine
{
// 自动洗衣
public void automatic()
{
this.receiveClothes();
this.wash();
this.drying();
}
// 接收衣服的方法
private void receiveClothes()
{
Console.WriteLine("洗衣机开始接收衣服");
}
// 洗涤的方法
private void wash()
{
Console.WriteLine("洗衣机开始洗衣服");
}
// 烘干的方法
private void drying()
{
Console.WriteLine("洗衣机开始烘干衣服");
}
}
public class Person
{
// 使用洗衣机洗衣服的方法
public void washClothes(WashingMachine washingMachine)
{
Console.WriteLine("准备清洗。");
washingMachine.automatic();
}
}
5.UML对比:
6.总结:
- 从迪米特法则的定义和特点可知,它强调以下两点:
– 从依赖者的角度来说,只依赖应该依赖的对象。
– 从被依赖者的角度说,只暴露应该暴露的方法。 - 在运用迪米特法则时要注意以下 6 点:
–1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
–2. 在类的结构设计上,尽量降低类成员的访问权限。
–3. 在类的设计上,优先考虑将一个类设置成不变类。
–4. 在对其他类的引用上,将引用其他对象的次数降到最低。
–5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
–6. 谨慎使用序列化(Serializable)功能。
三)DRY原则(Don’t Repeat Yourself.)
1.定义的解读:
- 一个规则,实现一次(one rule, one place)是面向对象编程中的基本原则,程序员的行事准则。意思就是尽可能的抽象出来功能,尽量不要复用。
- 并不是尽一切可能避免“重复”。“避免重复”并不等于“抽象”。有时候适当的重复代码,可提高可读性。
- 举例:1、密码与用户名相同格式下的检验;2、同一个数据对象的增删改查。
四)KISS原则(Keep It Simple&Stupid)
1.定义的解读:
- 让代码简单直接。从小到几行代码的写法大到整个系统的架构我们都应该保持简单易懂。
五)YAGNI原则(You Ain’t Gonna Need It)
1.定义的解读:
- 适可而止。只有当你需要的时候才去添加额外的功能,不要过度设计。
- 过度设计往往会延缓开发迭代的速度。
二、设计模式
一)观察者模式
1、简介
- 观察者模式是一种使用频率最高的设计模式之一,用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。(MVC中M与V的关系)
2、模式的结构:
- 抽象目标(Subject)类:提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
- 具体目标(Concrete Subject)类:实现抽象目标中通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
3、代码及UML
using System;
using System.Collections.Generic;
public class Demo
{
public static void Main(string[] args)
{
Subject subject = new ConcreteSubject();
subject.Add(new ConcreteObserver1());
subject.Add(new ConcreteObserver2());
subject.NotifyObservers();
}
//具体目标
public class ConcreteSubject : Subject
{
public override void NotifyObservers()
{
foreach(var temp in observers)
{
temp.Response();
}
}
}
//抽象目标
public abstract class Subject
{
protected List<Observer> observers = new List<Observer>();
//添加观察者
public void Add(Observer observer)
{
observers.Add(observer);
}
//删除观察者
public void Remove(Observer observer)
{
observers.Remove(observer);
}
//通知观察者
public abstract void NotifyObservers();
}
//具体观察者2
public class ConcreteObserver2 : Observer
{
public void Response()
{
Console.WriteLine("观察者2做出反应……");
}
}
//具体观察者1
public class ConcreteObserver1 : Observer
{
public void Response()
{
Console.WriteLine("观察者1做出反应……");
}
}
//抽象观察者
public interface Observer
{
void Response();
}
}
4、使用场景:
- 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
- 单向(物价上涨对消费者和商家的影响)
- 发布-订阅模式
- 同步–>异步–>线程池–>进程间(消息队列)
二)中介者模式
1、简介
- 定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。(MVC中C与M和V的关系)
2、模式的结构
- 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
- 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
- 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
3、代码及UML
using System;
using System.Collections.Generic;
public class Demo
{
public static void Main(string[] args)
{
Mediator mediator = new ConcreteMediator();
ConcreteColleage1 concreteColleage1 = new ConcreteColleage1();
ConcreteColleage2 concreteColleage2 = new ConcreteColleage2();
ConcreteColleage3 concreteColleage3 = new ConcreteColleage3();
mediator.Register(concreteColleage1);
mediator.Register(concreteColleage2);
mediator.Register(concreteColleage3);
concreteColleage1.Send();
concreteColleage2.Send();
concreteColleage3.Send();
}
//具体同事类2
public class ConcreteColleage3 : Colleage
{
public override void Receive()
{
Console.WriteLine("具体同事类3收到请求\n");
}
public override void Send()
{
Console.WriteLine("具体同事类3 发送请求\n");
mediator.Relay(this);
}
}
//具体同事类2
public class ConcreteColleage2 : Colleage
{
public override void Receive()
{
Console.WriteLine("具体同事类2收到请求\n");
}
public override void Send()
{
Console.WriteLine("具体同事类2 发送请求\n");
mediator.Relay(this);
}
}
//具体同事类1
public class ConcreteColleage1 : Colleage
{
public override void Receive()
{
Console.WriteLine("具体同事类1收到请求\n");
}
public override void Send()
{
Console.WriteLine("具体同事类1 发送请求\n");
mediator.Relay(this);
}
}
//抽象同事类
public abstract class Colleage
{
protected Mediator mediator;
public void SetMediator(Mediator mediator)
{
this.mediator = mediator;
}
public abstract void Receive();
public abstract void Send();
}
//具体中介者
public class ConcreteMediator : Mediator
{
protected List<Colleage> colleages = new List<Colleage>();
//同事的注册
public override void Register(Colleage colleage)
{
if (!colleages.Contains(colleage))
{
colleages.Add(colleage);
colleage.SetMediator(this);
}
}
//同事请求的转发
public override void Relay(Colleage colleage)
{
colleages.ForEach(cl =>
{
if (!colleage.Equals(cl))
{
cl.Receive();
}
});
}
}
//抽象中介者
public abstract class Mediator
{
public abstract void Register(Colleage colleage); //同事的注册
public abstract void Relay(Colleage colleage); //同事的转发
}
}
4、使用场景:
- 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
- 双向(共有通讯录为例)