我遇到的OOP原则之一是:-封装变化的内容。
我了解该词组的字面意思是什么,即隐藏各种内容。但是,我不知道它将如何对更好的设计做出贡献。有人可以用一个很好的例子来解释吗?
I don't know how exactly would it contribute to a better design
封装细节是关于“模型”与实现细节之间的松耦合。与实施细节的“模型”联系越少,解决方案就越灵活。而且它使演化它变得更加容易。“从细节中提取自己”。
我遇到的OOP原则之一是:-封装变化的内容。
我了解该词组的字面意思是什么,即隐藏各种内容。但是,我不知道它将如何对更好的设计做出贡献。有人可以用一个很好的例子来解释吗?
I don't know how exactly would it contribute to a better design
封装细节是关于“模型”与实现细节之间的松耦合。与实施细节的“模型”联系越少,解决方案就越灵活。而且它使演化它变得更加容易。“从细节中提取自己”。
Answers:
您可以编写如下代码:
if (pet.type() == dog) {
pet.bark();
} else if (pet.type() == cat) {
pet.meow();
} else if (pet.type() == duck) {
pet.quack()
}
或者您可以编写如下代码:
pet.speak();
如果封装了不同的内容,那么您不必担心。您只需要担心自己需要什么,无论使用什么,都可以根据变化情况来确定实际需要做的事情。
封装变化的内容,而您不必在乎分散代码的范围。您只需将宠物设置为某种会说话的类型,然后您就可以忘记将哪种类型当作宠物对待。您不必询问哪种类型。
您可能会认为类型是封装的,因为需要使用吸气剂才能访问它。我不。吸气剂并没有真正封装。当有人破坏您的封装时,它们只是争吵。它们是一个很好的装饰器,如面向方面的挂钩,通常用作调试代码。无论如何切片,您仍然可以公开类型。
您可能会看这个示例,并认为我正在将多态性和封装混为一谈。我不是。我将“变化”和“细节”混为一谈。
您的宠物是狗,这是一个细节。一种可能会因您而异。一个可能不会。但是可以肯定的是,可能因人而异。除非我们相信该软件仅会被爱狗的人使用,否则将狗视为一个细节并将其封装是明智的。这样,当我们与“鹦鹉就是我们”合并时,系统的某些部分将非常高兴地意识到狗的存在,并且不会受到影响。
与其余代码分离,分离和隐藏细节。不要让细节知识在您的系统中传播,您将遵循“封装变化的内容”。
这里的“变化”是指“由于需求变化而可能随时间变化”。这是一个核心设计原则:分离并隔离将来可能需要单独更改的代码或数据。如果单个需求发生更改,则理想情况下只应要求我们在单个位置更改相关代码。但是,如果代码库的设计不正确,即高度互连,并且需求的逻辑分散在许多地方,则更改将很困难,并且极有可能引起意想不到的后果。
假设您有一个在很多地方都使用营业税计算的应用程序。如果营业税税率发生变化,您希望什么:
在应用程序中计算营业税的任何地方,营业税率都是硬编码的文字。
销售税率是一个全局常数,在计算销售税的应用程序中的任何地方都将使用它。
有一个称为的方法calculateSalesTax(product)
,这是唯一使用营业税率的地方。
在配置文件或数据库字段中指定营业税率。
由于营业税率可能会因与其他要求无关的政治决定而发生变化,因此我们更希望将其隔离在配置中,因此可以在不影响任何代码的情况下进行更改。但是也可以想象,计算营业税的逻辑可能会发生变化,例如,不同产品的税率不同,因此我们也希望封装计算逻辑。全局常数可能看起来是个好主意,但实际上却是个坏主意,因为它可能会鼓励在程序的不同位置而不是单个位置使用销售税。
现在考虑另一个常数Pi,它在代码中的很多地方也使用。相同的设计原理是否成立?不,因为Pi不会改变。将其提取到配置文件或数据库字段中只会带来不必要的复杂性(其他所有条件都相同,我们更喜欢最简单的代码)。使其成为全局常量而不是在多个位置对其进行硬编码确实很有意义,以避免出现不一致并提高可读性。
关键是,如果仅查看程序现在的工作方式,营业税率和Pi相等,都是常数。只有当我们考虑将来可能发生的变化时,我们才意识到我们必须在设计中以不同的方式对待它们。
这个原则实际上是很深的,因为它意味着您不仅要看今天的代码库应该做的事情,还要考虑可能导致其改变的外力,甚至理解需求背后的不同利益相关者。
当前的两个答案似乎都只是部分达到目标,它们集中在使核心思想模糊的例子上。通常,这也不是(唯一)OOP原则,而是软件设计原则。
这个短语中“变化的”是代码。Christophe指出这一点通常是可能会有所不同的,这就是您经常预料到的。目的是保护自己免受将来代码中的更改的影响。这与针对接口进行编程密切相关。但是,Christophe将其限于“实施细节”是不正确的。实际上,此建议的价值通常是由于需求的变化。
这仅与封装状态间接相关,这是我相信David Arno所想到的。该建议并不总是(但经常如此)建议封装状态,并且该建议也适用于不可变对象。实际上,仅命名常量是封装变化的(非常基本的)形式。
CandiedOrange明确地将“变化内容”与“细节”混为一谈。这只是部分正确。我同意,任何变化的代码在某种意义上都是“细节”,但是“细节”可能不会变化(除非您定义“细节”以使此重言式)。封装不变的细节可能是有原因的,但是这一原则不是一个。粗略地说,如果您非常有信心“狗”,“猫”和“鸭”将是您唯一需要处理的类型,那么该格言并不表明重构CandiedOrange会执行。
在不同的上下文中转换CandiedOrange的示例,假设我们有一个像C这样的过程语言。如果我有一些代码包含:
if (pet.type() == dog) {
pet.bark();
} else if (pet.type() == cat) {
pet.meow();
} else if (pet.type() == duck) {
pet.quack()
}
我可以合理地期望这段代码将来会更改。我可以通过定义一个新过程来“封装”它:
void speak(pet) {
if (pet.type() == dog) {
pet.bark();
} else if (pet.type() == cat) {
pet.meow();
} else if (pet.type() == duck) {
pet.quack()
}
}
并使用此新过程代替代码块(即“提取方法”重构)。此时,添加“ cow”类型或仅需要更新speak
过程的任何内容。当然,在OO语言中,您可以改为使用CandiedOrange的答案所暗示的动态调度。如果您pet
通过界面访问,这自然会发生。通过动态分派消除条件逻辑是一个正交的问题,这也是我进行此程序翻译的原因之一。我还想强调一点,这不需要OOP特有的功能。即使使用OO语言,封装变化也不一定意味着需要创建新的类或接口。
作为一个更具原型性的示例(更接近但不太面向对象),假设我们要从列表中删除重复项。假设我们通过遍历列表来跟踪我们到目前为止在另一个列表中看到的项目并删除我们看到的所有项目来实现它。合理地假设,至少出于性能方面的原因,我们可能希望更改对可见项目的跟踪方式。封装各种变化的格言表明,我们应该构建一个抽象的数据类型来表示可见项的集合。现在,针对此抽象Set数据类型定义了我们的算法,如果我们决定切换到二叉搜索树,则无需更改或维护我们的算法。在OO语言中,我们可以使用类或接口来捕获此抽象数据类型。用SML / O这样的语言
对于需求驱动的示例,假设您需要针对某些业务逻辑验证某个字段。尽管您现在可能有特定的要求,但是您强烈怀疑它们会不断发展。您可以将当前逻辑封装在其自己的过程/函数/规则/类中。
尽管这是一个正交的问题,而不是“封装变化的内容”的一部分,但通常很自然的方法就是抽象出已封装的逻辑,并对其进行参数化。这通常会导致代码更灵活,并允许通过替换为替代实现而不是修改封装的逻辑来更改逻辑。
“封装变化的内容”是指隐藏可能更改和发展的实现细节。
例:
例如,假设类Course
跟踪Students
可注册的内容。您可以使用实现它,LinkedList
并公开容器以允许对其进行迭代:
class Course {
public LinkedList<Student> Atendees;
public bool register (Student s);
...
}
但这不是一个好主意:
如果您封装了变化的内容(或者说可能变化的内容),那么您就可以保持使用代码和封装的类自行发展的自由。这就是为什么它是OOP中的重要原则。
补充阅读: