从一些老生常谈的事情开始说起来吧,优秀的代码应符合以下特质:

1,可维护性

2,可扩展性

3,模块化

如果代码在其生命周期内保持易于维护、扩展和模块化,那么就上面列出的特性而言,这意味着代码高于平均水平。

下面所示的代码示例更接近Java和C#,但它们对于面向对象编程领域的任何开发者都是有帮助的。

以下是完整的原则列表:

1.Single Responsibility Principle (SOLID) 单一责任性原则

2. High Cohesion (GRASP) 高内聚

3. Low Coupling (GRASP) 低耦合

4. Open Closed Principle (SOLID) 开闭原则

5. Liskov Substitution principle (SOLID)  里氏替换原则

6. Interface Segregation Principle (SOLID) 接口分离原则

7. Dependency Inversion Principle (SOLID) 依赖倒置原则

8. Program to an Interface, not to an Implementation 面向接口编程

9. Hollywood Principle 好莱坞原则

10. Polymorphism (GRASP) 多态原则

11. Information Expert (GRASP) 信息专家模式

12. Creator (GRASP) 创造者原则

13. Pure Fabrication (GRASP) 纯虚构原则

14. Controller (GRASP) 控制器原则

15. Favor composition over inheritance 组合优于继承

16. Indirection (GRASP) 间接原则

·  正  ·  文  ·  来  ·  啦  ·

单一责任原则

单一责任原则(SRP)规定:

每个类应该只负责一种单一的功能

一个类使用其函数或契约(以及数据成员帮助函数)来履行其职责。

我们来看下面的这个类:

Class Simulation{
Public LoadSimulationFile()
Public Simulate()
Public ConvertParams()
}

这个类处理两个职责。第一类是加载仿真数据,第二类是执行仿真算法(使用Simulate和ConvertParams函数)。

类使用一个或多个函数履行职责。在上面的示例中,加载模拟数据是一种责任,执行模拟是另一种责任。加载模拟数据(即LoadSimulationFile)需要一个函数。剩下的两个功能需要执行模拟。

那么如何分辨自己的类有哪些功能呢?参考功能的定义短语为“改变的原因”。因此,寻找一个类改变的所有原因,如果有一个以上的理由需要改动这个类,那么这意味着这个类并没有遵守单一功能原则

上面的示例中,这个类不应该包含LoadSimulationFile函数(或者装载仿真数据的功能)。如果我们创建一个单独的类来加载模拟数据,那么这个类就不再违反SRP原则了。

一个类只能有一个功能,那么在设计软件的时候我们如何去遵守这个严格的规则?

让我们来考虑一下另一个与SRP密切相关的原则:高内聚性。高内聚力会给你一个主观的尺度,而不是客观的尺度,就像SRP那样。非常低的内聚力意味着一个类要履行许多职责。例如,一个类负责的职责超过10个。低内聚意味着一个类要履行5项职责,中等内聚意味着一个类要履行3项职责。高内聚意味着履行一个单一的责任。因此,设计时的经验法则是力求高内聚。

另一个需要在这里讨论的原则是低耦合。这个原则表明一个类应该独立完成特定的功能,使得类之间保持低依赖性。再次审视上面的示例类,在应用SRP和高内聚规则之后,我们决定创建一个独立的类来处理模拟数据文件。这样,我们就创建了两个互相依赖的类。

看起来采用高内聚似乎和低耦合原则相抵触了。因为原则是最小化耦合,并不是使耦合为零,因此这种程度的耦合是可以接受的。对于创建一个通过对象之间协作完成任务的面向对象的程序设计来说,一定程度的耦合是正常的。

另一方面,考虑一个链接数据库的GUI类,通过HTTP协议链接远程客户端并处理屏幕布局。这个GUI类依赖了太多的类,很明显违反了低耦合原则。如果不包含所有的相关类则该类不能被重用,任何对数据库组件的改变都将改变这个GUI类。

开闭原则

开闭原则描述为:

一个软件模块(类或者方法)应该对拓展开放而对修改关闭

换句话说,不能更新已经为项目编写的代码,但可以向项目添加新代码。

以下则是使用继承应用开放原则的示例:

