有人提出要用OO的思路来解决算法问题,OK,没问题,eaglet 今天就尝试用OO来实现算法。既然谈OO,我们就重点讨论设计,不重点讨论算法的效率了。
有道难题之OO
有人提出要用OO的思路来解决算法问题,OK,没问题,eaglet 今天就尝试用OO来实现算法。既然谈OO,我们就重点讨论设计,不重点讨论算法的效率了。
在开始OO之前,我想先说说什么是OO设计。OO设计就是面向对象设计,有人说不要认为你用了C#就OO了,不错用C#照样可以写出面向过程的代码。不过eaglet 还有补充一下,不要认为你用了class 就OO了。程序是否OO,要看程序是否很好的体现了面向对象的思想。面向对象程序设计可以被视作一种在程序中包含各种独立而又互相调用的单位和对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数 的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看 作一个小型的“机器”,或者说是负有责任的角色。
下面再简单列举一下面向对象的基本设计原则:
1) 单一职责原则 (The Single Responsiblity Principle,简称SRP)
2) 开放-封闭原则 (The Open-Close Principle,简称OCP)
3) Liskov 替换原则(The Liskov Substitution Principle,简称LSP)
4) 依赖倒置原则(The Dependency Inversion Pricinple,简称DIP)
5) 接口隔离原则 (The Interface Segregation Principle,简称ISP)
一、单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。测试驱动的开发实践常常会在设计出现臭味之前就迫使我们分离职责。
二、开闭原则(OCP)
软件实体(类、模块、函数)应该是可扩展的,但是不可修改的。也就是说:对于扩展是开放的,对于更改是封闭的。怎样可能在不改动模块源代码的情况下 去更改它的行为呢?怎样才能在无需对模块进行改动的情况下就改变它的功能呢?关键是抽象!因此在进行面向对象设计时要尽量考虑接口封装机制、抽象机制和多 态技术。该原则同样适合于非面向对象设计的方法,是软件工程设计方法的重要原则之一。
三、替换原则(LSP)
子类应当可以替换父类并出现在父类能够出现的任何地方。这个原则是Liskov于1987年提出的设计原则。它同样可以从Bertrand Meyer 的DBC (Design by Contract〔基于契约设计〕) 的概念推出。
四、依赖倒置原则(DIP)
1、高层模块不应该依赖于低层模块。二者都应该依赖于抽象。2、抽象不应该依赖于细节。细节应该依赖于抽象。在进行业务设计时,与特定业务有关的依 赖关系应该尽量依赖接口和抽象类,而不是依赖于具体类。具体类只负责相关业务的实现,修改具体类不影响与特定业务有关的依赖关系。在结构化设计中,我们可 以看到底层的模块是对高层抽象模块的实现(高层抽象模块通过调用底层模块),这说明,抽象的模块要依赖具体实现相关的模块,底层模块的具体实现发生变动时 将会严重影响高层抽象的模块,显然这是结构化方法的一个"硬伤"。面向对象方法的依赖关系刚好相反,具体实现类依赖于抽象类和接口。
五、接口分离原则(ISP)
采用多个与特定客户类有关的接口比采用一个通用的涵盖多个业务方法的接口要好。 ISP原则是另外一个支持诸如COM等组件化的使能技术。缺少 ISP,组件、类的可用性和移植性将大打折扣。这个原则的本质相当简单。如果你拥有一个针对多个客户的类,为每一个客户创建特定业务接口,然后使该客户类 继承多个特定业务接口将比直接加载客户所需所有方法有效。
当然并不是说面向对象设计必须要严格遵守这些原则,但我们在设计时至少需要参照这些原则,就如数据库设计需要参照范式原则一样。
下面我们开始正题:
我们就拿那个第一题来举例子,题目见 有道难题之eaglet的算法
这个题目本身很简单,既然要面向对象,我们需要考虑我们对这种问题做面向对象设计的目的是什么?我们假设我们有一个系统,这个系统专门用来计算农田中的胡萝卜,我们除了要计算特殊值在某个范围胡萝卜总数,可能还要计算这些符合条件的特殊值总和,可能还有很多很多业务需求。另外我们可能还需要能够方便的定义特殊值,有时候特殊值定义为某个网格中周围的胡萝卜数,有时候则为周围胡萝卜数加上自身的胡萝卜数。用户界面可以指定采用哪种特殊值定义方法来计算。有了这些需求,我们可以开始设计了。
首先我们需要把业务逻辑和数据分开,业务逻辑层只知道从给定的农田的各个网格中取出数据,并不关心这些数据是通过如何计算得来的。数据访问层则需要能够让业务层不进行代码修改的情况下可以得到不同定义的特殊值数据。
首先我们来设计数据访问层
第一步我们定义一个实体类
这个实体类提供某个网格的特殊值数据
public class Cell

