四人帮模式列表–同样的模式可能会导致你在面试中失败。
生成模式
结构模式
行为模式
Soft & Games
解释器模式是指行为设计模式。此模式允许您通过使用 AST 树来实现您自己的编程语言,该树的顶点是实现 Interpret 方法的终端和非终端表达式,该方法提供了该语言的功能。
有什么区别?不同之处在于,解释在终端表达式上结束,但对于非终端表达式,它会在所有传入的顶点/参数上继续深入。如果 AST 树仅由非终结符表达式组成,那么应用程序将永远无法完成,因为任何过程都需要一定的有限性,这种有限性就是终端表达式,它们通常包含数据,例如字符串。
AST 树的示例如下:

Dcoetzee,CC0,来自维基共享资源
如您所见,终端表达式是常量和变量,其余的是非终端表达式。
解释器实现不包括解析输入到 AST 树中的语言字符串。实现终结符和非终结符表达式的类、在输入处使用 Context 参数的 Interpret 方法、创建表达式的 AST 树并在根表达式处运行 Interpret 方法就足够了。上下文可用于在运行时存储应用程序状态。
该模式涉及:
C# 中的客户端示例
static void Main(string[] args)
{
var context = new Context();
var initialProgram = new PerformExpression(
new IExpression[] {
new SetExpression("alpha", "1"),
new GetExpression("alpha"),
new PrintExpression(
new IExpression[] {
new ConstantExpression("Hello Interpreter Pattern")
}
)
}
);
System.Console.WriteLine(initialProgram.interpret(context));
}
}
C# 中的抽象表达式示例
{
String interpret(Context context);
}
C# 中的终端表达式示例(字符串常量)
{
private String constant;
public ConstantExpression(String constant) {
this.constant = constant;
}
override public String interpret(Context context) {
return constant;
}
}
C# 中非终结符表达式的示例(使用分隔符“;”开始并连接从属顶点的结果
{
public PerformExpression(IExpression[] leafs) : base(leafs) {
this.leafs = leafs;
}
override public String interpret(Context context) {
var output = "";
foreach (var leaf in leafs) {
output += leaf.interpret(context) + ";";
}
return output;
}
}
众所周知,所有图灵完备的语言都是等价的。是否可以将面向对象模式转移到函数式编程语言?
作为实验,我们采用一种名为 Elm 的 FP 网络语言。 Elm中没有类,但是有Records和Types,因此实现中涉及到以下记录和类型:
在 Elm 中实现对整个可能表达式集的解释的函数示例:
case input.expression of
Constant text ->
{
output = text,
context = input.context
}
Perform leafs ->
let inputs = List.map (\leaf -> { expressionLeaf = leaf, context = input.context } ) leafs in
let startLeaf = { expressionLeaf = (Node (Constant "")), context = { variables = Dict.empty } } in
let outputExpressionInput = List.foldl mergeContextsAndRunLeafs startLeaf inputs in
{
output = (runExpressionLeaf outputExpressionInput).output,
context = input.context
}
Print printExpression ->
run
{
expression = printExpression,
context = input.context
}
Set key value ->
let variables = Dict.insert key value input.context.variables in
{
output = "OK",
context = { variables = variables }
}
Get key ->
{
output = Maybe.withDefault ("No value for key: " ++ key) (Dict.get key input.context.variables),
context = input.context
}
解释器模式中不包含将源代码解析为 AST 树;有多种解析源代码的方法,稍后会详细介绍。
在Elm解释器的实现中,我在AST树中编写了一个简单的解析器,由两个函数组成——解析顶点,解析从属顶点。
parseLeafs state =
let tokensQueue = state.tokensQueue in
let popped = pop state.tokensQueue in
let tokensQueueTail = tail state.tokensQueue in
if popped == "Nothing" then
state
else if popped == "Perform(" then
{
tokensQueue = tokensQueue,
result = (state.result ++ [Node (parse tokensQueue)])
}
else if popped == ")" then
parseLeafs {
tokensQueue = tokensQueueTail,
result = state.result
}
else if popped == "Set" then
let key = pop tokensQueueTail in
let value = pop (tail tokensQueueTail) in
parseLeafs {
tokensQueue = tail (tail tokensQueueTail),
result = (state.result ++ [Node (Set key value)])
}
else if popped == "Get" then
let key = pop tokensQueueTail in
parseLeafs {
tokensQueue = tail tokensQueueTail,
result = (state.result ++ [Node (Get key)])
}
else
parseLeafs {
tokensQueue = tokensQueueTail,
result = (state.result ++ [Node (Constant popped)])
}
parse tokensQueue =
let popped = pop tokensQueue in
let tokensQueueTail = tail tokensQueue in
if popped == "Perform(" then
Perform (
parseLeafs {
tokensQueue = tokensQueueTail,
result = []
}
).result
else if popped == "Set" then
let key = pop tokensQueueTail in
let value = pop (tail tokensQueueTail) in
Set key value
else if popped == "Print" then
Print (parse tokensQueueTail)
else
Constant popped
https://gitlab.com/demensdeum /patterns/-/tree/master/interpreter/elm
https://gitlab.com/demensdeum/patterns/-/tree/master/interpreter/csharp
https://en.wikipedia.org/wiki/Interpreter_pattern
https://elm-lang.org/
https://docs.microsoft.com/en-us/dotnet/csharp/
在这篇文章中,我将阐述在开发、支持应用程序以及在团队开发环境中架构决策的重要性。
自助手术餐巾 Lucifer Gorgonzola 教授。鲁布·戈德堡
在我年轻的时候,我曾开发过一个出租车叫车应用程序。在该程序中,您可以选择上车点、下车点,计算行程成本、关税类型,甚至预订出租车。我在预发布的最后阶段收到了该应用程序;在添加了一些修复后,该应用程序已在 AppStore 中发布。到了那个阶段,整个团队就明白它的实现很差,没有使用设计模式,系统的所有组件都是紧密相连的,一般来说,可以把它写成一个大的连续类(上帝对象),一切都不会改变,所以阶级如何混淆了它们的责任界限,并且在它们的总体质量上,彼此重叠,形成死耦合。后来,管理层决定使用正确的架构从头开始编写应用程序,最终产品已实现并应用于数十个 B2B 客户。
但是,我会描述过去建筑中的一个奇怪的事件,我有时会在半夜惊醒一身冷汗,或者在中午突然想起并开始歇斯底里地大笑。问题是我第一次没能击中杆子上的家伙,这导致了大部分应用程序的失败,但首先是最重要的事情。
这是一个普通的工作日,其中一位客户收到了一项任务,需要稍微完善应用程序设计–将接送地址选择屏幕中心的图标向上移动几个像素很简单。好吧,在专业地估计了 10 分钟的任务后,我将图标向上提升了 20 像素,完全没有怀疑,我决定检查出租车订单。
什么?该应用不再显示订单按钮?这是怎么发生的?
我简直不敢相信自己的眼睛;在将图标提高 20 像素后,应用程序停止显示继续订购按钮。回滚更改后,我再次看到了按钮。这里出了点问题。在调试器中花了 20 分钟后,我有点厌倦了对重叠类的调用的意大利面条,但我发现*移动图像确实改变了应用程序的逻辑*
这一切都是关于中心的图标–一个人在一根杆子上,当移动卡片时,他跳起来以动画相机的运动,这个动画之后底部的按钮消失了。显然程序认为移动了20像素的人是在跳跃,所以根据其内部逻辑隐藏了确认按钮。
怎么会发生这种事?屏幕的 *状态 *是否真的取决于状态机的模式,而是在杆上的位置 *表示?
事实证明,每次绘制地图时,应用程序 *目视戳*在屏幕中间,检查那里有什么,如果杆子上有一个人,则说明地图移动动画已经结束,需要显示按钮。如果该人不在那里,则地图会移动,并且该按钮必须被隐藏。
在上面的例子中,一切都很好,首先,它是 Goldberg Machines(深奥的机器)的一个例子,其次,这是一个开发人员不愿意以某种方式与团队中其他开发人员交互的例子(尝试在没有我),第三,您可以根据 SOLID、模式(代码异味)、MVC 违规等列出所有问题。
尽量不要这样做,向各个可能的方向发展,帮助同事的工作。大家新年快乐)
https://ru.wikipedia.org/wiki/Goldberg_Machine
https://ru.wikipedia.org/wiki/SOLID
https://refactoring.guru/ru/refactoring/smells
https://ru.wikipedia.org/wiki/Model -视图控制器
https://refactoring.guru/ru/design-patterns/state

Façade 指的是结构设计模式。它提供了一个单一的接口,可以处理复杂的系统,允许客户端不需要这些系统的实现细节,从而简化他们的代码,并实现客户端和较低层系统之间的松耦合。 GoF 有一个很好的 Façade 示例:一种编程语言编译器,为追求不同目标的不同客户端提供通过单一编译器外观接口汇编代码的能力。
https://refactoring.guru/ru/design-patterns/facade
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

