面试不问技术
设计模式为我们提供了指导,以帮助我们实施清晰,简洁的可维护代码。 在实施面向对象的设计时,鸭子输入和“告诉不问”模式会并存,以产生易于组合和维护的代码。 功能性编程和通用接口技术(如Monads)也由设计实现了Tell Do n't Ask。
在这里,我们将着重于通过面向对象的设计来实现易于阅读,易于更新的代码库,特别是使用“告诉不要问”原理。
代码设计怎么可能出错
除非您从设计错误中学习,否则使用设计模式的正确方法往往不会完全陷入。
例如,我决定构建一个命令行闪存卡游戏来学习日语字符的翻译。 因此,它的开始非常简单-我在YAML文件中提供了日语字符的映射。 将其加载为原始哈希,然后继续在游戏循环逻辑中对其进行引用。 由于这很简单,因此我继续将游戏系统抽象为带有ERB模板的MVC(模型/视图/控制器)布局,并为所有菜单和游戏显示单词添加了I18n支持。 在这一点上,我为自己所取得的成就感到自豪。
但是后来我想到了一个主意。 可以直接从键盘输入日语字符,那么为什么不随翻译游戏一起添加打字游戏呢? 仅通过添加有关YAML文件可用的游戏模式的详细信息,这对于当前代码库来说似乎很容易。 因此,我在字符映射中为翻译和键入模式进行了编写。
但是,然后要使用现有的代码库,我必须告诉代码库中的每个对象有关这两种状态以及如何分别处理每个状态的信息,并且我需要保持当前模式的状态。
当我完成并运行良好时,我仍然为它感到自豪,但是一门课对我很突出,因为它完全违反了“告诉不要问”模式。 其中的每个方法都是一个询问当前状态或行为的外部查询方法。 这些方法来自该类,是不执行操作的示例:
def mapped_as
case @mapping.first
when :k
@mapped_as.keys.first
else
@mapped_as.values.first
end
end
def match? input, comp_bitz
case comp_bitz.mapping
when :k
comp_bitz.value == comp_bitz.collection[input]
when :v
comp_bitz.value == input
end
end
def choose_display key, value
case @mapping.last
when :k
key
when :v
value
end
end
def choose_expected key, value
case @mapping.first
when :k
key
when :v
value
end
end
这是不好的设计,导致代码难以维护。 这不是鸭打字也不是告诉不要问。 整个过程中,代码基础都依赖于此,我知道我必须重构它……所以一年后,我做到了。
重构告诉不问
在应对重构项目的挑战时,我首先查看了我知道的问题区域,这就是我上面演示的查询对象。 在检查了一段时间的代码库后,我意识到它不可能一步一步地修改,而是需要大量重写,然后我将告诉您原因。
事实证明,问题不仅仅是拥有系统所依赖的查询对象。 它的根本问题是缺乏适当的抽象,这在编写“热爱原始类型”的代码时很常见。 通过导入闪存卡并将其直接用作整个应用程序中的哈希(原始类型),与之兼容的所有内容都必须专门编写代码来理解哈希的问题以及哈希结构背后的含义。
当您无法将数据抽象为表示性对象时,您将陷入在该对象的每次交互中实现抽象方法的麻烦,而不是仅以创建对象作为抽象而实现一次。 因此,我不得不对Indiana-Jones进行重构-我不得不在测试驱动的开发过程中对其进行重建,然后将代码交换到位,希望不要引起陷阱。
实施和测试的第一步是为单个闪存卡创建抽象。 在这种情况下,闪存卡是指脸上有日语字符且背面有翻译的卡片。 卡除了持有这两条信息外,没有其他责任。
当游戏最初仅被实现为翻译游戏时,感觉事物遵循了单一责任原则(每个类对象和方法仅负责一件事),因为只处理了一件事情。 但是,对系统进行根本性更改后,很明显,当前的设计并未遵循SRP,因为许多功能需要处理行为的双重性。
从YAML文件加载数据后,立即创建闪存卡的抽象,仅使用其两个数据的方法,它遵循SRP,并且是从中实现鸭子类型“告诉不问”的核心组成部分。
在设计适当的类型以遵循这些设计模式时,我发现最好在纸上写出应用程序抽象的想法,以便您可以更清晰地鸟瞰。 编写一个应用程序也可以帮助设想其设计, 即使它是一团糟 ,但如果您可以先仔细考虑一下设计,就可以跳过一团糟,那么重构时将更容易。
在设计过程中,我们现在有了闪存卡 。 他们的集合就是一套卡 。 从那里,我们可以拿起纸牌并将其设置为两种游戏模式之一 ,即打字练习或翻译 。 这两种类型的游戏模式将是同一只鸭子型游戏。
现在,当我们通过用户界面传递任一游戏的这种鸭子类型时,它都不会在乎哪个游戏。 它们都具有在每个对象上定义的相同方法,因此没有任何外部查询方法会询问任何问题。 这是我们的“不要问”模式。
实施告诉不问
在这里,让我们看一看“不要问”实现。 与其编写代码来查询我们所处的游戏模式,不如直接告诉它该模式并通过返回游戏来产生我们想要的内容。
class CardSet
attr_reader :card_set
def initialize(card_hash)
@card_set = card_set_builder(card_hash)
end
def game(mode)
case mode
when :translate
Modes::Translate.new(self)
when :typing_practice
Modes::TypingPractice.new(self)
else
raise "Invalid Game Mode!"
end
end
# ...
end
在这里,我们有一个CardSet
类,该类初始化要持有的卡集,并且没有其他责任。 这里的game
方法是我们的讲述方法。 我们告诉它要使用哪种模式,它返回给我们我们要玩的游戏的鸭子类型。
Modes::TypingPractice
Modes::Translate
和Modes::TypingPractice
都被子类Modes::TypingPractice
Modes::Game
类,并且仅在不同之处定义(相同)方法。
class Translate < Game
def match? input
current.translation.any? {|value| value == input }
end
def mode
:translate
end
end
class TypingPractice < Game
def match? input
"#{current}" == input
end
def mode
:typing_practice
end
end</code>
现在,当我们的游戏将mode
方法发送给显示时,我们就有了提供的名称以及match?
方法是区分这两种游戏的特定逻辑。 此行为仅特定于游戏,因此属于此逻辑。 它还在处理每个游戏之间提供了一种可靠的调用机制,这是鸭子类型的全部目的。
现在您可能会注意到使用此方法是否match?
是一种在外部使用的查询方法,该方法违反了“告诉不问”模式。 这是有充分理由的。
模式是帮助您的一般规则,但是在编程中总是有例外。 评分不属于我们的游戏鸭子类型的责任,并且属于我们用户界面的行为范围。 同样,评分是通过原始类型(例如Boolean和integer)实现的,因此我们已经退出了该游戏机制的鸭子输入角色。 此代码区域对系统代码的未来更改影响很小,并且是使用原始类型的可接受且最佳的时间。
摘要
完成重构后,我删除了大约600行代码,并添加了400行。代码库从难以理解和维护变为易于阅读,并易于使用新功能进行更新。 这就是当您遵循此设计原则时“告诉不要问”的功能。 它使您更容易理解代码,并为组合性提供了更好的保证。
每当您构建系统并从语言本身以外的其他来源引入数据时,强烈建议您立即将数据抽象为简单的抽象。 这将最大限度地提高您的灵活性。 创建仅遵循单一职责原则仅具有一个角色的对象。
最后,您可以使用“告诉不要问”原理,如上面的代码所示,通过告诉行为而不是查询行为,您可以让该方法将您当前对象的self
封装为duck类型,并在代码路径中继续前进。
在面向对象的设计中,将一个对象包装在另一个对象中可以防止出现许多反模式,并有助于使示波器保持纯净且不受干扰。 这正是我们想要的。
翻译自: https://www.javacodegeeks.com/2018/04/refactoring-for-the-tell-dont-ask-pattern.html
面试不问技术