如何编写不算差的面向对象程序?

显然面向对象编程方法曾被当作银弹,但是无论如何作为技术架构师货计算机科学专业毕业工作的人来说,掌握OOP这一技能会受到用人单位的相当重视。

我看到很多计算机程序员自豪地宣称:耶,我以面对对象方式设计了代码,将我的数据成员定义为私有,只能通过公有方法访问,我还创建了基类和子类的继承关系,在基类中创建了虚拟方法,可以在派生类中共享。这就是面向对象吗?

首先,我们需要了解面向对象编程背后的目的,除了编写单个任务或使用一次就丢弃的小型程序以外,复杂的软件应用程序总是需要反复的修改,源代码需要定期更新,添加新功能和修复错误,面向对象的编程方法可以帮助你组织代码,使得修改更加容易,除此以外,面向对象编程允许将大而复杂问题分解为更小更可重用的模块,这些模块用于管理大型代码的一致性。

大多数程序员认为他们理解面向对象的基础,但是涉及到具体应用实际时,他们就抓狂了

面向对象编程方法的掌握需要有耐心,不断练习和对自己的一点信心,它不只是一种编程技术,更是一种思维方式的转变,需要大量实践和良好的设计。

如何练习?起初尝试面向对象编程看起来令人恐惧,注意不要过度复杂,不要试图一次性学习所有的知识,尝试先从采取一小步步骤开始,理解你的代码使用面向对象设计的动机,然后将其翻译成代码,你将开始看到可以被抽象化 模块化和其它改善你设计的代码慢慢出现在你的视野里。

大多数代码不是很完美,并且需要很长时间才能列出所有可能的缺陷,我将列出一些最常见和常见的错误,并给出一些建议来解决它们。

1.向不相关的类添加太多功能或职责:


class Account
{
public void Withdraw(decimal amount)
{
try
{
// logic to withdraw money
}
catch (Exception ex)
{
System.IO.File.WriteAllText(@
"c:\logs.txt",ex.ToString());
}
}
}

这个类承担了额外功能,将额外的异常日志添加文件这个职责增加了进来,将来如果需要更改日志记录机制,将需要更改Account这个类。

单一职责:
一个类只需要承担一个责任,并且只有一个原因来改变类。 这可以通过将日志记录活动移动到一个单独的类来解决,这个类只关注日志异常并将其写入日志文件。


public class Logger
{
public void Handle(string error)
{
System.IO.File.WriteAllText(@"c:\logs.txt", error);
}
}

现在Account类具有将日志记录活动委托给Logger类的灵活性,并且它只能关注与帐户相关的活动。


class Account
{
private Logger obj = new Logger();
public void Withdraw(decimal amount)
{
try
{
// logic to withdraw money
}
catch (Exception ex)
{
obj.Handle(ex.ToString());
}
}
}

考虑有一个新的要求来处理不同的帐户类型,如储蓄帐户和当前帐户。 为了适应这个,我添加一个属性到Account类名为“AccountType”。 根据帐户的类型,利率也不同。 我写一个方法来计算帐户类型的利息:



class Account
{
private int _accountType;

public int AccountType
{
get { return _accountType; }
set { _accountType = value; }
}
public decimal CalculateInterest(decimal amount)
{
if (_accountType == "SavingsAccount")
{
return (amount/100) * 3;
}
else if (_accountType ==
"CurrentAccount")
{
return (amount/100) * 2;
}
}
}

上面的代码的问题是“分支”。 如果将来有一个新的帐户类型,再次需要更改帐户类以满足,需要增加新的if-else条件代码。 请注意,我们在每次需求变化时都要更改帐户类。 这显然不是一个可扩展的代码。

对扩展开放,对修改关闭。

我们可以尝试通过添加新帐户类型,是不是可以添加一个从Account类继承的新类来扩展代码? 通过这样做,我们不仅抽象了Account类,而且允许它在其子类中共享共同的行为。


public class Account
{
public virtual decimal CalculateInterest(decimal amount)
{
// default 0.5% interest
return (amount/100) * 0.5;
}
}
public class SavingsAccount: Account
{
public override decimal CalculateInterest(decimal amount)
{
return (amount/100) * 3;
}
}
public class CurrentAccount: Account
{
public override decimal CalculateInterest(decimal amount)
{
return (amount/100) * 2;
}
}

如果需求再次变动:正在开发新的帐户类型称为在线帐户。 此帐户的利率为5%,拥有类似储蓄银行账户的所有利益。 但是,您不能从ATM提取现金。 你只能电汇。


public class Account
{
public virtual void Withdraw(decimal amount)
{
// base logic to withdraw money
}
public virtual decimal CalculateInterest(decimal amount)
{
// default 0.5% interest
return (amount/100) * 0.5;
}
}
public class SavingsAccount: Account
{
public override void Withdraw(decimal amount)
{
// logic to withdraw money
}
public override decimal CalculateInterest(decimal amount)
{
return (amount/100) * 3;
}
}
public class OnlineAccount: Account
{
public override void Withdraw(decimal amount)
{
throw new Exception(
"Not allowed");
}
public override decimal CalculateInterest(decimal amount)
{
return (amount/100) * 5;
}
}