抽象工厂–提供了一个用于创建相关对象的接口,无需指定特定的类。
我真的很喜欢这个模式的替代名称– 套件(套件)
它与工厂方法非常相似,但是抽象工厂必须描述正在创建的对象之间的关系,否则它简直就是一个上帝对象 创造一切的反模式是随意的。
想象一下为眼镜开发一个 AR 框架;我们在屏幕上显示室内导航箭头、商店图标、有趣的地方、窗口和按钮,以及有关用户当前所在位置的信息。
同时,我们需要能够自定义 AR 环境控件的外观和行为。正是在这种情况下,您需要使用 Set 模式。
我们来写Abstract Factory和Abstract Products的接口–父协议、AR 环境元素:
protocol ARFactory {
func arrow() -> ARArrow
func icon() -> ARIcon
func button() -> ARButton
func window() -> ARWindow
}
protocol ARArrow {
var image: { get }
func handleSelection()
}
protocol ARIcon {
var image: { get }
var title: String
}
protocol ARButton {
var title: String
func handleSelection()
}
protocol ARWindow {
var title: String
var draw(canvas: Canvas)
}
现在套件开发人员需要基于抽象工厂接口实现具体工厂,并且他们必须一起实现所有元素;应用程序的其余部分将能够在不更改代码的情况下使用工厂。< /p>
https://refactoring.guru/ru/design-patterns /抽象工厂
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612
工厂方法模式指的是生成式设计模式。
该模式描述了用于创建特定类的对象的接口的创建。看起来很简单,对吧?
假设我们正在开发一个使用 AR 眼镜的框架,当将头倾斜到一侧时,可用应用程序的菜单应该出现在用户的眼前。应用程序将由第三方公司(我们框架的客户)开发。自然,我们不知道应该出现哪些应用程序、图标、名称,因此我们必须提供一个接口来实现应用程序的图标和相关信息。我们称之为产品:
protocol Product {
var name: String { get }
var image: Image { get }
var executablePath: String { get }
}
接下来,我们需要提供一个接口,以便我们的客户可以为其特定产品实施一系列应用程序的发布。带有名称的应用程序图标数组,我们已经在框架中绘制了它们。
让我们编写这个接口– Creator 接口包含返回产品数组的工厂方法。
protocol Creator {
func factoryMethod() -> [Product]
}
我们 AR 框架的第一个客户是 7B 公司。洪都拉斯领先的咖啡机软件供应商。他们希望销售增强现实眼镜,能够冲泡咖啡、检查水/咖啡豆是否已满,并使用室内地图模式显示前往最近咖啡机的路线。
他们负责软件的开发;我们只需提供有关Creator和Product界面的文档,以便正确显示应用程序列表及其进一步内容发射。
传输文档后,7B公司利用Creator接口,实现了Specific Creator……返回应用程序图标数组的类。图标应用程序本身是实现Product接口的特定产品类。
特定产品的示例代码:
class CoffeeMachineLocator: implements Product {
let name = “7B Coffee Machine Locator v.3000”
let image = Image.atPath(“images/locator.tga”)
let executablePath = “CoffeeMachineLocator.wasm”
}
class iPuchinno: implements Product {
let name = “iPuchinno 1.0.3”
let image = Image.atPath(“images/puchino.pvrtc”)
let executablePath = “neutron/ipuchBugFixFinalNoFreezeFixAlpha4.js”
}
类Concrete Creator,给出两个应用程序的数组:
class 7BAppsCreator: implements Creator {
func factoryMethod() -> [Product] {
return [CoffeeMachineLocator(), iPuchinno()]
}
}
此后,7B公司编译了Concrete Products、Concrete Creator库,并将其与我们的框架相结合,开始为其咖啡机销售AR眼镜,我们不需要添加。
https://refactoring.guru/ru/design-patterns/command
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612
命令模式是指行为设计模式。
这是我坚持了最长的时间的模式,它是如此简单,却又非常复杂。但就我个人而言,我发现自学的美妙之处在于你有足够的时间从各个角度研究某个主题。
因此,GoF 中的适用性描述得非常简洁明了:
将请求封装为对象,允许您使用不同的请求参数化客户端、使用队列、记录请求以及执行取消操作。
现在让我们根据描述实现该命令的简单版本:
string fakeTrumpsRequest = “SELECT * from Users where name beginsWith DonaldTrump”
我们将请求封装在一个字符串类对象中,它可以用于配置客户端、向队列添加命令、记录、取消(使用“快照”模式)
在我看来,这足以执行 SQL 查询等操作,但是实现细节、不同的应用程序选项、模式的代码库、客户端角色和辅助类也有很大不同。
命令模式以命令协议开始,其中包含单个execute()方法。接下来是具体的命令和接收器,CC实现了对接收器的操作,描述了接收器和动作之间的联系。有什么不清楚的吗?我也是,但我们继续吧。 客户端创建特定命令的实例,并将其与接收器关联。 祈求者 –执行启动命令过程的对象。
现在让我们尝试用一个例子来说明这一点,假设我们要更新 myPhone 上的 myOS,为此我们启动 myOS_Update! 应用程序,在其中按下“立即更新”按钮,10 秒后系统将执行此操作。报告更新成功。
上面示例中的客户端是 myOS_Update! 应用程序,Invoker 是“立即更新!”按钮,它启动特定命令 b>使用execute()方法更新系统,该方法访问接收器–操作系统更新守护进程。
让我们接受 myOS_Update 应用程序的 UI!太好了,他们决定将其作为单独的产品出售,以提供更新其他操作系统的界面。在这种情况下,我们将实现一个支持通过库扩展的应用程序,在库中将有特定命令、接收器的实现,我们将保留静态/不可变Invoker ,客户端,协议命令。
因此,不需要支持可变代码,因为我们的代码将保持不变,只有在客户端实现时才会出现问题,因为它们的特定命令和接收器。而且,在这个实现中,不需要传输主应用的源代码,即我们使用 Command 模式封装了命令和 UI 交互。
https://refactoring.guru/ru/design-patterns/command
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612
Builder 模式属于一组模式,其存在对我来说不是特别清楚,我注意到它明显的冗余。属于生成设计模式组。用于实现创建复杂对象的简单接口。
接口的简化。可以更方便地在带有大量参数的构造函数中创建对象,客观上提高了代码的可读性。
不带构建器的 C++ 示例:
auto weapon = new Weapon(“Claws”);
monster->weapon = weapon;
auto health = new MonsterHealth(100);
monster->health = health;
Пример со строителем на C++:
.addWeapon(“Claws”)
.addHealth(100)
.build();
Однако в языках поддерживающих именованные аргументы (named arguments), необходимость использовать именно для этого случая отпадает.
Пример на Swift с использованием named arguments:
let monster = Monster(weapon: “Claws”, health: 100)
不变性。使用构建器,您可以确保创建的对象的封装,直到最终组装阶段。在这里,您需要仔细考虑使用模式是否能让您摆脱工作环境的高度动态性,也许使用模式不会带来任何好处,因为开发团队中缺乏使用封装的文化; .
在对象创建的不同阶段与组件交互。同样使用该模式,可以确保在与系统的其他组件交互时逐步创建对象。这很可能非常有用(?)
当然,您需要“仔细”考虑是否值得在您的项目中广泛使用该模式。具有现代语法和高级 IDE 的语言在提高代码可读性方面消除了使用生成器的需要(请参阅有关命名参数的要点)
1994 年 GoF 书出版时是否应该使用这种模式?很可能是的,但是,从那些年的开源代码库来看,很少有人使用它。
https://refactoring.guru/ru/design-patterns/builder
Composite模式是指结构设计模式;在国内被称为“Compositor”。
假设我们正在开发一个应用程序 –相册。用户可以创建文件夹、在其中添加照片以及执行其他操作。您肯定需要能够显示文件夹中的文件数量、所有文件和文件夹的总数。
显然需要使用树,但是如何以简单方便的接口实现树架构呢?复合模式来救援。
package main
import "fmt"
type component interface {
dataCount() int
}
type file struct {
}
type directory struct {
c []component
}
func (f file) dataCount() int {
return 1
}
func (d directory) dataCount() int {
var outputDataCount int = 0
for _, v := range d.c {
outputDataCount += v.dataCount()
}
return outputDataCount
}
func (d *directory) addComponent(c component) {
d.c = append(d.c, c)
}
func main() {
var f file
var rd directory
rd.addComponent(f)
rd.addComponent(f)
rd.addComponent(f)
rd.addComponent(f)
fmt.Println(rd.dataCount())
var sd directory
sd.addComponent(f)
rd.addComponent(sd)
rd.addComponent(sd)
rd.addComponent(sd)
fmt.Println(sd.dataCount())
fmt.Println(rd.dataCount())
}
https://refactoring.guru/ru/design-patterns/复合