Class DataStream{
Public byte[] Read()
}
Class NetworkDataStream:DataStream{
Public byte[] Read(){
//Read from the network }
}
Class Client {
Public void ReadData(DataStream ds){
ds.Read();  }
}

在这个示例中,客户端读取(ds.Read())来自于网络数据流。如果我想要扩展这个客户端的功能使之能够读取其他数据流的内容,例如PCI数据流,那么我需要添加另外继承自DataStream的子类,如下所示:

Class PCIDataStream:DataStream{
Publc byte[] Read(){
//Read data from PCI    }
}

在这种情况下,客户端代码的运行没有任何错误。客户端认识基类,因此可以传递DataStream两个子类中的任何一个的对象,这样,客户端可以在未知子类的情况下读取数据。这是在不修改任何现有代码的情况下实现的。

我们也可以使用组合来应用这个原理,并且还有其他方法和设计模式来应用这个原理。其中一些方法将在本文中讨论。

然而,你必须将这个原则应用于每一段代码吗?当然不是了,因为大部分的代码其实是不怎么变动的,你只需要战略性的将这个原则应用到那些你预计将来会有变动的代码片上即可。

里氏替换原则

Liskov替代原则指出:

子类应当可以替换父类并出现在父类能够出现的任何地方

查看此定义的另一种方法是抽象类(接口或抽象类)对于客户端应该足够了。

为了详细说明,让我们考虑一个例子,如下:

Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}

这个例子是数据采集装置的抽象。数据采集装置按其接口类型不同而区分,它能够使用USB接口,网络接口(TCP 或者 UDP),PIC接口或者另外的计算机接口。然而,客户端设备不需要知道与其链接的是何种数据采集装置。为了在不改变客户端代码的情况下适应新的采集装置,这就需要程序员给接口提供极大的灵活性。

让我们回顾一下实现IDevice接口的两个具体类的历史,如下所示:

public class PCIDevice:IDevice {  public void Open(){ // Device specific opening logic    }   public void Read(){ // Reading logic specific to this device    }   public void Close(){    // Device specific closing logic.   }
}
public class NetWorkDevice:IDevice{ public void Open(){ // Device specific opening logic    }   public void Read(){ // Reading logic specific to this device    }   public void Close(){    // Device specific closing logic.   }
}

这三个方法(打开、读取和关闭)对于处理设备传入的数据已经足够了。然后,假设需要添加基于USB接口的另一个数据采集设备。

USB设备的问题在于,当你打开连接时,来自先前连接的数据仍保留在缓冲区中。因此,在对USB设备的第一次读取调用时,返回来自上一个会话的数据。有针对性的采集行为会破坏这些数据。幸运的是,基于USB的设备驱动程序提供刷新功能,预先清除了基于USB的采集设备中的缓冲区。那么如何在代码中实现这个功能,并使代码的改动最小?

一个简单但是草率的解决方案是更新代码,通过标识识别是否调用USB设备,如下:

public class USBDevice:IDevice{    public void Open(){ // Device specific opening logic    }   public void Read(){ // Reading logic specific to this device<br>  }
public void Close(){    // Device specific closing logic.   }   public void Refresh(){  // specific only to USB interface Device    }
}
//Client code..
Public void Acquire(IDevice aDevice){   aDevice.Open(); // Identify if the object passed here is USBDevice class Object.
if(aDevice.GetType() == typeof(USBDevice)){
USBDevice aUsbDevice = (USBDevice) aDevice;
aUsbDevice.Refresh();
}   // remaining code….
}

在这个解决方案中,客户端代码直接使用具体类以及接口(或抽象)。这意味着抽象不能够让客户履行其职责。

另一种陈述方式是基类无法满足需求(刷新操作),但是子类可以,实际上,子类有该项行为。因此,派生类和基类不兼容且子类不能被代替。所以,该解决方案违反了里氏替换原则。

下面这个示例中,客户端依赖于更多的实体(iDevices 和 USB Devices),一个实体的任何一点改变都将影响其他实体。因此,违反LSP原则将导致类之间的互相依赖。

下面是遵循LSP这个问题的解决方案:

Public Interface IDevice{
Void Open();
Void Refresh();
Void Read();
Void Close();
}

现在客户端如下:

Public void Acquire(IDevice aDevice)
{
aDevice.open();
aDevice.refresh();
aDevice.acquire()
//Remaining code..
}

现在客户端不依赖于iDevice的具体实现。因此,在此解决方案中,我们的接口(iDevice)足够满足客户端的需求。

在面向对象分析的上下文中,可以用另一个角度来看待LSP原理。总之,在OOA期间,我们考虑的类及其层次结构,它们可能是我们软件需要的一部分。

当我们考虑类和层级结构的时候我们可能会设计一些违反LSP规则的类。

让我们思考一个古典的例子,即长方形和正方形。一开始看起来正方形是长方形的特例,于是一个乐观的程序设计师将绘制出下面的层级继承关系:

Public class Rectangle{
Public void SetWidth(int width){}
Public void SetHeight(int height){}
}
Public Class Square:Rectangle{
//
}

接下来你会发现你不能使用这个正方形的对象去代替长方形的对象了。因为正方形继承自长方形,所以它也继承了设置长度和宽度的方法。于是一个正方形的客户端能够随意改变自己的长和宽为不同的大小,但是实际上正方形的长宽应该是相同的,因此我们软件的这个正常行为就失败了。

这个问题只能根据不同的使用场景和条件具体分析类来避免。因此如果你孤立的设计一个类很可能在实际运行中将会出错。就像我们的正方形和长方形那样,一开始认为很完美的关系设计,在不同的条件下,这种关系设计最终被认定并不符合我们软件正常运行的要求。

接口隔离原理

接口隔离原则(ISP)规定:

客户端不应该被强迫依赖他们不使用的接口

还是考虑前一个例子:

Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}

实现此接口有三个类:USBDevice,NetworkDevice和PCIDevice。 这个接口足够与网络和PCI设备配合使用。但是USB设备则需要另一个功能(Refresh())才能正常工作。

和USB设备一样,也许还有另外的设备也需要这个函数来支持工作。为此,接口被更新如下:

 Public Interface IDevice{
Void Open();
Void Refresh();
Void Read();
Void Close();
}

那么问题来了,任何一个实现该接口的类都需要去实现Refresh函数。

例如为了满足以上的设计,必须对网络设备和PCID设备添加下面的代码:

public  void Refresh()
{
// Yes nothing here… just a useless blank function
}

因此,iDevice代表一个Fat接口(功能太多)。此设计违反了接口隔离原则,因为Fat接口会导致不必要的客户端依赖于它。

有很多方法可以解决这个问题,但我将在保持我们预先定义的面向对象解决方案范围内的同时解决这个问题。

我们知道在open操作之后就会直接调用refresh函数,因此,我改变逻辑将设备客户端的refresh函数迁移至具体的实现类内部。在本例中我将调用refresh的逻辑移动到USB设备的具体实现类中:

Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}
Public class USBDevice:IDevice{
Public void Open{
// open the device here…
// refresh the device
this.Refresh(); }
Private void Refresh(){
// make the USb Device Refresh  }
}

通过这个方式,我减少了基类中的函数数目,让它变得更轻了。

依赖倒置原则(DIP)

这一原则是上述其他原则的概括。
在我们给出DIP的书面定义之前,请让我介绍一个与此紧密相连的一条原则,以帮助我们理解DIP。

也就是说:

面向接口编程,而不是面向实现编程

这很简单,请考虑以下示例:

Class PCIDevice{
Void open(){}
Void close(){}
}
Static void Main(){
PCIDevice aDevice = new PCIDevice();
aDevice.open();
//do some work
aDevice.close();
}

上面的例子违反了“程序到接口”的原则,因为我们正在使用具体类PCI Device的实例。下面的列子遵循这个原则:

Interface IDevice{
Void open();
Void close();
}
Class PCIDevice implements IDevice{
Void open(){ // PCI device opening code }
Void close(){ // PCI Device closing code }
}
Static void Main(){
IDevice aDevice = new PCIDevice();
aDevice.open();
//do some work
aDevice.close();
}

因此,遵循这一原则非常容易。依赖倒置原则与此原则类似,但DIP需要我们再做一步。

依赖反转:高级模块不应该依赖低级模块。二者应该依赖于抽象。

您可以很容易地理解为“两者都应该依赖于抽象”,正如它所说,每个模块都应该编程到一个接口。那么什么是高级模块和低级模块呢?

想要理解第一部分,我们必须要了解实际的高级模块和低级模块是什么?

请参阅以下代码:

Class TransferManager{
public void TransferData(USBExternalDevice usbExternalDeviceObj,SSDDrive  ssdDriveObj){ Byte[] dataBytes = usbExternalDeviceObj.readData();    // work on dataBytes e.g compress, encrypt etc..    ssdDriveObj.WrtieData(dataBytes);   }
}
Class USBExternalDevice{
Public byte[] readData(){   }
}
Class SSDDrive{
Public void WriteData(byte[] data){
}
}

上面的代码有三个类,TransferManager代表高级模块,因为它在一个方法中用了其它两个类。因此其他两个类则是低级模块。
在上面的代码中,高级模块使用较低级别的模块直接(没有任何抽象),因此违反了依赖性反转原则。

违背了依赖反转这条原则会让你的软件系统变得难以更改。比如,如果你想增加其他的外部设备,你将不得不改变高级模块。因此你的高级模块将会依赖于低级模块,依赖会让代码变得难以改变。

如果你理解了上面的原则:“面向接口编程”,那么这个解决方案就很容易了。

例如:

Class USBExternalDevice implements IExternalDevice{
Public byte[] readData(){
}
}
Class SSDDrive implements IInternalDevice{
Public void WriteData(byte[] data){
}
}
Class TransferManager implements ITransferManager{
public void Transfer(IExternalDevice externalDeviceObj, IInternalDevice internalDeviceObj){ Byte[] dataBytes = externalDeviceObj.readData();   // work on dataBytes e.g compress, encrypt etc..    internalDeviceObj.WrtieData(dataBytes); }
}
Interface IExternalDevice{  Public byte[] readData();
}
Interfce IInternalDevice{
Public void WriteData(byte[] data);
}
Interface ITransferManager {
public void Transfer(IExternalDevice usbExternalDeviceObj,SSDDrive  IInternalDevice);
}

从上面的例子中可以看出,高级模块和低级模块都依赖于抽象。因此它遵循依赖性倒置原则。

好莱坞原则

该原理类似于依赖性倒置原则描述为:

不要调用我们,我们会给你

这意味着高级组件可以以某种方式指示低级组件(或调用它们),这样两个组件都能不依赖于另一个了。

这条原则可以防止依赖恶化。依赖恶化发生在每个组件都依赖于其他各个组件。换句话说,依赖恶化是让依赖发生在各个方向(向上,横向,向下)。Hollywood原则可以让我们时依赖只向一个方向。

DIP和Hollywood之间的差异给了我们一条通用原则:无论是高级组件还是低级组件,都要依赖于抽象而不是具体的类。另一方面,Hollywood原则强调了高级组件和低级组件应该以不产生依赖的方式交互。

多态原则

什么 ?多态也是设计原则?没错,多态是任何面向对象语言都要提供的基础特征,它可以让父类的引用指向子类。

这也是GRASP的设计原则。 该原则提供了有关如何在面向对象设计中使用此OOP语言的功能。

这条原则严格限制了运行时类型信息的使用(RTTI)。在C#中,我们用如下方式实现RTTI:

if(aDevice.GetType() == typeof(USBDevice)){
//This type is of USBDEvice
}

在Java中,RTTI是使用函数getClass()或instanceOf()来完成的

if(aDevice.getClass() == USBDevice.class){    // Implement USBDevice  Byte[] data = USBDeviceObj.ReadUART32();
}

如果您已在项目中编写此类型代码,那么现在是时候重构该代码并使用多态原则对其进行改进的时候了。
请看下图:

在此我在接口中生成了read方法,然后委托他们的实现类去实现该方法,现在,我只用方法Read:

//RefactoreCode
IDevice aDevice = dm.getDeviceObject();
aDevice.Read();

getDeviceObject()的实现将从何而来?我们将在下面的创建者原则和信息专家原则中讨论,您将更好的学习如何将职责分配给类。

信息专家原则

这是一个简单的GRASP原则,它给出了关于赋予类职责的指导。你应该为具有履行该职责所必需信息的类分配责任。

如图:

在我们的场景中,模拟以全速(每秒600个循环)执行,而用户显示器将以降低的速度更新。在这里,我们必须分配一个责任来确定是否显示下一帧。

哪个类应该承担这个责任?我们有两个选项:Simulation类或SpeedControl类。

现在,SpeedControl类具有关于哪些帧已在当前序列中显示的信息,因此根据Information Expert SpeedControl应该具有此职责。

创建者原则

Creator是一个GRASP原则,它有助于确定哪个类应该负责创建一个类。对象创建是一个重要的过程,在决定谁应该创建类的实例时有一个原则是有用的。

根据Larman的说法,想要满足以下任何条件,则应该赋予班级B以创建另一个班级A的责任。
a)B含有A.
b)B聚合物A.
c)B具有A的初始化数据
d)B记录A.
e)B密切使用A.

在我们的多态性示例中,我使用了InformationExpert和Creator原则来赋予DeviceManager类创建设备对象(dm.getDeviceObject())的职责。这是因为DeviceManger具有创建设备对象的信息。

纯虚构原则

为了更好的理解Pure Fabrication,您首先必须要了解面向对象分析(OOA)。

面向对象分析是一个过程,通过它您可以识别问题域中的类。例如,银行系统的域模型包含类,如帐户,分支,现金,支票,交易等。在此示例中,域类需要存储有关客户的信息。为了做到这一点,一个选项是将数据存储责任委托给域类。此选项将降低域类的凝聚力(多个职责)。最终,此选项违反了SRP原则。

另一种选择是引入另一个不代表任何域概念的类。在银行示例中,我们可以引入一个名为“PersistenceProvider”的类。这个类不代表任何域实体。此类的目的是处理数据存储功能。因此“PersistenceProvider”是纯粹的制作。

控制器原则

当我开始开发软件时,我使用Java的swing组件来编写程序,而我的大多数逻辑都是在幕后。

然后我学习了域模型。所以,我把我的逻辑从幕后转移到了Domain模型。但我直接从侦听器调用域对象。这会在GUI组件(侦听器)和域模型之间创建依赖关系。控制器设计原则有助于把GUI组件和域模型类之间的依赖关系最小化。

控制器有两个目的。 第一个是封装系统操作。系统操作是您的用户想要实现的,例如购买产品或将商品输入购物车。然后通过调用软件对象之间的一个或多个方法调用来完成该系统操作。 第二个是在UI和域模型之间的提供层。

UI使用户能够执行系统操作。Controller是处理系统操作请求的UI层之后的第一个对象,然后将责任委派给底层域对象。

从UI我们将“移动游标”的责任委托给此Controller,然后调用底层域对象来移动游标。

通过使用Controller原则,您可以灵活地插入另一个用户界面,如命令行界面或Web界面。

组合优于继承

面向对象编程中主要有两个工具来扩展现有代码的功能。 第一个是继承,第二个是组合。

在编程中,通过引用另一个对象,您可以扩展该对象的功能。 如果使用合成来添加一个新类去创建其对象,那么就可以使用它的对象来扩展代码。

组合的一个非常有用的功能是可以在运行时设置行为。 另一方面,使用继承只能在编译时设置行为。

以下是类设计:

我们可以添加新的类并把它们的引用在自己的代码中。 请参阅下面的列表:

clientData.setPolarity(new PolarityOfTypeA); // or clientData.setPolarity(new PolarityOfTypeB)
clientData.FormatPolarity;
clientData.setEndianness(new LittleEndiannes());// setting the behavior at run-time
clientData.FormatStream();

那么,我们可以根据自己想要的行为提供类的实例。 这个功能减少了类的总数,当然最终也减少了可维护性问题。

间接原则

间接原则给出了这样一个问题:你如何使对象以一种薄弱的方式进行交互?

方法就是:将交互的责任交给中间对象,使不同组件之间的耦合保持在较低的水平。

例如,要将域代码与配置分离,需要添加一个特定的类 - 如下所示:

Public Configuration{   public int GetFrameLength(){    // implementation   }   public string GetNextFileName(){    }   // Remaining configuration methods
}

这样一来,不管哪个域想要读取某个配置设置,它都要询问Configuration类对象。 因此,主代码必须要与配置代码分离。

如果您已经阅读了纯虚构原则,那么这个配置类就是纯虚构的一个例子。但是间接的目的是创建去耦合。而纯虚构的目的则是保持领域模型的整洁,只代表领域概念和职责。

通过这篇文章,相信您能够很快的了解面向对象的设计原则,并牢牢掌握SOLID和GRASP规则背后的思想,这些原则是非常基础而且重要的。正是由于这些原则的基础性,理解、融汇贯通这些原则需要不少的经验和知识的积累。上述的图片以及代码很好的注释了这些原则。希望这篇文章能够对您有所帮助!

长按二维码 ▲

订阅「架构师小秘圈」公众号

如有启发,帮我点个在看,谢谢↓

不懂SOLID,GRASP这些软件开发原则!写出来的代码都是垃圾!相关推荐

  1. 趣图图解 SOLID 软件开发原则

    今天早上我发现了Motivator这个工具.它能让你制作出自己想要的图片.下面就是我的首次尝试,以SOLID软件开发原则为主题的具有启发意义的图片.这图片都是从谷歌里搜索出来的,我"借用&q ...

  2. 《敏捷软件开发-原则、方法与实践》-Robert C. Martin

    Review of Agile Software Development: Principles, Patterns, and Practices 本书主要包含4部分内容,这些内容对于今天的软件工程师 ...

  3. 开课吧:深入了解软件开发原则有哪些?

    在软件开发中,前人对软件系统的设计和开发总结了一些原则和模式,不管用什么语言做开发,都将对我们系统设计和开发提供指导意义. 深入了解软件开发原则有哪些? 1.不要重复你自己:DRY(Don'trepe ...

  4. 对《敏捷软件开发:原则、模式与实践》中保龄球程序重构的一些思考

    前几天看了<敏捷软件开发:原则.模式与实践>中第六章:一次编程实战,文章中主要描述了一对开发人员进行一次记录保龄球比赛成绩程序的开发过程.仔细研究之后,发现一个问题,拿出来和大家讨论讨论. ...

  5. [zt]软件开发金钥匙——写给毕业生的忠告

    转自:http://www.cppblog.com/szhoftuncun/archive/2008/09/29/63052.html [zt]软件开发金钥匙--写给毕业生的忠告 "又是一年 ...

  6. 举例说明层次分析的三大原则_20202021企业软件开发流程(3)软件开发过程和软件开发原则...

    知识点 1.软件过程就是软件开发过程中软件活动的集合. 2.软件过程各阶段定义 1)问题定义:人们通过开展技术探索和市场调查等活动,研究系统的可行性和可能的解决方案,确定待开发系统的总体目标和范围. ...

  7. 敏捷软件开发:原则、模式与实践(C#版)

    刚才在china-pub看到<敏捷软件开发:原则.模式与实践(C#版)>已经出版了.这本书是以前那本<敏捷软件开发:原则.模式与实践>的C#版,这是不是说明C#程序员的数量已经 ...

  8. 一些软件软件开发原则

    下面这些原则,不单单只是软件开发,可以推广到其它生产活动中,甚至我们的生活中. Don't Repeat Yourself (DRY) DRY 是一个最简单的法则,也是最容易被理解的.但它也可能是最难 ...

  9. 敏捷软件开发:原则、模式与实践pdf

    下载地址:网盘下载 内容简介  · · · · · · 在本书中,享誉全球的软件开发专家和软件工程大师Robert C.Martin将向您展示如何解决软件开发人员.项目经理及软件项目领导们所面临的最棘 ...

最新文章

  1. mysql 触发器编程_【mysql的编程专题】触发器
  2. 面试必备:4种经典限流算法讲解
  3. 常用正则表达式大全——包括校验数字、字符、一些特殊的需求
  4. 怎么注销笔记本icloud_如何在笔记本电脑或台式机的Web浏览器中在线查看Apple iCloud照片
  5. mysql临时关闭索引功能_MYSQL中常用的强制性操作(例如强制索引)
  6. win7下安装python失败问题_win7下安装ipython失败
  7. 天池-新闻推荐-Baseline
  8. C# continue,break,return 跳转语句的用法
  9. java并发:初探用户线程和守护线程
  10. 正态分布的概率密度函数python_python绘制正态分布及三大抽样分布的概率密度图像...
  11. 管理感悟:你是产品的第一个用户
  12. android sid如何验证有效性,使用RMAN验证备份的有效性
  13. 基于labview的周立功usbcan盒的研究
  14. CAD转CAD注意事项
  15. 三菱 PLC ST语言 步进电机正反转
  16. DB2数据库HANG住的时候应该收集什么数据以及如何处理
  17. 【淘宝商家应用接口】拼多多平台流量解析,如何充分利用平台分配的流量?
  18. 《联盟》读书笔记(二)
  19. 十年老撕鸡分享,五分钟搭建个人轻论坛
  20. c++“不允许使用不完整的类型“

热门文章

  1. 最小割 ---- 集合冲突模型 ----- P1646 [国家集训队]happiness
  2. php 读取excel转html,PHPExcel 转HTML
  3. 数学建模公式编辑器_一款“神奇”的数学公式编辑器
  4. UVA10652 Board Wrapping(求凸包、计算凸多边形面积)
  5. HDU 4635 Strongly connected(缩点、最多可加边数使得仍然非强连通)
  6. 算法_贪心 刷题总结
  7. 计算机网络实验三张芳,【喜讯】实验室于俊清老师获得2009年华中科技大学青年教师教学竞赛一等奖...
  8. 计算机网络与通信pdf谢希仁_考研刷题资料谢希仁《计算机网络》(第7版)配套题库【考研真题精选(部分视频讲解)+章节题库】...
  9. centos pureftpd mysql_使用PureFTPd和MySQL的虚拟主机(包括配额和带宽管理)在CentOS 6.2上...
  10. Mysql中分页查询两个方法比较