什么是设计模式?
设计模式,重要的不是模式,而是设计。我们在开发过程中不能老师蒙头敲代码,堆积木。这种无脑的堆代码的行为不但会使自己成为真真正正的码农,而且在这个途中会给自己和别人挖无数的坑,最终导致自己开始怀疑自己当初为什么选择当程序员这么苦逼的职业。
在编写代码的时候,我们除了在思考某些功能应该怎么用代码实现的同时也要想想我们怎么把这些代码通过良好的规则把它实现得漂亮。
怎么使用设计模式?
我自己当初进入程序员行业的时候,目标是架构师,当时满脑子设计模式、程序框架等,总感觉自己站在高出指点江山。做产品的时候使用了各种框架,各种模式。于是我创建了一个超级计算机的机箱,最终产品只是往这个机箱里塞进1个手机主板。而且我在建造这个超级计算机的机箱的时候就已经精疲力尽了。
上面这种行为叫做设计模式驱动编程,是一种极为不健康的编程方式。
现在我们推崇的编程方式叫敏捷开发,拥抱变化,在变化中合理使用设计模式能够减少变化带来的很多问题。
接口和抽象类(类的继承)
这两个东西在未来所要接触到的设计模式息息相关。
接口偏向于通用化,偏向于组合使用。 例子: USB接口。
USB内有4条数据线,分别定义了电源传输以及数据传输。
USB磁盘、USB风扇、手绘板、手机、充电宝等等他们在根本的种类上都没有共性,USB磁盘是存储设备、USB风扇是避暑设备、手写板是系统输入设备、手机是多媒体设备、充电宝是电源提供设备。
但他们在实现上有一个共同点,就是都实现了USB接口提供了电源和数据的传输。
USB接口规定如下:
public interface IUsb
{
void TransmitPower(Electric e);
void ReceivePower(Electric e);
object OnData(object data);
}
USB风扇的实现如下:
public class UsbFan : IUsb
{
private DcMotor dcMotor;
public Electric TransmitPower()
{
//USB风扇不输出电流,故不实现任何方法。
}
public void ReceivePower(Electric e)
{
dcMotor.ReceivePower(e);
//让风扇的马达接收电量,使他们转起来。
}
public object OnData(object data)
{
return "__POWER__ONLY__";
//不响应任何数据请求,并且告诉某些智能设备这台没有数据交换功能。
}
}
USB磁盘的实现如下:
public class UsbHardDrive : IUsb
{
private Pcb pcb;
public Electric TransmitPower()
{
//USB磁盘不输出电流,故不实现任何方法。
}
public void ReceivePower(Electric e)
{
pcb.ReceivePower(e);
//将电源发送至u盘的电路板。
}
public object OnData(object data)
{
if(data == "fsList")
return pcb.flash.GetFileSystemInfo();
else if(data is FileRequestInfo)
return pcb.flash.GetFile(data.filePath);
else if(data is FileWriteInfo)
return pcb.flash.WriteFile(data.filePath, data.stream);
//...
}
}
充电宝的实现:
public class UsbPowerSupply : IUsb
{
private LiBattery bat;
public Electric TransmitPower()
{
return bat.GetElectricCurrent("5V2A");
}
public void ReceivePower(Electric e)
{
if(e.voltage>6||e.current>0.2f)
return; //如果电压和电流超过目标数量,则保护电池。
bat.Charge(e);
}
public object OnData(object data)
{
return "__POWER__ONLY__";
//不响应任何数据请求,并且告诉某些智能设备这台没有数据交换功能。
}
}
那么抽象类与类的继承呢,就有一个偏向,因为类中都包含了一些具体的代码,其他类在继承该基类的时候就继承了这个基类的所有功能,另外在C#和Java中一个类只能继承一个基类,是不允许出现杂交的情况的。
说道杂交就想到了生物的例子: 狗是一个父类,那么继承狗的子类们有哈士奇、柯基、秋田、金毛、萨摩等等。继承于狗的这些子类们有个共同的特点:他们都是狗。
狗的基类:
public abstract class Dog
{
public int LegCount { return 4; }
public abstract string keyword { get; }
public abstract int HairLength { get; }
pubiic abstract Color[] HairColor { get; }
public string Bark()
{
return "Wang!";
}
public void WorldInteract(object o)
{
if(o is Pillar)
PeeOn(o);
else if(o is Ball)
Catch(o);
}
}
哈士奇的实现:
public class Husky : Dog
{
public override string Keyword { get { return "蠢萌"; }
public override int HairLength { get{ return 5cm; } }
public override Color[] HairColor
{
get
{
return new Color[]{ Color.black, Color.grey, Color.white };
}
}
}
萨摩的实现:
public class Samoyed : Dog
{
public override string Keyword { get { return "可爱"; }
public override int HairLength { get{ return 10cm; } }
public override Color[] HairColor
{
get
{
return new Color[]{ Color.white };
}
}
}
真正重要的原则:
理想目标--KISS原则: Keep It Simple and Stupid,将你的代码写的简单粗暴。
时刻践行的DRY原则: Don't Repeat Yourself。不要重复写代码。有些功能的代码实现起来高度重复,有时往往只要改动其中的几个变量就可以的时候,严禁复制粘贴再修改。
血淋淋的事实:
public int[] arr1;
public int[] arr2;
public void FillArray1()
{
for(var i = 0; i < 100; i++)
{
if(i ==0)
arr1[i] = 0;
else
arr1[i] = arr1[i-1] + i;
}
}
public void FillArray2()
{
for(var i = 0; i < 100; i++)
{
if(i ==0)
arr2[i] = 0;
else
arr2[i] = arr1[i-1] + i;
}
}
public static void Main(string[] args)
{
FillArray1();
FillArray2();
}
改成这样即可:
public int[] arr1;
public int[] arr2;
public void FillArray(int[] arr)
{
for(var i = 0; i < 100; i++)
{
if(i ==0)
arr[i] = 0;
else
arr[i] = arr[i-1] + i;
}
}
public static void Main(string[] args)
{
FillArray(arr1);
FillArray(arr2);
}
面向对象中的5大原则: S.O.L.I.D.
1. S(RP):Simple Responsibility Principle 单一职责原则
2. O(CP): Open Close Principle 开闭原则
3. L(SP): Liskov Substitution Principle 里氏替换原则
4. I(SP): Interface Segregation Principle 接口隔离原则
5. D(IP): Dependency Inversion Principle 依赖反转原则
SRP:
每一个类,每一个方法只干一件事。
public class GridMap
{
public Node GetNode(int x, int y);
public Vector2 Size { get; }
public void DrawNode(int x, int y, bool walkable, int cost);
public void DrawNodeInfo(float h, float g, bool isOpen, bool isClose);
}
绘制地图和获取地图基本信息这两个东西被包含在了一个类里。而地图信息是通用的,绘制功能却特别依赖于各个平台的具体的图形功能。所以在具体使用当中我们不得不把它拆成:
public class GridMap
{
public Node GetNode(int x, int y);
public Vector2 Size { get; set; }
public GridMapDrawer Drawer{ get; set; }
public void DrawNode(int x, int y, bool walkable, int cost)
{
drawer.DrawNode(x, y, walkable, cost);
}
public void DrawNodeInfo(float h, float g, bool isOpen, bool isClose)
{
drawer.DrawNodeInfo(h, g, isOpen, isClose);
}
}
public class GridMapDrawer
{
public void DrawNode(int x, int y, bool walkable, int cost);
public void DrawNodeInfo(float h, float g, bool isOpen, bool isClose);
}
两个类进行物理隔离以后,到时候即便要改绘制的类,我们也只要改GridMapDrawer
这个类就好了,这样避免不小心动到GridMap的代码,可以至少保证GridMap是安全的。
LSP:
程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。
也可说在开发过程中不要破坏代码的继承体系。
用一个Window类来做例子。
public class Window
{
public virtual void Show()
{
DrawWindow();
}
}
public class HisWindow : Window
{
public override void Show()
{
//do nothing.
}
}
public class MyWindow : Window
{
private int openWins = 0;
public override void Show()
{
base.Show();
openWins++;
}
}
Q1:如何防止同事在派生你写的类的时候无意或刻意违反你写该类的初衷?
ISP:
多个特定客户端接口要好于一个宽泛用途的接口。
拿刚才USB接口的例子。USB接口定义了4个方法,电源的输入输出,还有数据的输入输出。然而有的USB设备只输出电源,有的USB设备只读写数据,那么其他的方法的实现其实就是一个空方法。这会造成:1、冗余而又不可删除的代码产生,2、容易造成使用上的误解造成不必要的bug。
public class TffClass
{
public IUsb usbDevice;
public void Run()
{
if(Random.value < 0.1f)
usbDevice = new UsbPowerSupply();
else
usbDevice = new UsbDiskDrive();
}
}
public class YslClass
{
public TffClass tff;
public void Run()
{
WriteFile(DateTime.Now.ToString(), tff.usbDevice);
}
public void WriteFile(string fileName, IUsb usb)
{
// ...
}
}
解决办法就是:
public interface IUsbPower{ }
public interface IUsbData { }
pulic class TffClass
{
public IUsbData usbDevice;
//从根本上杜绝了我把充电宝当成usb移动硬盘的情况。
}
其它范例:Unity.EventSystems下的各种事件响应接口。
IPointerDownHandler
就只有一个OnPointerDown
方法。它并不设计成
public interface IPointerInteracctionHandler
{
void OnPointerDown();
void OnPointerUp();
void OnPointerEnter();
void OnPointerExit();
void OnPointerClick();
}
Q1: 当我们在实现一个接口时不得不留下空的方法,应该怎么办更好?
DIP:
DIP有2个概念:
- 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
根据GridMap的例子,GridMap引用了GridMapDrawer这个类,那么GridMap属于高层,GridMapDrawer属于低层次。依赖抽象的意思是GridMap和GridMapDrawer之间的关系不是直接引用关系,而是通过接口或者抽象类的间接类型引用。
public class GridMap
{
public Node GetNode(int x, int y);
public Vector2 Size { get; set; }
//原代码
//public GridMapDrawer Drawer { get; set; }
//现在的代码
//public IGridMapDrawer Drawer {get; set ; }
}
public interface IGridMapDrawer
{
void DrawNode(int x, int y, bool walkable, int cost);
void DrawNodeInfo(float h, float g, bool isOpen, bool isClose);
}
//原代码
//public class GridMapDrawer { ... }
//现在的代码
public class GridMapDrawer : IGridMapDrawer { ... }
为什么我要将GridMapDrawer抽象化成一个接口?在SRP的例子中提到,绘制类型库在不同平台下有不同的实现,即便是同一平台也可能有多种不同的实现,如此抽象化以后就预留了未来为不同的图形库绘制做准备。
比如windows下有GDI+和DirectX两种不同的图形库。如果我们上一个GridMapDrawer
的实现是基于GDI+的,这时候上头说我们可以尝试一下DirectX的绘制系统,此时我们可以实现两套代码,分别实现GDI+和DirectX的绘制类型。给自己留一手,万一上头反悔我们的改动仅仅是一行代码:
public class GdiGridMapDrawer : IGridMapDrawer
{
//...
}
public class DxGridMapDrawer : IGridMapDrawer
{
//...
}
public class Main
{
public static void Main(string[] args)
{
var map = new GridMap();
//create drawer like this
map.Drawer = new GdiGridMapDrawer();
//or
map.Drawer = new DxGridMapDrawer();
}
}
这么做完以后上头想怎么搞就怎么搞,他们开心就好,我们在实现不同类型的IGridMapDrawer类型上做到了零修改。
还有一个例子,比较贴近我们:道具。
一般情况下我们最方便的编写模式是这样的:
public class Prop
{
public void OnEat(Player p)
{
if(this.name == "zoom")
{
p.transform.localScale = Vector3.one * 1.5f;
}
else if(this.name == "shrink")
{
p.transform.localScale = Vector3.one * .5f;
}
else if(this.name == "speedup")
{
p.speed = 2f;
}
}
}
这时候多变的策划决定要删除缩小的道具,并且增加一个时间减慢和有绚丽特效的道具。改OnEat里的代码就违反了关闭修改的原则,所以我们应当这样抽象化。
//新建一个抽象类
public abstract class PropBase
{
public abstract void OnEat(Player p);
}
//实现各种具体道具类
public class ZoomProp : PropBase { }
public class SpeedupProp : PropBase{ }
public class TimeSlowProp : PropBase { }
public class FancyFxProp : PropBase { }
Q:用if else的模式写其代码来速度更快也更直观为什么一定要写成这样?
OCP:
开放扩展,关闭修改。这个概念特别的虚,它是一个非常范的规则,但是基本上就是利用到面对对象语言中的多态和继承的规则。
比如我们要实现一个新的GridMapDrawer,但是多了一个方法叫做DrawRoute(),那么我们直接修改GridMapDrawer的话就违反了OCP,但是由于我们的GridMapDrawer继承了IGridMapDrawer的方法,那我们可以这样:
public interface IGridMapDrawRoute
{
void DrawRoute(Node[] nodes);
}
public class AdvGridMapDrawer : GridMapDrawer, IGridMapDrawRoute
{
public void DrawRoute(Node[] nodes)
{
//...
}
}
开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
Q:一个类中的bug修复和功能本身的修改、修改是不是违反了开闭原则?
最后的总结:
OCP:告诉我们用抽象构建框架,用实现扩展细节。
而其他4个原则只是我们实现细节中的注意事项:
SRP:实现类要职责单一;
LSP:不要破坏类的继承体系;
DIP:要面向抽象编程;
ISP:设计接口要尽量精简单一。
Q: 我们越坚持SOLID的原则就越背离KISS原则。那我们在平时的开发中应该怎么办?
网友评论