适配器模式是指结构设计模式。
适配器提供两个类/接口之间的数据/接口转换。
假设我们正在开发一个基于神经网络来确定买家在商店中的目标的系统。该系统接收来自商店摄像机的视频流,根据顾客的行为识别顾客,并将他们分类。团体的类型来买东西(潜在买家),只是来看一看(围观者),来偷东西(小偷),来退货(不满意的买家),来喝醉了/喝醉了(潜在的吵闹)。
像所有经验丰富的开发人员一样,我们找到了一个现成的神经网络,可以根据视频流对笼子里的猴子种类进行分类,柏林动物园动物研究所免费提供了该网络,并在视频流上对其进行重新训练从商店购买并获得一个可用的最先进的系统。
有一个小问题–视频流以mpeg2格式编码,我们的系统仅支持OGG Theora。我们没有系统的源代码,我们唯一能做的就是–更改数据集并训练神经网络。该怎么办?编写一个适配器类,用于传输来自 mpeg2 -> OGG Theora 的流并将其发送到神经网络。
根据经典方案,该模式涉及客户端、目标、适应者和适配器。本例中的客户端是一个神经网络,它接收 OGG Theora 中的视频流,目标为“”。与其交互的接口,adaptee –输出mpeg2视频流的接口,适配器–将 mpeg2 转换为 OGG Theora 并通过目标接口发送。
一切看起来都很简单吗?
https://ru.wikipedia.org/wiki/Adapter_ (设计模式)
https://refactoring.guru/ru/design-patterns/adapter
委托模式是主要的设计模式之一。
假设我们正在开发一个理发店应用程序。该应用程序有一个日历,用于选择记录的日期;点击日期将打开一个可供选择的理发师列表。
让我们实现一个简单的系统组件链接,使用指向彼此的指针将日历和屏幕结合起来,实现列表显示:
// псевдокод
class BarbershopScreen {
let calendar: Calendar
func showBarbersList(date: Date) {
showSelectionSheet(barbers(forDate: date))
}
}
class Calendar {
let screen: BarbershopScreen
func handleTap(on date: Date) {
screen.showBarbersList(date: date)
}
}
几天后,要求发生变化;在显示列表之前,您需要显示包含多种服务选择(胡须修剪等)的优惠,但并非总是如此,除了周六之外的所有日子。
我们向日历添加一个检查是否是星期六的检查,根据它,我们调用理发师列表或服务列表的方法,为了清楚起见,我将演示:
// псевдокод
class BarbershopScreen {
let calendar: Calendar
func showBarbersList(date: Date) {
showSelectionSheet(barbers(forDate: date))
}
func showOffersList() {
showSelectionSheet(offers)
}
}
class Calendar {
let screen: BarbershopScreen
func handleTap(on date: Date) {
if date.day != .saturday {
screen.showOffersList()
}
else {
screen.showBarbersList(date: date)
}
}
}
一周后,我们被要求在反馈屏幕上添加日历,就在那时,第一个架构问题发生了!
该怎么办?日历与理发预约屏幕紧密相连。
哇!啊!哦哦
如果您继续使用这个疯狂的应用程序架构,您应该复制整个日历类并将此副本与反馈屏幕相关联。
好吧,看起来不错,然后我们又添加了几个屏幕和几份日历,然后 X 时刻就来了。我们被要求更改日历的设计,这意味着现在我们需要找到日历的所有副本并对所有副本添加相同的更改。这种“做法”极大地影响了开发速度,也增加了出错的机会。结果,这样的项目最终会陷入崩溃状态,甚至原始架构的作者也不再理解他的类的副本是如何工作的,并且沿途添加的其他黑客也会立即崩溃。
需要做什么,或者更好的是,现在开始做什么还不算太晚?使用委托模式!
委托是一种通过公共接口传递类事件的方法。下面是日历委托的示例:
protocol CalendarDelegate {
func calendar(_ calendar: Calendar, didSelect date: Date)
}
现在让我们将使用委托的代码添加到示例代码中:
// псевдокод
class BarbershopScreen: CalendarDelegate {
let calendar: Calendar
init() {
calendar.delegate = self
}
func calendar(_ calendar: Calendar, didSelect date: Date) {
if date.day != .saturday {
showOffersList()
}
else {
showBarbersList(date: date)
}
}
func showBarbersList(date: Date) {
showSelectionSheet(barbers(forDate: date))
}
func showOffersList() {
showSelectionSheet(offers)
}
}
class Calendar {
weak var delegate: CalendarDelegate
func handleTap(on date: Date) {
delegate?.calendar(self, didSelect: date)
}
}
这样一来,我们就将日历与屏幕完全解开了,当从日历中选择日期时,它会传输日期选择事件– *将事件处理委托给订阅者;订阅者就是屏幕。
我们从这种方法中得到什么好处?现在我们可以独立地更改日历和屏幕逻辑,而无需重复类,从而简化了进一步的支持;这样就实现了系统组件实现的“全责原则”,遵守了DRY原则。
使用委托时,您可以添加、更改显示窗口的逻辑、屏幕上任何内容的顺序,这完全不会影响日历和其他类,客观上它们不应该参与与它们不直接相关的进程。< br/>或者,不太打扰自己的程序员可以使用通过公共总线发送消息,而不需要编写单独的协议/委托接口,在这种情况下最好使用委托。我在上一篇文章中写过这种方法的缺点– “观察者模式。”
https://refactoring.guru/ru/replace-inheritance -with-delegation

观察者模式是指行为设计模式。
该模式允许您使用通用接口将对象状态的更改发送给订阅者。
假设我们正在为程序员开发一个信使,我们在应用程序中有一个聊天屏幕。当您收到包含“问题”和“错误”或“出现问题”文本的消息时,您需要将错误列表屏幕和设置屏幕设置为红色。
接下来,我将描述解决该问题的两种选择,第一种很简单,但极难支持,第二种支持更稳定,但需要你在最初的实施过程中转过头来。
该模式的所有实现都包含数据更改时发送消息、订阅消息以及方法中的进一步处理。共享总线选项包含一个将消息分派给接收者的单个对象(通常是单例)。
实现的简单性如下:
Apple 提供的实现选项之一(NSNotificationCenter 子系统)添加了消息头与收件人在传递时调用的方法名称的匹配。
这种方法的最大缺点是——如果您进一步更改消息,则需要首先记住并手动编辑处理和发送该消息的所有位置。有一种情况是快速初始实施,然后是长期、复杂的支持,需要知识库才能正确操作。
在这个实现中,我们将创建最终的多播委托类;就像共享总线的情况一样,对象可以订阅它来接收“消息”或“事件”,但解析和过滤消息的工作是未分配给对象。相反,订阅者类必须实现委托的多播方法来通知它们。
这是通过使用委托接口/协议来实现的;当通用接口发生变化时,应用程序将不再构建,此时需要重做所有处理给定消息的地方,而不需要维护单独的知识库记住这些地方。 编译器是你的朋友。
这种方法提高了团队的生产力,因为不需要编写或存储文档,新开发人员不需要尝试理解消息及其参数是如何处理的,而是使用方便且易于理解的界面,这就是通过代码实现文档范式的方式。
多播委托本身基于委托模式,我将在下一篇文章中介绍它。
https://refactoring.gu/ru/design-patterns/observer
代理模式是指结构设计模式。
该模式描述了通过类层处理类的技术——代理人。代理允许您更改原始类的功能,并且能够保留原始行为,同时维护原始类接口。
让我们想象一下这种情况– 2015年,西欧国家之一决定记录该国用户网站的所有请求,以改进统计并深入了解公民的政治情绪。
让我们想象一下公民用来访问互联网的网关的简单实现的伪代码:
class InternetRouter {
private let internet: Internet
init(internet: Internet) {
self.internet = internet
}
func handle(request: Request, from client: Client) -> Data {
return self.internet.handle(request)
}
}
在上面的代码中,我们创建了一个 Internet 路由器类,其中包含一个指向提供 Internet 访问的对象的指针。当客户发出网站请求时,我们会从互联网返回响应。
使用代理模式和单例反模式,我们将添加用于记录客户端名称和 URL 的功能:
class InternetRouterProxy {
private let internetRouter: InternetRouter
init(internet: Internet) {
self.internetRouter = InternetRouter(internet: internet)
}
func handle(request: Request, from client: Client) -> Data {
Logger.shared.log(“Client name: \(client.name), requested URL: \(request.URL)”)
return self.internetRouter.handle(request: request, from: client)
}
}
由于代理类InternetRouterProxy中保留了原始InternetRouter接口,因此只需将InternerRouter中的初始化类替换为其代理即可,无需进一步更改代码库。
https://refactoring.guru/ru/design-patterns/代理
原型模式属于生成设计模式组。
假设我们正在开发约会应用程序 Tender,根据我们的商业模式,我们有一个付费机会来复制您自己的个人资料,自动更改姓名和照片的顺序。这样做是为了让用户有机会在应用程序中同时维护与不同朋友组的多个个人资料。
通过点击创建个人资料副本的按钮,我们需要实现复制个人资料、自动生成姓名以及重新排序照片。
朴素的伪代码实现:
fun didPressOnCopyProfileButton() {
let profileCopy = new Profile()
profileCopy.name = generateRandomName()
profileCopy.age = profile.age
profileCopy.photos = profile.photos.randomize()
storage.save(profileCopy)
}
现在让我们想象一下其他团队成员复制粘贴了复制代码或从头开始想出它,然后添加了一个新字段–喜欢。该字段存储个人资料喜欢的数量,现在您需要通过添加新字段来手动更新*所有*发生复制的位置。代码的维护和测试非常耗时且困难。
为了解决这个问题,原型设计模式被发明了。让我们创建一个通用的复制协议,其中的 copy() 方法返回具有必要字段的对象的副本。更改实体字段后,您只需要更新一个 copy() 方法,而不需要手动搜索并更新所有包含复制代码的地方。
https://refactoring.guru/ru/design-patterns/prototype
在这篇文章中我将描述状态机(State Machine)的使用,展示一个简单的实现,一个使用State模式的实现。值得一提的是,如果状态少于三个,则不宜使用 State 模式,因为这通常会导致代码可读性和相关支持问题不必要的复杂性——凡事都要有个度。

