学习如何编写好软件






3.93/5 (27投票s)
本文将逐步指导您每次编写出色的软件。
引言
我已经为不同的客户开发软件很长时间了。我总是尽力编写出色的软件,满足所有客户需求,最重要的是,我很高兴我为客户交付了一份出色的工作。我总是努力定义什么是优秀的软件,以及如何每次都能交付出色的工作。但每个客户面临的挑战和客户需求都不同。期望水平也因客户而异。
弄清楚项目从何开始,以及如何交付令客户、开发人员和架构师都满意的软件,绝非易事。我们对优秀软件和编程进行了大量研究,但在编程时,大多数开发人员和架构师都忽略了这些规则,这给他们带来了麻烦。
在本文中,我们将研究什么是优秀软件,以及如何应用面向对象分析和设计规则来编写优秀软件。我们已经学习了封装、委托、解耦和其他面向对象实践,但我们如何在实际编程中使用它们呢?
什么是优秀软件?
这是一个模糊的术语,因为不同的人对它有不同的定义和含义。
客户说优秀的软件能做客户想要的事情,如果他以新的方式使用它,它仍然应该有效,不会崩溃并给出意想不到的结果。大多数客户不关心开发软件的架构和其他最佳编程实践。相反,他更关心软件应该给出的结果。崩溃的应用程序和奇怪的结果会激怒客户,并影响他对程序员的信心。
面向对象程序员说优秀的软件是用面向对象的方式编写的代码,它不重复代码。每个对象都负责处理自己的行为。在您的应用程序上应用适当的面向对象规则,使其坚固且易于扩展。您的结构应该更灵活,以处理新的更改。
架构师说优秀的软件是您使用适当的分层和设计模式的软件。您的软件包含可重用组件,因为重复解决相同的问题并不好。您的应用程序开放扩展,但封闭修改。组件应该松散耦合。
质量保证团队说没有软件是优秀的软件,因为如果他们认为软件是优秀的,那他们怎么能从中发现错误呢?这是我同事给我的一个有趣而有趣的答案。但客户和测试人员的想法几乎相同。优秀的软件能给出预期的结果,不会崩溃,如果他们想以不同的方式使用它,会给出正确的结果。最重要的是,它满足所有客户需求。
项目经理、董事总经理、客户经理和首席执行官等其他人的想法几乎相同,这些想法已包含在上述定义中。
我们从不同的人那里听说过优秀软件。当我们交付软件时,如何才能让所有这些人满意呢?在这个阶段答案并不容易,但您将在本文中学习优秀软件,这将增强您交付优秀软件的智力。
手机店项目
我的一个朋友开了一家手机店,他卖新旧手机。他以前手动管理纸质库存。有一天,他决定放弃纸质库存系统,想为他的商店使用一个自动化系统来管理库存和搜索功能,以帮助客户选择他们梦想的手机。他正在转向一个新系统,因为他想增加销售额并管理库存。他把这个项目交给了一家知名公司来构建自动化系统。几个月后,这家编程公司给了他一个他们为他构建的新电脑系统。让我们看看这家公司是如何为手机店设计新系统的。
手机店新应用
这家编程公司完全用一个新系统取代了手写笔记,以帮助客户搜索手机。他们提供了这个UML类图来展示他们所做的工作。
他们创建了两个类:`Mobile` 包含设备信息,`Inventory` 包含所有设备以及添加、搜索和获取手机的方法。
让我们看看编程公司编写的实际代码。
手机代码
`Mobile` 类,正如UML图中所示,包含手机设备的所有属性,它有一个构造函数,在创建对象时获取所有数据,并且它只公开属性的getter方法。
public class Mobile
{
private string _iemi;
private double _price;
private string _brand;
private string _model;
private string _color;
public Mobile(string ieme, double price, string brand, string model, string color)
{
_iemi = ieme;
_price = price;
_brand = brand;
_model = model;
_color = color;
}
public string IEMI{ get{ return _iemi; } }
public double Price{ get{ return _price; } }
public string Brand{ get{ return _brand; } }
public string Model{ get{ return _model; } }
public string Color{ get { return _color; } }
}
库存代码
Inventory
类包含所有手机的列表,并公开添加手机、获取手机和搜索手机的方法。
public class Inventory
{
List<mobile> _mobiles = new List<mobile>();
public void AddMobile(string ieme, double price, string brand, string model, string color)
{
var mobile = new Mobile(ieme, price, brand, model, color);
_mobiles.Add(mobile);
}
public Mobile GetMobile(string ieme)
{
foreach (var mobile in _mobiles)
{
if (mobile.IEMI == ieme)
{
return mobile;
}
}
return null;
}
public Mobile SearchMobile(Mobile mobile)
{
foreach (var m in _mobiles)
{
if (!string.IsNullOrEmpty(mobile.Brand) && mobile.Brand != m.Brand)
continue;
if (!string.IsNullOrEmpty(mobile.Model) && mobile.Model != m.Model)
continue;
if (!string.IsNullOrEmpty(mobile.Color) && mobile.Color != m.Color)
continue;
return m;
}
return null;
}
}
SearchMobile
方法将在本文后面的进一步讨论中使用,它是手机店最重要的功能,因为客户应用程序将使用它为客户查找手机。客户可以根据品牌、型号和颜色进行搜索。此方法包含比较搜索中第一个合适设备的逻辑。
软件存在搜索问题
当手机店开始使用新的电脑系统搜索手机时,有时搜索系统没有返回任何结果,但店主知道他库存中有设备。因此,由于系统奇怪的结果,他开始失去客户。
编程公司交付的软件未能满足客户,客户带着投诉找他们。因此,编程公司交付的软件并非优秀软件。
我们如何才能每次都交付优秀的软件?
问题在于我们如何才能每次都编写出色的软件。有没有一套规则可以遵循,从而编写出色的软件?再次阅读客户、程序员和架构师的定义,并尝试定义一套优秀软件的规则。优秀的软件必须满足客户,它必须应用面向对象编程原则,并且应该松散耦合。我们可以将优秀软件的定义分解为三个步骤。
考虑到不同人的观点,我们总结出这三个主要步骤来编写优秀的软件。现在我们将把这三个步骤应用到手机店项目中,以编写优秀的软件。
软件符合客户要求
编程公司交付的项目未能满足客户。他们坐在一起,开始寻找搜索功能损坏的原因。当他们通过编写测试工具分析和测试应用程序时,他们发现问题出在字母大小写上。搜索模块以区分大小写的方式匹配产品。例如,手机店的库存中有“Nokia”,而客户使用字符串“nokia”搜索设备。
他们开始讨论不同的解决方案。一位开发人员说只需修复问题并再次交付,暂时不用担心设计。另一位开发人员说,我们不能交付再次导致其他问题的项目,所以我们必须足够聪明,以更好的设计修复问题。
他们都同意 “不要制造问题来解决问题”。这是处理事情的明智和正确方法。他们最终确定了以下解决方案。
- 每次字符串比较都应使用
ToLower
,以避免下次出现问题。 - 对固定范围的值使用常量或枚举,以避免拼写错误和大小写问题。
软件的第一次改进
他们在软件中进行的第一次改进是为了避免字符串比较问题,即在比较语句中添加 ToLower()
并使用枚举表示颜色。
新的搜索方法包含小写字符串比较,并在需要时使用枚举。
public Mobile SearchMobile(Mobile mobile)
{
foreach (var m in _mobiles)
{
if (!string.IsNullOrEmpty(mobile.Brand) && mobile.Brand.ToLower() != m.Brand.ToLower())
continue;
if (!string.IsNullOrEmpty(mobile.Model) && mobile.Model.ToLower() != m.Model.ToLower())
continue;
if (mobile.Color != MobileColor.None && mobile.Color != m.Color)
continue;
return m;
}
return null;
}
新搜索功能稳定,现在可以正常运行了。客户现在很满意,所以在这个阶段我们已经完成了第一个任务 “软件符合客户要求”。几天后,手机店想到,应用程序应该返回所有符合搜索条件的手机。通过这项新功能,他可以增加销量,并让客户从多个设备中选择一款合适的设备。
他提出了一个新的应用程序需求。搜索工具必须返回所有符合搜索条件的手机。编程公司很容易地将该逻辑加入其中。
public List<mobile> SearchMobile(Mobile mobile)
{
List<mobile> toReturn = new List<mobile>();
foreach (var m in _mobiles)
{
if (!string.IsNullOrEmpty(mobile.Brand) && mobile.Brand.ToLower() != m.Brand.ToLower())
continue;
if (!string.IsNullOrEmpty(mobile.Model) && mobile.Model.ToLower() != m.Model.ToLower())
continue;
if (mobile.Color != MobileColor.None && mobile.Color != m.Color)
continue;
toReturn.Add(m);
}
return toReturn;
}
编程公司以非常小的努力交付了软件,这让客户非常满意,因为它完全按照他想要的方式运行。它增加了销售额,他现在对软件很满意。
现在软件的表现正如预期。所以我们的第一个任务已经完成。
尝试应用一些面向对象原则
客户满意,软件运行正常。现在,让我们尝试应用一些面向对象原则,以增加一些灵活性来处理新的变化。当编程公司开始研究时,他们注意到搜索将Mobile
作为参数,但它不使用IEMI
和Price
来过滤记录。这意味着设计有问题,我们必须重新考虑以进行改进。
他们提出了新的解决方案。Mobile
对象并没有达到其名称所暗示的功能。它也没有代表一个单一的概念。让我们尝试删除不直接属于手机的额外属性。我们创建一个新类 MobileSpec
,并将所有与规格相关的属性添加到其中。系统的新UML图如下。
应用一些基本的面向对象概念后,代码现在清晰了许多,看起来也更灵活了。新的Mobile
类现在是这样的:
public class Mobile
{
private string _iemi;
private double _price;
private MobileSpec _mobileSpec;
public Mobile(string ieme, double price, string brand, string model, MobileColor color)
{
_iemi = ieme;
_price = price;
_mobileSpec = new MobileSpec( brand, model, color);
}
public string IEMI{ get{ return _iemi; } }
public double Price{ get{ return _price; } }
public MobileSpec Spec{ get{ return _mobileSpec; } }
}
搜索现在也更清晰了。
public List<mobile> SearchMobile(MobileSpec mobileSpec)
{
List<mobile> toReturn = new List<mobile>();
foreach (var m in _mobiles)
{
var specs = m.Spec;
if (!string.IsNullOrEmpty(mobileSpec.Brand) &&
mobileSpec.Brand.ToLower() != specs.Brand.ToLower())
continue;
if (!string.IsNullOrEmpty(mobileSpec.Model) &&
mobileSpec.Model.ToLower() != specs.Model.ToLower())
continue;
if (mobileSpec.Color != MobileColor.None && mobileSpec.Color != specs.Color)
continue;
toReturn.Add(m);
}
return toReturn;
}
现在只需将新代码与旧代码进行比较。您会注意到,应用了一些基本的OOP规则后,新代码设计良好且更具灵活性。
可重用和松耦合组件
几个月后,客户回到这家编程公司,要求他们为手机添加一些额外的字段。因为现在市面上有各种各样的智能手机,它们有内存、屏幕分辨率和操作系统。
编程公司接受了修改,并开始考虑在 MobileSpec
文件中添加这些新字段。但他们很快注意到,需要在三个不同区域的类中进行更改。
- 在 MobileSpec 文件中添加字段。
- 在
Mobile
类的构造函数上添加参数,并在代码中将这些参数传递给MobileSpec
。 - 在
Inventory
类的Search
方法中,添加新字段进行压缩。并更改Inventory
类的Add
方法。
这算是一个好的设计吗?
优秀的软件拥有可重用组件,并且开放扩展,封闭修改。但现有的手机项目结构中不具备这两个特性。这意味着每当我们在 MobileSpec
类中添加新的规格时,我们需要再次更改所有其他类。这真是一团糟。
我们如何使其松耦合?每个类都应该自己完成自己的职责,而其他类不应代表任何类执行工作。观察Inventory
类中Search
方法的代码。它在其中比较了MobileSpec
对象。这意味着每当我们更改MobileSpec
类时,都需要更改Search
方法。所以我们的设计是紧耦合的。
他们做的第一件事是,从Inventory
类中删除MobileSpecs
比较,并在MobileSpec
类中编写一个新的Equal
方法,并将所有比较逻辑放入该方法中。
public class MobileSpec
{
public bool IsEqual(MobileSpec mobileSpec)
{
if (!string.IsNullOrEmpty(mobileSpec.Brand) && mobileSpec.Brand.ToLower() != Brand.ToLower())
return false;
if (!string.IsNullOrEmpty(mobileSpec.Model) && mobileSpec.Model.ToLower() != Model.ToLower())
return false;
if (mobileSpec.Color != MobileColor.None && mobileSpec.Color != Color)
return false;
return true;
}
}
Inventory
类中的Search
方法现在干净多了,它没有做任何属于MobileSpec
类的额外工作。MobileSpec
类的更改不会影响Inventory
类的Search
方法。
public class Inventory
{
.
.
.
public List<mobile> SearchMobile(MobileSpec mobileSpec)
{
List<mobile> toReturn = new List<mobile>();
foreach (var m in _mobiles)
{
if (m.Spec.IsEqual(mobileSpec))
{
toReturn.Add(m);
}
}
return toReturn;
}
}
现在他们认为没有必要在Mobile
类的构造函数中为MobileSpec
类单独设置参数。只需将MobileSpec
作为参数,即可消除依赖关系,使设计尽可能地松散耦合。
public class Mobile
{
private string _iemi;
private double _price;
private MobileSpec _mobileSpec;
public Mobile(string ieme, double price,MobileSpec spec)
{
_iemi = ieme;
_price = price;
_mobileSpec = spec;
}
.
.
.
.
}
有了这个改变,Inventory
类的AddMobile
方法也将随之改变。在这里也需要将MobileSpec
作为参数传递。
public void AddMobile(string ieme, double price,MobileSpec spec)
{
var mobile = new Mobile(ieme, price, spec);
_mobiles.Add(mobile);
}
现在,如果你查看解决方案,设计是灵活和可重用的。你可以独立使用MobileSpec
类,它封装了所有属性和方法。该设计具有松散耦合的组件,并且Mobile
类的更改不会影响其他类。
现在让我们在 MobileSpec
类中添加新字段。您会注意到,其他类将不需要任何更改。您只需要在 IsEqual
方法中添加新字段、属性和比较语句。
public class MobileSpec
{
private string _brand;
private string _model;
private MobileColor _color;
private int? _internalMemory;
private string _operatingSystem;
public MobileSpec(string brand, string model, MobileColor color)
: this(brand, model, color, null, "")
{
}
public MobileSpec(string brand, string model, MobileColor color,
int? internalMemory, string operatingSystem)
{
_brand = brand;
_model = model;
_color = color;
_internalMemory = internalMemory;
_operatingSystem = operatingSystem;
}
public string Brand{get{ return _brand;}}
public string Model{get{ return _model;}}
public MobileColor Color{get{ return _color;}}
public int? InternalMemory{get{ return _internalMemory;}}
public string OperatingSystem{get{ return _operatingSystem;}}
public bool IsEqual(MobileSpec mobileSpec)
{
if (!string.IsNullOrEmpty(mobileSpec.Brand) && mobileSpec.Brand.ToLower() != Brand.ToLower())
return false;
if (!string.IsNullOrEmpty(mobileSpec.Model) && mobileSpec.Model.ToLower() != Model.ToLower())
return false;
if (mobileSpec.Color != MobileColor.None && mobileSpec.Color != Color)
return false;
if (!string.IsNullOrEmpty(mobileSpec.OperatingSystem) &&
mobileSpec.OperatingSystem.ToLower() != OperatingSystem.ToLower())
return false;
if (mobileSpec.InternalMemory.HasValue && mobileSpec.InternalMemory != InternalMemory)
return false;
return true;
}
}
恭喜!你已经编写出了优秀的软件
这些是一些基本规则,遵循它们,你就能每次都写出优秀的软件。只要养成习惯,享受你的工作吧。