{
private int _SpecialNumber;

public int SpecialNumber

{

get
{ return _SpecialNumber; }
}

public Cell(int specialNumber)

{
_SpecialNumber = specialNumber;
}
}
接下来考虑到业务层需要不进行修改就能够更改网格的特殊值定义,我们设计一个抽象工厂,通过这个抽象工厂抽象出不同类型
的工厂以生产不同特殊值定义的网格对象。

/**//// <summary>
/// 生产Cell的工厂抽象接口
/// </summary>
public interface ICellFactory

{
void Init(string[] field);


/**//// <summary>
/// 根据指定的 i, j 生产出一个Cell
/// </summary>
/// <param name="i"></param>
/// <param name="j"></param>
/// <returns></returns>
Cell Output(int i, int j);
}
完成了这两个,数据访问层的设计就结束了,下面就是业务逻辑层了。
业务逻辑层比较简单,就是根据需求从数据层获取数据并进行一定的逻辑计算得到结果提供给调用者。
简单起见,我只实现了题目要求的业务逻辑
class T1_Object

{
public static int countSpecialNumbers(string[] field, int A, int B, ICellFactory cellFactory)

{
//Check paramaters
if (field == null)

{
return 0;
}

if (field.Length == 0)

{
return 0;
}

int width = field[0].Length;

if (width == 0)

{
return 0;
}

if (A > B)

{
throw new ArgumentException("A > B");
}

cellFactory.Init(field);

//Begin calculate
int count = 0;

for (int i = 0; i < field.Length; i++)

{
for (int j = 0; j < field[i].Length; j++)

{
Cell cell = cellFactory.Output(i, j);

if (cell.SpecialNumber >= A && cell.SpecialNumber <= B)

{
count++;
}

}
}

return count;
}
}
和原题的区别是这里多了一个工厂接口的输入参数。
设计到这里实际上已经完成,下面要做的是编写两个工厂类,一个用于生产第一种特殊值定义的网格对象(特殊值定义为某个网格中周围的胡萝卜数),一个用于
生产第二种特殊值定义的网格对象(特殊值定义为周围胡萝卜数加上自身的胡萝卜数)。

/**//// <summary>
/// 特殊值定义为某个网格中周围的胡萝卜数
/// </summary>
public class Factory1 : ICellFactory

{
string[] _Matrix;
int _Width;

private int GetSpecialNumbers(int i, int j)

{
int y = i - 1 >= 0 ? i - 1 : 0;
int sum = 0;

while (y <= i + 1 && y < _Matrix.Length)

{
int x = j - 1 >= 0 ? j - 1 : 0;

while (x <= j + 1 && x < _Width)

{
if (x != j || y != i)

{
sum += _Matrix[y][x] - '0';
}

x++;
}

y++;
}

return sum;
}


ICellFactory Members#region ICellFactory Members

public void Init(string[] field)

{
_Matrix = field;
_Width = field[0].Length;
}

public Cell Output(int i, int j)

{
return new Cell(GetSpecialNumbers(i, j));
}

#endregion
}

/**//// <summary>
/// 特殊值定义为周围胡萝卜数加上自身的胡萝卜数
/// </summary>
public class Factory2 : ICellFactory