假设我们正在为民用飞机的媒体系统开发视频播放器屏幕,播放器必须能够加载视频流、播放视频、允许用户停止下载过程、倒带以及执行其他通常的操作一名玩家。
假设播放器缓存了视频流的下一个块,检查是否有足够的块用于播放,开始向用户播放片段,同时继续下载下一个。
此时,用户快退到视频的中间,即现在需要停止播放当前片段并从新位置开始加载。然而,在某些情况下这是无法做到的——用户在观看有关航空安全的视频时无法控制视频流的播放。让我们检查 isSafetyVideoPlaying 标志来检查这种情况。
系统还必须能够暂停当前视频并通过播放器广播船长和船员的警报。让我们添加另一个 isAnnouncementPlaying 标志。另外,要求在显示有关使用播放器的帮助时不要暂停播放,另一个标志是HelpPresenting。
媒体播放器示例伪代码:
class MediaPlayer {
public var isHelpPresenting = false
public var isCaching = false
public var isMediaPlaying: Bool = false
public var isAnnouncementPlaying = false
public var isSafetyVideoPlaying = false
public var currentMedia: Media = null
fun play(media: Media) {
if isMediaPlaying == false, isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
if isCaching == false {
if isHelpPresenting == false {
media.playAfterHelpClosed()
}
else {
media.playAfterCaching()
}
}
}
fun pause() {
if isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
currentMedia.pause()
}
}
}
由于高可变性(熵),上面的示例难以阅读且难以维护。此示例基于我使用不使用状态机的*许多*项目的代码库的经验。
每个复选框必须专门“控制”应用程序的界面元素和业务逻辑;添加另一个复选框的开发人员必须能够处理它们,使用所有可能的选项多次检查和重新检查所有内容。
代入公式“2 ^ 复选框数量”,只需 6 个复选框即可获得 2 ^ 6 = 64 个应用程序行为选项,所有这些复选框组合都需要手动检查和维护。
从开发人员的角度来看,使用这样的系统添加新功能如下所示:
–我们需要添加显示航空公司浏览器页面的功能,并且如果机组人员宣布某些内容,它应该像电影一样最小化。
–好的,我会做的。 (哦该死,我必须添加另一个标志并仔细检查标志相交的所有位置,有很多东西需要更改!)
这也是标志系统的一个弱点–更改应用程序的行为。很难想象如何快速/灵活地基于标志更改行为,如果只更改一个标志后您必须仔细检查所有内容。这种开发方法会导致很多问题、时间和金钱的损失。
如果您仔细查看这些标志,您就会明白,实际上我们正在尝试处理现实世界中发生的特定进程。我们列出了它们:正常模式、显示安全视频、广播船长或船员的消息。对于每个进程,一组已知的规则会改变应用程序的行为。
根据状态机(state machine)模式的规则,我们将所有进程列为枚举中的状态,将状态这样的概念添加到播放器代码中,通过删除标志上的组合来实现基于状态的行为。这样我们就可以将测试的选项减少到精确的状态数量。
伪代码:
enum MediaPlayerState {
mediaPlaying,
mediaCaching,
crewSpeaking,
safetyVideoPlaying,
presentingHelp
}
class MediaPlayer {
fun play(media: Media) {
media.play()
}
func pause() {
media.pause()
}
}
class MediaPlayerStateMachine {
public state: MediaPlayerState
public mediaPlayer: MediaPlayer
public currentMedia: Media
//.. init (mediaPlayer) etc
public fun set(state: MediaPlayerState) {
switch state {
case mediaPlaying:
mediaPlayer.play(currentMedia)
case mediaCaching, crewSpeaking,
safetyVideoPlaying, presentingHelp:
mediaPlayer.pause()
}
}
}
标志系统和状态机的巨大区别在于 set(state: ..) 方法中的逻辑状态切换漏斗,它允许你将人类对状态的理解转化为程序代码,而无需玩逻辑当进一步的代码支持时将标志转换为状态的游戏。
接下来我将展示状态机的简单实现和状态模式之间的区别。假设我们需要添加 10 个状态;结果,状态机类将增长到一个 godobject 的大小,这将变得难以维护且成本高昂。当然,这个实现比flag实现要好(使用flag系统,开发者会先开枪自杀,如果没有,那么看到2^10=1024个变体,QA就会挂掉自己,但如果他们都*不注意*任务的复杂性,那么应用程序简单的用户会注意到它将拒绝使用特定的标志组合)
如果状态数量较多,就需要使用State模式。
让我们向状态协议添加一组规则:
protocol State {
func playMedia(media: Media, context: MediaPlayerContext)
func shouldCacheMedia(context: MediaPlayerContext)
func crewSpeaking(context: MediaPlayerContext)
func safetyVideoPlaying(context:MediaPlayerContext)
func presentHelp(context: MediaPlayerContext)
}
让我们将一组规则的实现移至单独的状态,例如一个状态的代码:
class CrewSpeakingState: State {
func playMedia(context: MediaPlayerContext) {
showWarning(“Can’ t play media - listen to announce!”)
}
func mediaCaching(context: MediaPlayerContext) {
showActivityIndicator()
}
func crewSpeaking(context: MediaPlayerContext) {
set(volume: 100)
}
func safetyVideoPlaying(context: MediaPlayerContext) {
set(volume: 100)
}
func presentHelp(context: MediaPlayerContext) {
showWarning(“Can’ t present help - listen to announce!”)
}
}
接下来,让我们创建一个每个状态都可以工作的上下文,并集成状态机:
final class MediaPlayerContext {
private
var state: State
public fun set(state: State) {
self.state = state
}
public fun play(media: Media) {
state.play(media: media, context: this)
}
…
остальные возможные события
}
应用程序组件通过公共方法与上下文一起工作;状态对象本身决定使用上下文内的状态机从哪个状态转换到哪个状态。
因此,我们实现了上帝对象分解,维护变化的状态会容易得多,这得益于编译器跟踪协议的变化,由于代码行数的减少而降低了理解状态的复杂度,并且专注于解决特定的状态问题。您现在还可以在团队中共享工作,为团队成员提供特定状态的实现,而不必担心需要“解决”冲突,这种情况在使用一个大型状态机类时会发生。
https://refactoring.guru/ru/design-patterns/state
模式方法指的是行为设计模式。该模式描述了一种按需替换类的部分逻辑的方法,而对于后代而言,整个部分保持不变。

假设我们正在开发一家客户银行,请考虑开发授权模块的任务——用户必须能够使用抽象登录数据登录应用程序。
授权模块必须是跨平台的,支持不同的授权技术,存储不同平台的加密数据。为了实现该模块,我们选择跨平台的Kotlin语言,使用授权模块的抽象类(协议),我们将为MyPhone手机编写一个实现:
class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
fun loginAndPassword() : Pair {
return Pair("admin", "qwerty65435")
}
}
class ServerApiClient {
fun authorize(authorizationData: AuthorizationData) : Unit {
println(authorizationData.login)
println(authorizationData.password)
println("Authorized")
}
}
class AuthorizationData {
var login: String? = null
var password: String? = null
}
interface AuthorizationModule {
abstract fun fetchAuthorizationData() : AuthorizationData
abstract fun authorize(authorizationData: AuthorizationData)
}
class MyPhoneAuthorizationModule: AuthorizationModule {
override fun fetchAuthorizationData() : AuthorizationData {
val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
val authorizationData = AuthorizationData()
authorizationData.login = loginAndPassword.first
authorizationData.password = loginAndPassword.second
return authorizationData
}
override fun authorize(authorizationData: AuthorizationData) {
ServerApiClient().authorize(authorizationData)
}
}
fun main() {
val authorizationModule = MyPhoneAuthorizationModule()
val authorizationData = authorizationModule.fetchAuthorizationData()
authorizationModule.authorize(authorizationData)
}
现在,对于每个手机/平台,我们都必须复制用于向服务器发送授权的代码,这违反了 DRY 原则。上面的例子很简单,在更复杂的类中会有更多的重复。为了消除代码重复,您应该使用模板方法模式。
让我们将模块的公共部分移至不可变的方法中,并将加密数据传输的功能转移到特定的平台类:
class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
fun loginAndPassword() : Pair {
return Pair("admin", "qwerty65435")
}
}
class ServerApiClient {
fun authorize(authorizationData: AuthorizationData) : Unit {
println(authorizationData.login)
println(authorizationData.password)
println("Authorized")
}
}
class AuthorizationData {
var login: String? = null
var password: String? = null
}
interface AuthorizationModule {
abstract fun fetchAuthorizationData() : AuthorizationData
fun authorize(authorizationData: AuthorizationData) {
ServerApiClient().authorize(authorizationData)
}
}
class MyPhoneAuthorizationModule: AuthorizationModule {
override fun fetchAuthorizationData() : AuthorizationData {
val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
val authorizationData = AuthorizationData()
authorizationData.login = loginAndPassword.first
authorizationData.password = loginAndPassword.second
return authorizationData
}
}
fun main() {
val authorizationModule = MyPhoneAuthorizationModule()
val authorizationData = authorizationModule.fetchAuthorizationData()
authorizationModule.authorize(authorizationData)
}
https://refactoring.guru/ru/design-模式/模板方法
https://gitlab.com/demensdeum/patterns/< /p>
桥接模式是指结构设计模式。它允许您通过将逻辑移动到单独的抽象类中来抽象类逻辑的实现。听起来很简单,对吧?