现在,考虑我想关闭我的所有帐户。 所以,每个帐户的所有钱必须在完全关闭帐户之前取出。


public void CloseAllAccounts()
{
// Retrieves all accounts related to this customer
List<Account> accounts = customer.GetAllAccounts();
foreach(Account acc in accounts)
{
// Exception occurs here
acc.Withdraw(acc.TotalBalance);
}
}

根据继承层次结构,Account对象可以指向其任何一个子对象。 在编译期间没有注意到异常行为。 但是,在运行时,它抛出异常“不允许”。 我们从中推断出什么? 父对象无法无缝替换子对象。

让我们创建2个接口 - 一个处理兴趣(IProcessInterest)和另一个处理撤回(IWithdrawable)


interface IProcessInterest
{
decimal CalculateInterest(decimal amount);
}
interface IWithdrawable
{
void Withdraw(double amount);
}


OnlineAccount类将仅实现IProcessInterest,而Account类将实现IProcessInterest和IWithdrawable。


public class OnlineAccount: IProcessInterest
{
public decimal CalculateInterest(decimal amount)
{
return (amount/100) * 5;
}
}
public class Account: IProcessInterest, IWithdrawable
{
public virtual void Withdraw(decimal amount)
{
// base logic to withdraw money
}
public virtual decimal CalculateInterest(decimal amount)
{
// default 0.5% interest
return (amount/100) * 0.5;
}
}

现在,这看起来很干净。 我们可以创建一个IWithdrawable列表并向其中添加相关的类。 如果通过添加OnlineAccount到GetAllAccounts方法中的列表而产生错误,我们将得到一个编译时错误。


public void CloseAllAccounts()
{
// Retrieves all withdrawable accounts related to this customer
List<IWithdrawable> accounts = customer.GetAllAccounts();
foreach(Account acc in accounts)
{
acc.Withdraw(acc.TotalBalance);
}
}

▪ 假设我们的Account类又面对新的需求。 商业建议提出一个API,允许从不同的第三方银行的ATM提款。 我们暴露了一个Web服务,其它银行可以开始使用Web服务提款。 一切听起来不错,直到现在。 几个月后,该业务又出现了另一个要求,即一些其他银行也要求从其ATM中设置该帐户的提款限额。 没问题,提出更改请求 - 它可以很容易做到。


interface IWithdrawable
▪ {
void Withdraw(decimal amount);
void SetLimit(decimal limit);
▪ }



我们刚刚做的是很奇怪。 通过改变现有的接口,你正在添加一个破坏性改变,并打扰所有的原本很愉快消费我们网络服务的银行,只是因为添加撤回这个功能。 现在你迫使他们也使用新暴露的方法。 这种方式不好。

修复此问题的最佳方法是创建新接口,而不是修改现有接口。 当前接口IWithdrawable可以不动,增加一个新的接口 - 例如,IExtendedWithdrawable创建实现IWithdrawable。
interface IExtendedWithdrawable: IWithdrawable
{
void SetLimit(decimal limit);
}

因此,旧客户端将继续使用IWithdrawable,新客户端可以使用IExtendedWithdrawable。 简单,但有效!


▪ 让我们回到第一个问题,我们添加Logger类以委托来自Account类的日志记录的责任。 它将异常记录到文件。 有时为了方便访问,很容易通过电子邮件获取日志文件或将其与某些第三方日志查看器集成。 让我们实现:


interface ILogger
{
void Handle(string error);
}
public class FileLogger : ILogger
{
public void Handle(string error)
{
System.IO.File.WriteAllText(@"c:\logs.txt", error);
}
}
public class EmailLogger: ILogger
{
public void Handle(string error)
{
// send email
}
}
public class IntuitiveLogger: ILogger
{
public void Handle(string error)
{
// send to third party interface
}
}
class Account : IProcessInterest, IWithdrawable
{
private ILogger obj;
public void Withdraw(decimal amount)
{
try
{
// logic to withdraw money
}
catch (Exception ex)
{
if (ExType ==
"File")
{
obj = new FileLogger();
}
else if(ExType ==
"Email")
{
obj = new EmailLogger();
}
obj.Handle(ex.Message.ToString());
}
}
}

我们编写了一个优秀的可扩展代码 - ILogger,它作为其他日志记录机制的通用接口。 然而,我们再次违背了Account类单一职责的意愿,赋予其更多的责任,也就是它要决定创建的Logging类的哪个实例。 这不是Account类的工作。

解决方案是“反转依赖”到一些其他类,而不是Account类。 让我们通过Account类的构造函数注入依赖。 因此,我们正在把责任推诿给客户端,它来决定必须应用哪种日志记录机制。 客户端可能依赖于应用程序配置设置。 我们不必担心本文中的配置级别理解 - 至少。


class Account : IProcessInterest, IWithdrawable
{
private ILogger obj;
public Customer(ILogger logger)
{
obj = logger;
}
}
// Client side code
IWithdrawable account = new SavingsAccount(new FileLogger());

如果您密切关注,你可能有容易猜到的是,所有这些建议的修复都不过是应用面向对象设计的SOLID原则 。

How to write an object oriented program that doesn