根据他的轶事,您的朋友似乎正面临着许多不利因素。那是不幸的,并且可能是一个非常艰苦的工作环境。尽管有困难,他仍在使用模式使自己的生活更轻松的正确道路上,而离开了那条道路实在可耻。意大利面条代码是最终结果。
由于存在两个不同的问题领域,即技术问题和人际问题,因此我将分别解决每个问题。
人际交往
您的朋友正面临着快速变化的需求而苦恼,这正如何影响他编写可维护代码的能力。我首先要说的是,每天如此长的时间内每天两次更改需求是一个更大的问题,并且具有不切实际的隐含期望。需求变化的速度超过了代码的变化速度。我们不能期望代码或程序员能跟上潮流。这种快速的变化步伐是更高层次上所需产品概念不完整的征兆。这是个问题。如果他们不知道自己真正想要的是什么,他们将浪费大量的时间和金钱,永远无法得到它。
为更改设置边界可能会很好。每两周将变更分组在一起,然后在实施期间将其冻结两周。在接下来的两个星期内建立新清单。我感到其中一些变化重叠或矛盾(例如,在两个选项之间来回摆动)。当变化迅速而激烈时,它们都是当务之急。如果您让他们积累到列表中,则可以与他们一起组织和确定最重要的事情,以最大程度地提高工作量和生产率。他们可能会发现他们的某些更改很愚蠢或不太重要,这给您的朋友提供了喘息的空间。
但是,这些问题不应阻止您编写好的代码。错误的代码会导致更严重的问题。从一种解决方案重构到另一种解决方案可能需要花费一些时间,但是有可能这样的事实表明,通过模式和原则,良好的编码实践会带来好处。
在频繁变化的环境中,技术债务将在某个时候到期。与其付款,而不是先等它变得太大而无法克服,不如先付钱。如果模式不再有用,则将其重构,但不要再回到牛仔编码方式。
技术
您的朋友似乎对基本的设计模式有很好的了解。空对象是解决他所面临问题的好方法。实际上,这仍然是一个好方法。他似乎面临挑战的地方是理解模式背后的原理,原因。否则,我不相信他会放弃他的方法。
(下面是原始问题中并未要求的一组技术解决方案,但这些技术解决方案说明了我们如何能够遵循用于说明目的的模式。)
空对象背后的原理是封装变化的思想。我们隐藏了哪些更改,因此我们不必在其他任何地方都应对它。在这里,空对象将product.Price
实例中的方差封装起来(我将其称为Price
对象,空对象的价格为NullPrice
)。Price
是领域对象,一种业务概念。有时,根据我们的业务逻辑,我们还不知道价格。有时候是这样的。空对象的完美用例。Price
有一个ToString
输出价格的方法,如果不知道,则输出空字符串(或NullPrice#ToString
返回一个空字符串)。这是合理的行为。然后需求就改变了。
我们必须将a输出null
到API视图或将其他字符串输出到管理者视图。这如何影响我们的业务逻辑?好吧,事实并非如此。在以上声明中,我两次使用了“视图”一词。这个词可能没有明确说出来,但是我们必须训练自己以听取需求中隐藏的词。那么,“视图”为何如此重要?因为它告诉我们真正必须在哪里进行更改:在我们看来。
撇开:这里是否使用MVC框架无关紧要。虽然MVC对“视图”有非常特定的含义,但我将其用于一段演示代码的更一般(也许更适用)的含义。
因此,我们确实需要在视图中修复此问题。我们该怎么做?最简单的方法是if
声明。我知道null对象旨在摆脱所有ifs,但我们必须务实。我们可以检查字符串是否为空,然后切换:
if(product.Price.ToString("c").Length == 0) { // one way of many
writer.write("Price unspecified [Change]");
} else {
writer.write(product.Price.ToString("c"));
}
这如何影响封装?这里最重要的部分是视图逻辑封装在视图中。这样,我们可以使业务逻辑/域对象与视图逻辑的变化完全隔离。很难看,但是可以用。但是,这不是唯一的选择。
我们可以说我们的业务逻辑发生了一点变化,因为如果没有设置价格,我们想输出默认字符串。我们可以对我们的Price#ToString
方法进行一些细微的调整(实际上是创建一个重载的方法)。我们可以接受默认的返回值,如果未设置价格,则返回该值:
class Price {
...
// A new ToString method
public string ToString(string c, string default) {
return ToString(c);
}
...
}
class NullPrice {
...
// A new ToString method
public string ToString(string c, string default) {
return default;
}
...
}
现在,我们的视图代码变为:
writer.write(product.Price.ToString("c", "Price unspecified [Change]"));
条件不存在了。但是,这样做太多可能会使特殊情况下的方法激增到您的域对象中,因此,只有在只有少数情况下才有意义。
我们可以改用创建一个返回布尔值的IsSet
方法Price
:
class Price {
...
public bool IsSet() {
return return true;
}
...
}
class NullPrice {
...
public bool IsSet() {
return false;
}
...
}
查看逻辑:
if(product.Price.IsSet()) {
writer.write(product.Price.ToString("c"));
} else {
writer.write("Price unspecified [Change]");
}
我们在视图中看到了有条件的返回,但是对于业务逻辑来说,确定价格是否已确定,情况就更强了。Price#IsSet
现在我们可以在其他地方使用它了。
最后,我们可以封装将价格完全呈现在视图助手中的想法。这将隐藏条件,同时尽可能保留域对象:
class PriceStringHelper {
public PriceStringHelper() {}
public string PriceToString(Price price, string default) {
if(price.IsSet()) { // or use string length to not change the Price class at all
return price.ToString("c");
} else {
return default;
}
}
}
查看逻辑:
writer.write(new PriceStringHelper().PriceToString(product.Price, "Price unspecified [Change]"));
进行更改的方法还有很多(我们可以归纳PriceStringHelper
为一个对象,如果字符串为空,则返回默认值),但是这些方法可以快速(大部分)保留模式和原理,例如以及进行此类更改的实用方面。