假设我们实现一个垃圾邮件机器人,它应该能够向不同类型的信使发送消息。
我们使用通用协议来实现它:
protocol User {
let token: String
let username: String
}
protocol Messenger {
var authorize(login: String, password: String)
var send(message: String, to user: User)
}
class iSeekUUser: User {
let token: String
let username: String
}
class iSeekU: Messenger {
var authorizedUser: User?
var requestSender: RequestSender?
var requestFactory: RequestFactory?
func authorize(login: String, password: String) {
authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
}
func send(message: String, to user: User) {
requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
}
}
class SpamBot {
func start(usersList: [User]) {
let iSeekUMessenger = iSeekU()
iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
for user in usersList {
iSeekUMessennger.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
}
}
}
现在让我们想象一下,用于为 iSekU Messenger 发送消息的新的、更快的协议的发布。要添加新协议,您需要复制 iSekU 机器人的实现,仅更改其中的一小部分。如果只改变了一小部分类逻辑,则不清楚为什么要这样做。这种做法违反了 DRY 原则;随着产品的进一步开发,新功能的实现会出现错误和延迟,从而体现出缺乏灵活性。
让我们将协议的逻辑转移到一个抽象类中,从而实现桥接模式:
protocol User {
let token: String
let username: String
}
protocol Messenger {
var authorize(login: String, password: String)
var send(message: String, to user: User)
}
protocol MessagesSender {
func send(message: String, to user: User)
}
class iSeekUUser: User {
let token: String
let username: String
}
class iSeekUFastMessengerSender: MessagesSender {
func send(message: String, to user: User) {
requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
}
}
class iSeekU: Messenger {
var authorizedUser: User?
var requestSender: RequestSender?
var requestFactory: RequestFactory?
var messagesSender: MessengerMessagesSender?
func authorize(login: String, password: String) {
authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
}
func send(message: String, to user: User) {
messagesSender?.send(message: message, to: user)
}
}
class SpamBot {
var messagesSender: MessagesSender?
func start(usersList: [User]) {
let iSeekUMessenger = iSeekU()
iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
for user in usersList {
messagesSender.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
}
}
}
这种方法的优点之一无疑是能够通过编写实现抽象逻辑的插件/库来扩展应用程序的功能,而无需更改主应用程序的代码。
与策略模式有什么区别?两种模式非常相似,但是,策略描述了切换*算法*,而桥接允许您切换大部分*任何复杂逻辑*。
https://refactoring.guru/ru/design-patterns/bridge
https://gitlab.com/demensdeum/patterns/一个> p>
责任链是指行为设计模式。
电影公司 Jah-Pictures 制作了一部关于利比里亚共产主义拉斯塔法里教徒的纪录片,名为《马利的红色黎明》。这部电影很长(8小时),很有趣,但在上映之前,事实证明,在一些国家,电影中的镜头和短语可能被认为是异端邪说,不会获得发行许可。电影制片人决定手动和自动地从电影中删除包含可疑短语的时刻。需要进行双重检查,以免在人工检查和安装过程中出现错误时,经销商代表不会在某些国家被简单枪决。
国家分为四组:没有审查制度的国家,有中等、中等和非常严格的审查制度。决定使用神经网络对观看的电影片段中的异端级别进行分类。对于该项目,需要购买非常昂贵的最先进的神经元,并针对不同级别的审查进行训练,这是开发人员的任务 -将影片分成碎片,并通过一系列神经网络传输它们,从自由到严格,直到其中一个检测到异端,然后将片段转移到人工审查以进一步编辑。不可能通过所有神经元,因为他们的工作需要太多的计算能力(毕竟我们还要支付电费),在第一个工作时停止就足够了。
朴素的伪代码实现:
import StateOfArtCensorshipHLNNClassifiers
protocol MovieCensorshipClassifier {
func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}
class CensorshipClassifier: MovieCensorshipClassifier {
let hnnclassifier: StateOfArtCensorshipHLNNClassifier
init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier) {
self.hnnclassifier = hnnclassifier
}
func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
return hnnclassifier.shouldBeCensored(movieChunk)
}
}
let lightCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"))
let normalCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal"))
let hardCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard"))
let classifiers = [lightCensorshipClassifier, normalCensorshipClassifier, hardCensorshipClassifier]
let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
for classifier in classifiers {
if classifier.shouldBeCensored(chunk) == true {
print("Should censor movie chunk: \(chunk), reported by \(classifier)")
}
}
}
一般来说,使用分类器数组的解决方案并没有那么糟糕,但是!让我们想象一下,我们无法创建一个数组,我们有机会只创建一个分类器实体,它已经确定了电影片段的审查类型。在开发扩展应用程序功能的库(插件)时,此类限制是可能的。
让我们使用装饰器模式–让我们将链中下一个分类器的引用添加到分类器类中,并在第一个成功分类时停止验证过程。
因此,我们实现了责任链模式:
import StateOfArtCensorshipHLNNClassifiers
protocol MovieCensorshipClassifier {
func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}
class CensorshipClassifier: MovieCensorshipClassifier {
let nextClassifier: CensorshipClassifier?
let hnnclassifier: StateOfArtCensorshipHLNNClassifier
init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier, nextClassifier: CensorshipClassifiers?) {
self.nextClassifier = nextClassifier
self.hnnclassifier = hnnclassifier
}
func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
let result = hnnclassifier.shouldBeCensored(movieChunk)
print("Should censor movie chunk: \(movieChunk), reported by \(self)")
if result == true {
return true
}
else {
return nextClassifier?.shouldBeCensored(movieChunk) ?? false
}
}
}
let censorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"), nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal", nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard")))))
let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
censorshipClassifier.shouldBeCensored(chunk)
}
https://refactoring.guru/ru/设计模式/责任链
https://gitlab.com/demensdeum/patterns/< /p>
Decorator模式是指结构设计模式。
装饰器用作继承的替代方法来扩展类的功能。
有一项任务是根据产品类型扩展应用程序的功能。客户需要三种类型的产品——基础、专业、终极。
基本的–统计字符数,专业–功能 基本+以大写字母打印文本,终极&#8211;基本+专业+打印文字“ULTIMATE”。
我们使用继承来实现它:
protocol Feature {
func textOperation(text: String)
}
class BasicVersionFeature: Feature {
func textOperation(text: String) {
print("\(text.count)")
}
}
class ProfessionalVersionFeature: BasicVersionFeature {
override func textOperation(text: String) {
super.textOperation(text: text)
print("\(text.uppercased())")
}
}
class UltimateVersionFeature: ProfessionalVersionFeature {
override func textOperation(text: String) {
super.textOperation(text: text)
print("ULTIMATE: \(text)")
}
}
let textToFormat = "Hello Decorator"
let basicProduct = BasicVersionFeature()
basicProduct.textOperation(text: textToFormat)
let professionalProduct = ProfessionalVersionFeature()
professionalProduct.textOperation(text: textToFormat)
let ultimateProduct = UltimateVersionFeature()
ultimateProduct.textOperation(text: textToFormat)
现在有需求实现“极致之光”产品——基本版 + 旗舰版,但没有专业版的功能。第一个发生是因为……您必须为这样一个简单的任务创建一个单独的类并复制代码。
让我们继续使用继承来实现:
protocol Feature {
func textOperation(text: String)
}
class BasicVersionFeature: Feature {
func textOperation(text: String) {
print("\(text.count)")
}
}
class ProfessionalVersionFeature: BasicVersionFeature {
override func textOperation(text: String) {
super.textOperation(text: text)
print("\(text.uppercased())")
}
}
class UltimateVersionFeature: ProfessionalVersionFeature {
override func textOperation(text: String) {
super.textOperation(text: text)
print("ULTIMATE: \(text)")
}
}
class UltimateLightVersionFeature: BasicVersionFeature {
override func textOperation(text: String) {
super.textOperation(text: text)
print("ULTIMATE: \(text)")
}
}
let textToFormat = "Hello Decorator"
let basicProduct = BasicVersionFeature()
basicProduct.textOperation(text: textToFormat)
let professionalProduct = ProfessionalVersionFeature()
professionalProduct.textOperation(text: textToFormat)
let ultimateProduct = UltimateVersionFeature()
ultimateProduct.textOperation(text: textToFormat)
let ultimateLightProduct = UltimateLightVersionFeature()
ultimateLightProduct.textOperation(text: textToFormat)
为了清楚起见,可以进一步开发该示例,但即使现在,支持基于继承基础的系统的复杂性也是可见的——麻烦且缺乏灵活性。
装饰器是一组描述功能的协议,是一个抽象类,其中包含对扩展功能的装饰器类的子具体实例的引用。
让我们使用以下模式重写上面的示例:
protocol Feature {
func textOperation(text: String)
}
class FeatureDecorator: Feature {
private var feature: Feature?
init(feature: Feature? = nil) {
self.feature = feature
}
func textOperation(text: String) {
feature?.textOperation(text: text)
}
}
class BasicVersionFeature: FeatureDecorator {
override func textOperation(text: String) {
super.textOperation(text: text)
print("\(text.count)")
}
}
class ProfessionalVersionFeature: FeatureDecorator {
override func textOperation(text: String) {
super.textOperation(text: text)
print("\(text.uppercased())")
}
}
class UltimateVersionFeature: FeatureDecorator {
override func textOperation(text: String) {
super.textOperation(text: text)
print("ULTIMATE: \(text)")
}
}
let textToFormat = "Hello Decorator"
let basicProduct = BasicVersionFeature(feature: UltimateVersionFeature())
basicProduct.textOperation(text: textToFormat)
let professionalProduct = ProfessionalVersionFeature(feature: UltimateVersionFeature())
professionalProduct.textOperation(text: textToFormat)
let ultimateProduct = BasicVersionFeature(feature: UltimateVersionFeature(feature: ProfessionalVersionFeature()))
ultimateProduct.textOperation(text: textToFormat)
let ultimateLightProduct = BasicVersionFeature(feature: UltimateVersionFeature())
ultimateLightProduct.textOperation(text: textToFormat)
现在我们可以创建任何类型产品的变体——在应用程序启动阶段初始化组合类型就足够了,下面的例子是Ultimate + Professional版本的创建:
ultimateProfessionalProduct.textOperation(text: textToFormat)
https://refactoring.guru/ru/design-patterns/decorator
https://gitlab.com/demensdeum/patterns
中介者模式指的是行为设计模式。
有一天,您收到一份开发笑话应用程序的订单 –用户按下屏幕中间的按钮,就会听到鸭子嘎嘎的有趣声音。
上传到应用商店后,该应用程序大受欢迎:每个人都在浏览你的应用程序,埃隆·马斯克在下一次火星超高速隧道启动时在他的 Instagram 上嘎嘎叫,希拉里·克林顿在辩论中击败了唐纳德·特朗普并赢得乌克兰选举,成功!
应用程序的简单实现如下所示:
class DuckButton {
func didPress() {
print("quack!")
}
}
let duckButton = DuckButton()
duckButton.didPress()
接下来,您决定添加狗吠的声音,为此您需要显示两个用于选择声音的按钮:与一只鸭子和一只狗。让我们创建两个按钮类:DuckButton 和 DogButton。
更改代码:
class DuckButton {
func didPress() {
print("quack!")
}
}
class DogButton {
func didPress() {
print("bark!")
}
}
let duckButton = DuckButton()
duckButton.didPress()
let dogButton = DogButton()
dogButton.didPress()
再一次成功后,我们添加了猪叫声,已经有三类按钮了:
class DuckButton {
func didPress() {
print("quack!")
}
}
class DogButton {
func didPress() {
print("bark!")
}
}
class PigButton {
func didPress() {
print("oink!")
}
}
let duckButton = DuckButton()
duckButton.didPress()
let dogButton = DogButton()
dogButton.didPress()
let pigButton = PigButton()
pigButton.didPress()
用户抱怨声音相互重叠。
我们添加一个检查来防止这种情况发生,同时将类相互引入:
class DuckButton {
var isMakingSound = false
var dogButton: DogButton?
var pigButton: PigButton?
func didPress() {
guard dogButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false else { return }
isMakingSound = true
print("quack!")
isMakingSound = false
}
}
class DogButton {
var isMakingSound = false
var duckButton: DuckButton?
var pigButton: PigButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false else { return }
isMakingSound = true
print("bark!")
isMakingSound = false
}
}
class PigButton {
var isMakingSound = false
var duckButton: DuckButton?
var dogButton: DogButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
dogButton?.isMakingSound ?? false == false else { return }
isMakingSound = true
print("oink!")
isMakingSound = false
}
}
let duckButton = DuckButton()
duckButton.didPress()
let dogButton = DogButton()
dogButton.didPress()
let pigButton = PigButton()
pigButton.didPress()
在您申请成功的浪潮中,政府决定制定一项法律,规定移动设备上的嘎嘎声、吠声和咕噜声只能在工作日的上午 9:00 至 15:00 进行;届时,您的应用程序的用户将面临因使用个人电子设备制作淫秽声音而被判处 5 年监禁的风险。
更改代码:
import Foundation
extension Date {
func mobileDeviceAllowedSoundTime() -> Bool {
let hour = Calendar.current.component(.hour, from: self)
let weekend = Calendar.current.isDateInWeekend(self)
let result = hour >= 9 && hour <= 14 && weekend == false
return result
}
}
class DuckButton {
var isMakingSound = false
var dogButton: DogButton?
var pigButton: PigButton?
func didPress() {
guard dogButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("quack!")
isMakingSound = false
}
}
class DogButton {
var isMakingSound = false
var duckButton: DuckButton?
var pigButton: PigButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("bark!")
isMakingSound = false
}
}
class PigButton {
var isMakingSound = false
var duckButton: DuckButton?
var dogButton: DogButton?
func didPress() {
guard duckButton?.isMakingSound ?? false == false &&
dogButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("oink!")
isMakingSound = false
}
}
let duckButton = DuckButton()
let dogButton = DogButton()
let pigButton = PigButton()
duckButton.dogButton = dogButton
duckButton.pigButton = pigButton
dogButton.duckButton = duckButton
dogButton.pigButton = pigButton
pigButton.duckButton = duckButton
pigButton.dogButton = dogButton
duckButton.didPress()
dogButton.didPress()
pigButton.didPress()
突然,手电筒应用程序开始将我们的应用程序挤出市场,我们不要让它打败我们,通过按“oink-oink”按钮添加手电筒,以及其余按钮:
import Foundation
extension Date {
func mobileDeviceAllowedSoundTime() -> Bool {
let hour = Calendar.current.component(.hour, from: self)
let weekend = Calendar.current.isDateInWeekend(self)
let result = hour >= 9 && hour <= 14 && weekend == false
return result
}
}
class Flashlight {
var isOn = false
func turn(on: Bool) {
isOn = on
}
}
class DuckButton {
var isMakingSound = false
var dogButton: DogButton?
var pigButton: PigButton?
var flashlight: Flashlight?
func didPress() {
flashlight?.turn(on: true)
guard dogButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("quack!")
isMakingSound = false
}
}
class DogButton {
var isMakingSound = false
var duckButton: DuckButton?
var pigButton: PigButton?
var flashlight: Flashlight?
func didPress() {
flashlight?.turn(on: true)
guard duckButton?.isMakingSound ?? false == false &&
pigButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("bark!")
isMakingSound = false
}
}
class PigButton {
var isMakingSound = false
var duckButton: DuckButton?
var dogButton: DogButton?
var flashlight: Flashlight?
func didPress() {
flashlight?.turn(on: true)
guard duckButton?.isMakingSound ?? false == false &&
dogButton?.isMakingSound ?? false == false &&
Date().mobileDeviceAllowedSoundTime() == true else { return }
isMakingSound = true
print("oink!")
isMakingSound = false
}
}
let flashlight = Flashlight()
let duckButton = DuckButton()
let dogButton = DogButton()
let pigButton = PigButton()
duckButton.dogButton = dogButton
duckButton.pigButton = pigButton
duckButton.flashlight = flashlight
dogButton.duckButton = duckButton
dogButton.pigButton = pigButton
dogButton.flashlight = flashlight
pigButton.duckButton = duckButton
pigButton.dogButton = dogButton
pigButton.flashlight = flashlight
duckButton.didPress()
dogButton.didPress()
pigButton.didPress()
这样一来,我们就有了一个庞大的应用程序,其中包含大量的复制粘贴代码,里面的类之间通过死链接连接起来——不存在弱耦合,这样的奇迹很难维护,而且由于犯错误的可能性很高,因此未来会发生变化。
让我们添加一个中间中介类 - ApplicationController。该类将提供对象的松耦合,确保类之间的职责分离,并消除重复的代码。
让我们重写一下:
import Foundation
class ApplicationController {
private var isMakingSound = false
private let flashlight = Flashlight()
private var soundButtons: [SoundButton] = []
func add(soundButton: SoundButton) {
soundButtons.append(soundButton)
}
func didPress(soundButton: SoundButton) {
flashlight.turn(on: true)
guard Date().mobileDeviceAllowedSoundTime() &&
isMakingSound == false else { return }
isMakingSound = true
soundButton.didPress()
isMakingSound = false
}
}
class SoundButton {
let soundText: String
init(soundText: String) {
self.soundText = soundText
}
func didPress() {
print(soundText)
}
}
class Flashlight {
var isOn = false
func turn(on: Bool) {
isOn = on
}
}
extension Date {
func mobileDeviceAllowedSoundTime() -> Bool {
let hour = Calendar.current.component(.hour, from: self)
let weekend = Calendar.current.isDateInWeekend(self)
let result = hour >= 9 && hour <= 14 && weekend == false
return result
}
}
let applicationController = ApplicationController()
let pigButton = SoundButton(soundText: "oink!")
let dogButton = SoundButton(soundText: "bark!")
let duckButton = SoundButton(soundText: "quack!")
applicationController.add(soundButton: pigButton)
applicationController.add(soundButton: dogButton)
applicationController.add(soundButton: duckButton)
pigButton.didPress()
dogButton.didPress()
duckButton.didPress()
许多有关用户界面应用程序体系结构的文章描述了 MVC 模式及其派生物。模型用于处理业务逻辑数据,视图或表示在界面中向用户显示信息/提供与用户的交互,控制器是确保系统组件交互的中介者。
https://refactoring.guru/ru/design-patterns/调解员
https://gitlab.com/demensdeum/patterns/< /p>
The Strategy pattern allows you to select the type of algorithm that implements a common interface, right while the application is running. This pattern refers to the behavioral design patterns.