{
string[] _Matrix;
int _Width;

private int GetSpecialNumbers(int i, int j)

{
int y = i - 1 >= 0 ? i - 1 : 0;
int sum = 0;

while (y <= i + 1 && y < _Matrix.Length)

{
int x = j - 1 >= 0 ? j - 1 : 0;

while (x <= j + 1 && x < _Width)

{
sum += _Matrix[y][x] - '0';
x++;
}

y++;
}

return sum;
}


ICellFactory Members#region ICellFactory Members

public void Init(string[] field)

{
_Matrix = field;
_Width = field[0].Length;
}

public Cell Output(int i, int j)

{
return new Cell(GetSpecialNumbers(i, j));
}

#endregion
}

测试代码

Code
Console.WriteLine("面向对象 工厂1");
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "111", "111", "111" },
4, 8, new Factory1()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "111", "141", "111" },
4, 8, new Factory1()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "2309", "0239", "2314" },
5, 7, new Factory1()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "924", "231", "390", "910", "121" },
31, 36, new Factory1()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "5" },
3, 8, new Factory1()));
Console.WriteLine(T1_Object.countSpecialNumbers(testField,
3, 18, new Factory1()));
Console.WriteLine("面向对象 工厂2");
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "111", "111", "111" },
4, 8, new Factory2()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "111", "141", "111" },
4, 8, new Factory2()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "2309", "0239", "2314" },
5, 7, new Factory2()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "924", "231", "390", "910", "121" },
31, 36, new Factory2()));
Console.WriteLine(T1_Object.countSpecialNumbers(new string[] { "5" },
3, 8, new Factory2()));
Console.WriteLine(T1_Object.countSpecialNumbers(testField, 3, 18, new Factory2()));
结果
面向对象 工厂1
5
9
3
0
0
26
面向对象 工厂2
8
4
2
1
1
26
下面我们再对照面向对象设计要求来检查这个设计
一、单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。测试驱动的开发实践常常会在设计出现臭味之前就迫使我们分离职责
数据实体类 Cell 只在构造的时候引起变化,其职责和其他类完全分离。
二、开闭原则(OCP)
软 件实体(类、模块、函数)应该是可扩展的,但是不可修改的。也就是说:对于扩展是开放的,对于更改是封闭的。怎样可能在不改动模块源代码的情况下 去更改它的行为呢?怎样才能在无需对模块进行改动的情况下就改变它的功能呢?关键是抽象!因此在进行面向对象设计时要尽量考虑接口封装机制、抽象机制和多 态技术。该原则同样适合于非面向对象设计的方法,是软件工程设计方法的重要原则之一。
更改特殊值定义的行为不需要改动业务逻辑层和数据访问层代码,只需要实现对应的接口就可以完成。
三、替换原则(LSP)
子类应当可以替换父类并出现在父类能够出现的任何地方。这个原则是Liskov于1987年提出的设计原则。它同样可以从Bertrand Meyer 的DBC (Design by Contract〔基于契约设计〕) 的概念推出。
这里没有设计子类和父类,忽略。
四、依赖倒置原则(DIP)
1、 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。2、抽象不应该依赖于细节。细节应该依赖于抽象。在进行业务设计时,与特定业务有关的依 赖关系应该尽量依赖接口和抽象类,而不是依赖于具体类。具体类只负责相关业务的实现,修改具体类不影响与特定业务有关的依赖关系。在结构化设计中,我们可 以看到底层的模块是对高层抽象模块的实现(高层抽象模块通过调用底层模块),这说明,抽象的模块要依赖具体实现相关的模块,底层模块的具体实现发生变动时 将会严重影响高层抽象的模块,显然这是结构化方法的一个"硬伤"。面向对象方法的依赖关系刚好相反,具体实现类依赖于抽象类和接口。
业务逻辑层不依赖于底层的数据访问层,其依赖于工厂抽象。工厂抽象只抽象为初始化和获取网格对象,不依赖于任何细节,比如计算特殊值的方法啦什么的。
五、接口分离原则(ISP)
采 用多个与特定客户类有关的接口比采用一个通用的涵盖多个业务方法的接口要好。 ISP原则是另外一个支持诸如COM等组件化的使能技术。缺少 ISP,组件、类的可用性和移植性将大打折扣。这个原则的本质相当简单。如果你拥有一个针对多个客户的类,为每一个客户创建特定业务接口,然后使该客户类 继承多个特定业务接口将比直接加载客户所需所有方法有效。
这里只有一个接口,忽略。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述