Suppose we are developing a music player with embedded codecs. The built-in codecs imply reading music formats without using external sources of the operating system (codecs), the player should be able to read tracks of different formats and play them. VLC player has such capabilities, it supports various types of video and audio formats, it runs on popular and not very operating systems.
Imagine what a naive player implementation looks like:
var player: MusicPlayer? func play(filePath: String) { let extension = filePath.pathExtension if extension == "mp3" { playMp3(filePath) } else if extension == "ogg" { playOgg(filePath) } } func playMp3(_ filePath: String) { player = MpegPlayer() player?.playMp3(filePath) } func playOgg(_ filePath: String) { player = VorbisPlayer() player?.playMusic(filePath) }
Next, we add several formats, which leads to the need to write additional methods. Plus, the player must support plug-in libraries, with new audio formats that will appear later. There is a need to switch the music playback algorithm, the Strategy pattern is used to solve this problem.
Let’s create a common protocol MusicPlayerCodecAlgorithm, write the implementation of the protocol in two classes MpegMusicPlayerCodecAlgorithm and VorbisMusicPlayerCodecAlgorithm, to play mp3 and ogg files with-but. Create a class MusicPlayer, which will contain a reference for the algorithm that needs to be switched, then by the file extension we implement codec type switching:
import Foundation class MusicPlayer { var playerCodecAlgorithm: MusicPlayerCodecAlgorithm? func play(_ filePath: String) { playerCodecAlgorithm?.play(filePath) } } protocol MusicPlayerCodecAlgorithm { func play(_ filePath: String) } class MpegMusicPlayerCodecAlgorithm: MusicPlayerCodecAlgorithm { func play(_ filePath: String) { debugPrint("mpeg codec - play") } } class VorbisMusicPlayerCodecAlgorithm: MusicPlayerCodecAlgorithm { func play(_ filePath: String) { debugPrint("vorbis codec - play") } } func play(fileAtPath path: String) { guard let url = URL(string: path) else { return } let fileExtension = url.pathExtension let musicPlayer = MusicPlayer() var playerCodecAlgorithm: MusicPlayerCodecAlgorithm? if fileExtension == "mp3" { playerCodecAlgorithm = MpegMusicPlayerCodecAlgorithm() } else if fileExtension == "ogg" { playerCodecAlgorithm = VorbisMusicPlayerCodecAlgorithm() } musicPlayer.playerCodecAlgorithm = playerCodecAlgorithm musicPlayer.playerCodecAlgorithm?.play(path) } play(fileAtPath: "Djentuggah.mp3") play(fileAtPath: "Procrastinallica.ogg")
The above example also shows the simplest example of a factory (switching the codec type from the file extension) It is important to note that the Strategy strategy does not create objects, it only describes how to create a common interface for switching the family of algorithms.
https://refactoring.guru/en/design-patterns/strategy
https://gitlab.com/demensdeum/patterns/
In this article I will describe the Iterator pattern.
This pattern refers to the behavioral design patterns.
Suppose we need to print a list of tracks from the album “Procrastinate them all” of the group “Procrastinallica”.
The naive implementation (Swift) looks like this:
for i=0; i < tracks.count; i++ { print(tracks[i].title) }
Suddenly during compilation, it is detected that the class of the tracks object does not give the number of tracks in the count call, and moreover, its elements cannot be accessed by index. Oh…
Suppose we are writing an article for the magazine “Wacky Hammer”, we need a list of tracks of the group “Djentuggah” in which bpm exceeds 140 beats per minute. An interesting feature of this group is that its records are stored in a huge collection of underground groups, not sorted by albums, or for any other grounds. Let’s imagine that we work with a language without functionality:
var djentuggahFastTracks = [Track]() for track in undergroundCollectionTracks { if track.band.title == "Djentuggah" && track.info.bpm == 140 { djentuggahFastTracks.append(track) } }
Suddenly, a couple of tracks of the group are found in the collection of digitized tapes, and the editor of the magazine suggests finding tracks in this collection and writing about them. A Data Scientist friend suggests to use the Djentuggah track classification algorithm, so you don’t need to listen to a collection of 200 thousand tapes manually. Try:
var djentuggahFastTracks = [Track]() for track in undergroundCollectionTracks { if track.band.title == "Djentuggah" && track.info.bpm == 140 { djentuggahFastTracks.append(track) } } let tracksClassifier = TracksClassifier() let bpmClassifier = BPMClassifier() for track in cassetsTracks { if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 { djentuggahFastTracks.append(track) } }
Now, just before sending to print, the editor reports that 140 beats per minute are out of fashion, people are more interested in 160, so the article should be rewritten by adding the necessary tracks.
Apply changes:
var djentuggahFastTracks = [Track]() for track in undergroundCollectionTracks { if track.band.title == "Djentuggah" && track.info.bpm == 160 { djentuggahFastTracks.append(track) } } let tracksClassifier = TracksClassifier() let bpmClassifier = BPMClassifier() for track in cassetsTracks { if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 { djentuggahFastTracks.append(track) } }
The most attentive ones noticed an error; the bpm parameter was changed only for the first pass through the list. If there were more passes through the collections, then the chance of a mistake would be higher, that is why the DRY principle should be used. The above example can be developed further, for example, by adding the condition that you need to find several groups with different bpm, by the names of vocalists, guitarists, this will increase the chance of error due to duplication of code.
In the literature, an iterator is described as a combination of two protocols / interfaces, the first is an iterator interface consisting of two methods – next(), hasNext(), next() returns an object from the collection, and hasNext() reports that there is an object and the list is not over. However in practice, I observed iterators with one method – next(), when the list ended, null was returned from this object. The second is a collection that should have an interface that provides an iterator – the iterator() method, there are variations with the collection interface that returns an iterator in the initial position and in end – the begin() and end() methods are used in C ++ std.
Using the iterator in the example above will remove duplicate code, eliminate the chance of mistake due to duplicate filtering conditions. It will also be easier to work with the collection of tracks on a single interface – if you change the internal structure of the collection, the interface will remain old and the external code will not be affected.
Wow!
let bandFilter = Filter(key: "band", value: "Djentuggah") let bpmFilter = Filter(key: "bpm", value: 140) let iterator = tracksCollection.filterableIterator(filters: [bandFilter, bpmFilter]) while let track = iterator.next() { print("\(track.band) - \(track.title)") }
While the iterator is running, the collection may change, thus causing the iterator’s internal counter to be invalid, and generally breaking such a thing as “next object”. Many frameworks contain a check for changing the state of the collection, and in case of changes they return an error / exception. Some implementations allow you to remove objects from the collection while the iterator is running, by providing the remove() method in the iterator.
https://refactoring.guru/en/design-patterns/iterator
https://gitlab.com/demensdeum/patterns/
在这篇文章中,我将描述“快照”模式。或“纪念品”
此模式指的是“行为”模式。设计模式。

假设我们正在开发一个图形编辑器,并且我们需要添加根据用户命令回滚操作的功能。同样非常重要的是,在实现此模式时,系统组件无权访问回滚“操作”的内部状态,其他系统组件只能访问快照对象而无法更改;其内部状态,提供清晰、简单的外部接口。为了解决这个问题,使用了“快照”模式。或“守护者”。
工作示例“快照”介绍如下:
单击时,会出现一个精灵,单击卷曲的箭头时,操作会被取消–精灵消失了。该示例由三个类组成:
在模式“快照”的上下文中课程有:
该模式的一个重要特征是,只有源才能访问快照中已保存状态的内部字段,这是保护快照免受外部更改(来自想要绕过封装进行更改的开发人员的更改)所必需的; ,破坏系统逻辑)。为了实现封装,使用内置类,并且在 C++ 中,它们使用指定友元类的功能。就我个人而言,我为 Rise 实现了一个没有封装的简单版本,并在为 Swift 实现时使用 Generic。在我的版本中– Memento 仅将其内部状态赋予同一类状态的实体:
https://refactoring.guru/design-patterns/memento
https://gitlab.com/demensdeum/patterns/< /p>
在这篇文章中,我将描述一种名为“Visitor”的设计模式。或“访客”
此模式属于行为模式组。
该模式主要用于绕过早期绑定语言中单次调度的限制。
爱丽丝 X 作者 NFGPhoto (CC-2.0)
让我们创建一个抽象类/协议 Band,创建 MurpleDeep 的子类,创建一个具有两个方法的 Visitor 类 –一个用于将 Band 的任何后代输出到控制台,第二个用于输出任何 MurpleDeep,主要是方法的名称(签名)相同,参数仅因类而异。使用带 Band 参数的中间打印输出方法,我们创建一个 Visitor 实例并调用 MurpleDeep 的访问方法。
下面是 Kotlin 中的代码:
输出将为“这是 Band 类“
许多文章(包括俄语)都用巧妙的文字描述了为什么会发生这种情况,但我建议您想象一下编译器如何看待代码,也许一切都会立即变得清晰:
有很多解决方案可以解决这个问题,接下来我们将考虑使用访问者模式的解决方案。
我们将带有 Visitor 参数的accept方法添加到抽象类/协议中,在方法内部调用visitor.visit(this),然后将accept方法的重写/实现添加到MurpleDeep类中,果断而冷静地违反了DRY,再次编写Visitor.visit(this).< br />最终代码:
https://refactoring.guru/ru/设计模式/访客双调度
https://gitlab.com/demensdeum/patterns
在这篇文章中,我将描述“轻量级”结构模式。或“机会主义者” (蝇量级)
该模式属于结构模式组。
让我们看一下下面该模式如何工作的示例:
为什么需要它? 节省内存。我同意,在Java广泛使用的时代(白白消耗cpu和内存),这不再那么重要,但值得使用。
上面的例子中只输出了 40 个对象,但是如果将数量增加到 120,000,内存消耗也会相应增加。
让我们看看 Chromium 浏览器中不使用享元模式的内存消耗情况:

如果不使用模式,内存消耗约为 300 MB。
现在让我们向应用程序添加一个模式并查看内存消耗:

使用该模式,内存消耗约为 200 MB,因此我们在测试应用程序中节省了 100 MB 内存;在严肃的项目中,差异可能会更大。
在上面的例子中,我们画了 40 只猫,或者为了清楚起见,画了 12 万只猫。每只猫都作为 png 图像加载到内存中,然后在大多数渲染中将其转换为用于渲染的位图(实际上是 bmp),这样做是为了速度,因为压缩的 png 需要很长时间来渲染。不使用该模式时,我们将 12 万张猫的图片加载到 RAM 中并进行绘制,但是当使用“轻量级”模式时,我们会加载 12 万张猫的图片。我们将一只猫加载到内存中,并以不同的位置和透明度将其绘制 12 万次。整个神奇之处在于,我们在渲染时将坐标和透明度与猫图像分开实现,渲染只需要一只猫并使用具有坐标和透明度的对象来正确渲染。
以下是语言 Rise 的示例 >< /p>
不使用模式:

猫图像是为循环中的每个对象单独加载的–猫图像。
使用模式:

一张猫的图片被 12 万个物体使用。
用于 GUI 框架,例如 Apple 的“重用”; (重用)UITableViewCell 表格单元格,这为不了解此模式的初学者增加了入门障碍。也常用于游戏开发。
https://gitlab.com/demensdeum/patterns/< /p>
https://refactoring.guru/ru/design-patterns/蝇量级
http://gameprogrammingpatterns.com/flyweight.html
在这篇笔记中,我将描述我和我的同事在处理各种(成功的和不那么成功的)项目时使用单例模式(外国文学中的单例)的经验。我将描述为什么我个人认为这种模式不能在任何地方使用,我还将描述团队中的哪些心理因素影响这种反模式的集成。献给所有跌倒和残废的开发人员,他们试图理解为什么这一切都始于一名团队成员带来了一只可爱的小狗,易于处理,不需要特殊的照顾和知识来照顾它,并以饲养的野兽结束劫持你的项目,需要越来越多的工时,消耗用户的神经和金钱,并为评估看似简单的实施创建绝对可怕的数字事情。

Wolf in sheep’s clothing by SarahRichterArt
故事发生在另一个宇宙,所有巧合都是随机的…
每个人在生活中有时都会有不可抗拒的想要抚摸猫的欲望。世界各地的分析师预测,第一家创建猫配送和租赁应用程序的初创公司将变得非常受欢迎,并且在不久的将来将被 Moogle 以数万亿美元收购。很快这种事就发生了。秋明州的一个人创建了 Cat@Home 应用程序,很快就成为了万亿富翁,Moogle 公司获得了新的利润来源,数百万压力重重的人们有机会请一只猫到他们家进一步熨烫并平静下来。
来自摩尔曼斯克的一位极其富有的牙医 Alexey Goloborodko 对《福布斯》上一篇有关 Cat@Home 的文章印象深刻,决定他也想成为天文数字般的富有。为了实现这个目标,他通过朋友找到了一家来自 Goldfield – 的公司。提供软件开发服务的 Wakeboard DevPops 向他们订购了 Cat@Home 克隆版本的开发。
该项目名为Fur&Pure,委托给一支由20人组成的才华横溢的开发团队;接下来我们关注一个5人的移动开发团队。每个团队成员都分担自己的工作,以敏捷和 Scrum 武装,团队按时(六个月内)完成开发,没有错误,在 iStore 中发布应用程序,在 iStore 中被 100,000 个用户评为 5,有很多关于应用程序有多棒、服务有多出色(毕竟是另类宇宙)的评论。猫熨好了,应用程序发布了,一切似乎都很顺利。不过,Moogle 并不急于斥资数万亿美元收购一家初创公司,因为 Cat@Home 中不仅出现了猫,还出现了狗。
应用程序的所有者决定是时候将狗添加到应用程序中,要求公司进行评估,并收到大约至少六个月的时间来将狗添加到应用程序中。事实上,应用程序将再次从头开始编写。在此期间,Moogle 将在应用程序中添加蛇、蜘蛛和豚鼠,而 Fur&Pur 将只接收狗。
为什么会发生这种情况?缺乏灵活的应用程序架构是一切的罪魁祸首;最常见的因素之一是单例设计反模式。
为了在家订购一只猫,消费者需要创建一个请求并将其发送到办公室,办公室将处理该请求并将猫寄给快递员,快递员将已经收到服务费用。
一位程序员决定创建一个类“Cat Application”;具有必要的字段,通过单例将此类带入全局应用程序空间。他为什么要这么做?为了节省时间(节省半小时的一分钱),因为公开应用程序比考虑应用程序架构和使用依赖项注入更容易。然后其他开发人员选择这个全局对象并将他们的类绑定到它。 例如,所有屏幕本身都访问全局对象“CatRequest”;并显示应用程序上的数据。这样,这样一个单体应用程序就被测试并发布了。
一切似乎都很好,但突然出现一位客户,要求将狗的请求添加到应用程序中。团队开始疯狂地评估系统中有多少组件将受到此更改的影响。分析结束时,我们发现有必要重做 60% 到 90% 的代码,以便让应用程序在全局单例对象中不仅接受“对 Cat 的请求”,还可以接受“请求猫”。而且还是“申请一只狗”,现阶段评估其他动物的添加已经没什么用了,应付至少两只。
首先,在需求收集阶段,明确指出创建灵活、可扩展架构的需求。其次,值得对产品代码进行独立审查,并对薄弱环节进行强制研究。如果你是一名开发人员并且喜欢单身,那么我建议你在为时已晚之前醒悟过来,否则肯定会失眠和神经紧张。如果您正在开发一个具有大量单例的遗留项目,请尝试尽快摆脱它们或项目。
您需要从单例全局对象/变量的反模式切换到依赖注入–最简单的设计模式,在初始化阶段将所有必要的数据赋予类的实例,而无需进一步绑定到全局空间。
https://stackoverflow。 com/questions/137975/what-is-so-bad-about-singleton
http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/
https://blog.ndepend.com/singleton-pattern-costs/