在 Linux 上构建适用于 iOS 的 C++ SDL 应用程序

在这篇文章中,我将描述在 Linux 上为 iOS 构建 C++ SDL 应用程序的过程,在没有付费 Apple Developer 订阅的情况下签署 ipa 存档,以及在没有越狱的情况下使用 macOS 将其安装在干净的设备 (iPad) 上。< /p>

首先,让我们安装 Linux 的构建工具链:
https://github.com/tpoechtrager/cctools-port

需要从存储库下载工具链,然后按照Godot Engine网站上的说明完成安装:
https://docs.godotengine.org/ru/latest/development/compiling/cross-compiling_for_ios_on_linux.html

目前,您需要下载 Xcode dmg 并从那里复制 sdk 来构建 cctools-port。此阶段在 macOS 上更容易完成;只需从已安装的 Xcode 复制必要的 sdk 文件即可。成功编译后,终端将包含交叉编译器工具链的路径。

接下来您可以开始构建适用于 iOS 的 SDL 应用程序。让我们打开 cmake 并添加必要的更改来构建 C++ 代码:

SET(CMAKE_SYSTEM_NAME Darwin)
SET(CMAKE_C_COMPILER arm-apple-darwin11-clang)
SET(CMAKE_CXX_COMPILER arm-apple-darwin11-clang++)
SET(CMAKE_LINKER arm-apple-darwin11-ld)

现在您可以使用 cmake 和 make 进行编译,但不要忘记将 $PATH 添加到交叉编译器工具链:


PATH=$PATH:~/Sources/cctools-port/usage_examples/ios_toolchain/target/bin

为了与框架和SDL正确链接,我们将它们编写在cmake中,例如游戏Space Jaguar的依赖项:


target_link_libraries(
${FSEGT_PROJECT_NAME}
${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/libclang_rt.ios.a
${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/libSDL2.a
${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/libSDL2_mixer.a
${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/libSDL2_image.a
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/CoreServices.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/ImageIO.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/Metal.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/AVFoundation.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/GameController.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/CoreMotion.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/CoreGraphics.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/AudioToolbox.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/CoreAudio.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/QuartzCore.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/OpenGLES.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/UIKit.framework"
"${FLAME_STEEL_PROJECT_ROOT_DIRECTORY}/scripts/buildScripts/ios/resources/libs/Foundation.framework"
)

就我而言,SDL、SDL_Image、SDL_mixer 库预先在 macOS 上的 Xcode 中编译以进行静态链接;从 Xcode 复制的框架。还添加了 libclang_rt.ios.a 库,其中包括特定于 iOS 的运行时调用,例如 isOSVersionAtLeast。包含一个用于 OpenGL ES 的宏,禁用移动版本中不支持的功能,类似于 Android。

解决所有构建问题后,您应该获得 ARM 的已组装二进制文件。接下来,让我们考虑在没有越狱的设备上运行组装的二进制文件。

在 macOS 上,安装 Xcode,在 Apple 门户上注册,无需支付开发者计划费用。在Xcode中添加帐户->首选项->帐户、创建空白应用程序并在真实设备上构建。在组装过程中,设备将被添加到免费的开发者帐户中。组装并启动后,您需要构建存档才能执行此操作,选择 Generic iOS Device and Product ->档案。构建存档后,从中提取embedded.mobileprovision和PkgInfo文件。从设备的构建日志中,找到具有正确签名密钥的 codesign 行、扩展名为 app.xcent 的授权文件的路径,将其复制。

从压缩包中复制.app文件夹,将压缩包中的二进制文件替换为Linux中交叉编译器编译的二进制文件(例如SpaceJaguar.app/SpaceJaguar),然后将必要的资源添加到.app中,检查存档中 .app 中的 PkgInfo 和Embedded.mobileprovision 文件的完整性,如有必要,请再次复制。我们使用协同设计命令 – 重新签名 .app codesign 需要输入密钥进行签名,即权利文件的路径(可以使用 .plist 扩展名重命名)

重新签名后,创建一个 Payload 文件夹,将扩展名为 .app 的文件夹移动到那里,在根目录中创建一个包含 Payload 的 zip 存档,并使用 .ipa 扩展名重命名该存档。之后,在 Xcode 中,打开设备列表并将新的 ipa 拖放到设备的应用程序列表中;通过 Apple Configurator 2 安装不适用于此方法。如果重新签名正确完成,则具有新二进制文件的应用程序将安装在具有 7 天证书的 iOS 设备(例如 iPad)上,这对于测试期来说足够了。

来源

https://github.com/tpoechtrager/cctools-port
https://docs.godotengine.org/ru/latest/development/compiling/cross-compiling_for_ios_on_linux.html
https://jonnyzzz.com/blog/2018/06/13/link-error-3/
https://stackoverflow.com/questions/6896029/re-sign-ipa-iphone
https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html

修复 Windows 10 中 HDD 速度缓慢的问题

此说明谨献给所有不放弃的硬盘用户。


Original (Mae Mu)

使用配备双 HDD (Windows 10) 和 SSD (Ubuntu) 的 HP Pavilion 笔记本电脑 1.5 年之后,我开始注意到应用程序的加载时间很长,界面普遍无响应,并且在最简单的操作上冻结在 Windows 10 中。问题被最小化到可以再次使用笔记本电脑的程度。接下来,我将描述我为解决该问题所采取的步骤。

诊断

为了开始研究,我们需要消除任何类型的骗局;首先,让我们确定硬盘故障的主要原因。使用硬盘时可能会出现什么问题?问题可能出现在电子设备的物理层面和逻辑、软件数据层面。
电子问题包括:计算机/笔记本电脑电源不工作、笔记本电脑电池问题;硬盘驱动器组件的磨损、驱动器内部组件的电路和芯片问题、固件错误、驱动器冲击/跌落的后果或影响其运行的其他设备的类似问题。
硬盘驱动器的严重磨损被认为是出现大量坏扇区(坏块)以致驱动器无法进一步运行的时刻。这些块被硬盘固件阻挡,数据会自动转移到其他扇区,直到某个关键时刻才应该影响磁盘的运行。
程序逻辑问题包括由于应用程序操作不正确而导致的文件系统错误、用户操作:热时关闭设备、在没有正确停止应用程序的情况下完成录制过程、驱动程序错误、操作系统服务。
如果没有专门的电子诊断工具,我们只能检查软件层面的正确性;在此过程中,可能会发现电子问题,通常通过块修复方法(更换元件/芯片)来排除;接下来,我们将考虑使用诊断实用程序的软件诊断方法。值得注意的是,所有实用程序都必须以最高优先级在系统上启动,因为其他应用程序可能会干扰性能测量并阻止磁盘读/写,这将导致错误的诊断结果。

智能

S.M.A.R.T.存储设备状态监控系统HDD、SDD、eMMC等。允许您评估设备的磨损情况,查看坏块数量,并根据数据采取进一步的措施。您可以在不同的应用程序中查看 SMART 以使用磁盘;我更喜欢使用制造商提供的实用程序。对于我的希捷硬盘,我使用了 SeaTools 实用程序,其状态显示为 GOOD,即磁盘固件认为一切正常。

制造商实用程序

磁盘制造商的实用程序提供测试来验证其操作。 SeaTools 有多种类型的测试,您可以使用它们来定位问题。快速而简单的测试可能不会发现任何问题,因此更喜欢长时间的测试。就我而言,只有长测试发现了错误。

慢行

为了检查读取的正确性,查找慢速或死块,我编写了一个应用程序 Slowride,它的工作原理非常简单——打开块设备描述符,使用指定的用户设置,读取整个设备的数据,使用时间测量,慢速块的输出。程序在出现第一个错误时停止;在这种情况下,您将不得不转向更严肃的实用程序来删除数据,因为无法使用简单的方法读取磁盘数据。
就我而言,读取整个磁盘的操作正确,但速度略有下降 –在磁盘的某些区域上,一秒内达到 90MB/秒 (5400rpm)。由此可以得出结论,我正在处理一个软件问题。

声学分析

此方法不适用于软件诊断方法,但对于修复问题相当重要。例如,如果电源部分工作,硬盘驱动器可能会冻结/冻结并发出很大的咔哒声。
就我而言,在 Windows 10 中使用磁盘时,我听到了所有 HDD 用户都熟悉的声音,
尝试在操作系统中执行某些操作时,磁盘头来回运行的巨大破裂声,但声音几乎是恒定的,这让我认为碎片太多磁盘,后台服务导致磁盘过载。

修复

软件诊断期间未检测到电子问题;整个磁盘的逐块读取正确完成,但 SeaTools 在长时间测试期间显示错误。

制造商实用程序

除了诊断之外,磁盘制造商的软件还提供错误纠正程序。在 SeaTools 中,“全部修复”按钮负责执行此操作;在确认您同意可能丢失数据后,将开始更正过程。此修复对我的情况有帮助吗?不,磁盘继续大声且缓慢地运行,但长时间测试不再显示错误。

CHKDSK

CHKSDK 是一款 Microsoft 实用程序,用于排除 Windows 文件系统的软件错误。随着时间的推移,此类错误会在磁盘上累积,并可能极大地干扰工作,包括导致根本无法读取/写入任何数据。您可以在 Microsoft 网站上找到使用该实用程序的说明,但我建议使用所有可能的标志来纠正错误(在撰写本文时,这是 /r /b /f);您需要通过Windows终端(cmd)以管理员权限运行扫描,对于系统分区,扫描将在系统启动时进行,并且可能需要很长时间,在我的例子中需要12个小时。
此修复对我的情况有帮助吗?没有。

磁盘碎片整理

磁盘上的数据按块进行处理;大文件通常写入多个块/片段。随着时间的推移,许多删除的文件会创建不在附近的空块,因此,在写入文件时,它们会填充这些空白,并且磁盘头必须在物理上移动很长的距离。这个问题称为碎片,只有硬盘用户才会遇到。在几次修复时,我的硬盘碎片为41%,直观上看起来像这样:

也就是说,一切都很糟糕。您可以使用碎片整理实用程序或内置碎片整理程序查看碎片并对其进行碎片整理。您还可以启用“优化驱动器”服务。在 Windows 10 中,在控制面板中安排碎片整理。 只有 HDD 驱动器需要碎片整理;SSD 驱动器不建议启用碎片整理,因为这会导致磁盘磨损加速,显然出于这个原因,后台碎片整理默认处于禁用状态。

另一个已知的碎片整理选项是“将数据传输到另一个磁盘、格式化磁盘并将数据复制回来。在这种情况下,数据将被写入完全空的扇区,同时保持系统操作的正确逻辑结构。此选项在重置正常复制期间可能不会移动的潜在关键元数据时充满了问题。

禁用服务

使用 Mark Russinovich 的实用程序进程监视器您可以跟踪加载硬盘及其工作的进程,只需启用 IO 写入/读取列即可。研究完本专栏后,我通过控制面板服务面板禁用了 Xbox Game Bar 服务,这是众所周知的 Superfetch 程序的后台加速服务,名称为 SysMain。 Superfetch 必须不断分析用户使用的应用程序,并通过缓存到 RAM 来加速其启动;在我的例子中,这导致了整个磁盘的后台加载并且无法工作。

清理磁盘

我还删除了旧的应用程序和不必要的文件,从而释放了正确的碎片扇区,简化了操作系统的操作,减少了无用的、繁重的服务和程序的数量。

总计

什么最有帮助?对磁盘进行碎片整理后,性能出现了显着差异;通过禁用 Xbox 和 Superfetch 服务消除了自发冻结。如果我使用SSD就不会出现这些问题了吗?肯定不会出现由于碎片而导致运行缓慢的问题,无论如何都必须修复服务问题,并且软件错误不取决于驱动器的类型。在不久的将来,我计划完全过渡到 SSD,但现在“煎饼万岁,煎饼永远!”

链接

http://www.outsidethebox.ms/why-windows-8-defragments-your-ssd-and-how-you-can-avoid-this/
https://channel9.msdn.com/Shows/The-Defrag-Show
https://www.seagate.com/ru/ru/support/downloads/seatools/
https://www.ccleaner.com/defraggler/download
https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/chkdsk
https://gitlab.com/demensdeum/slowride/

用C++ FCGI编写后端服务器

关于我如何为 3D 编辑器 Cube Art Project 编写服务器部分的简要说明,服务器应该保存并显示 Web 版本用户的工作,使用保存按钮为他们提供短 URL。起初我想使用 Swift/PHP/Ruby/JS 或一些类似的现代语言作为后端,但在考虑了我的 VPS 的特点后,我决定用 C/C++ 来编写服务器。
首先,您需要在服务器上安装 libfcgi 以及 Web 服务器的 fcgi 支持模块,例如 Ubuntu 和 Apache:

sudo apt install libfcgi libapache2-mod-fcgid

接下来我们在config中配置模块:

FcgidMaxProcessesPerClass –每个类的最大进程数,我将其设置为 1 个进程,因为我不希望有很大的负载。
AddHandler fcgid-script .fcgi – fcgi 模块应启动的文件扩展名。
将启动 cgi 应用程序的文件夹添加到配置中:

接下来,我们用 C/C++ 编写一个支持 fcgi 的应用程序,对其进行汇编,并将其复制到 /var/www/html/cgi-bin 文件夹中。
代码和构建脚本示例:
https://gitlab.com/demensdeum/cube-art-project-server/-/blob/master/src/cubeArtProjectServer.cpp
https://gitlab.com/demensdeum/cube-art-project-server/-/blob/master/src/build.sh
此后您需要重新启动您的网络服务器:

systemctl restart apache2

接下来,输入必要的权限以通过 chmod 执行 cgi-bin 文件夹。
之后,您的 cgi 程序应该使用链接通过浏览器运行,例如 Cube Art Project 服务器:
http://192.243.103.70/cgi-bin/cubeArtProject/cubeArtProjectServer.fcgi
如果出现问题,请查看 Web 服务器日志,或使用调试器连接到正在运行的进程;调试过程不应与调试常规客户端应用程序的过程有所不同。

来源

https://habr.com/ru/post/154187/
http://chriswu.me/blog/writing-hello-world-in-fcgi-with-c-plus-plus/

源代码

https://gitlab.com/demensdeum/cube-art -项目服务器

将 C++ SDL 应用程序移植到 Android

在这篇文章中,我将描述我移植 3D 编辑器原型的经验 Android 上的 Cube Art Project
首先,让我们看看模拟器中运行的结果;带有红色 3D 立方体光标的编辑器:

为了成功组装,您必须执行以下操作:

  1. 安装最新的 Android SDK 和 NDK(NDK 版本越新越好)。
  2. 下载 SDL2 源代码,从那里获取模板来构建 Android 应用程序。
  3. 将 SDL Image、SDL Mixer 添加到程序集中。
  4. 添加我的游戏引擎和工具包的库及其依赖项(GLM、现代 C++ 的 JSON)
  5. 调整 Gradle 的程序集文件。
  6. 调整 C++ 代码以与 Android 兼容,更改受影响的平台相关组件(OpenGL ES、图形上下文初始化)
  7. 在模拟器上构建并测试项目。

项目模板

加载源SDL、SDL Image、SDL Mixer:
https://www.libsdl.org/download-2.0.php
docs 文件夹包含使用 android 项目模板的详细说明;将 android-project 目录复制到单独的文件夹,创建符号链接或将 SDL 文件夹复制到 android-project/app/jni。
我们用正确的标识符替换 avd 标志,从 Sdk:

目录启动 android 模拟器

cd ~/Android/Sdk/emulator
./emulator -avd Pixel_2_API_24

在脚本中指定路径,组装项目:

rm -rf app/build || true
export ANDROID_HOME=/home/demensdeum/Android/Sdk/
export ANDROID_NDK_HOME=/home/demensdeum/Android/android-ndk-r21-beta2/
./gradlew clean build
./gradlew installDebug

应使用文件中的 C 代码组装 SDL 项目模板

android-sdl-test-app/cube-art-project-android/app/jni/src/YourSourceHere.c

依赖项

下载SDL_image、SDL_mixer的源代码:
https://www.libsdl.org/projects/SDL_image/
https://www.libsdl.org/projects/SDL_mixer/

加载项目的依赖项,例如我的共享库:
https://gitlab.com/demensdeum/FlameSteelCore/
https://gitlab.com/demensdeum/FlameSteelCommonTraits
https://gitlab.com/demensdeum/FlameSteelBattleHorn
https://gitlab.com/demensdeum/FlameSteelEngineGameToolkit/
https://gitlab.com/demensdeum/FlameSteelEngineGameToolkitFSGL
https://gitlab.com/demensdeum/FSGL
https://gitlab.com/demensdeum/cube-art-project

我们将所有这些上传到 app/jni,每个“模块”放入一个单独的文件夹,例如 app/jni/FSGL。接下来,您可以选择查找 Application.mk 和 Android.mk 文件的工作生成器,我没有找到它们,但也许有一个基于 CMake 的简单解决方案。点击链接并开始熟悉 Android NDK 的程序集文件格式:
https://developer.android.com/ndk/guides/application_mk
https://developer.android.com/ndk/guides/android_mk

您还应该了解 NDK 中不同的 APP_STL 实现:
https://developer.android.com/ndk/guides/cpp-support.html

熟悉后,我们为每个“模块”创建一个 Android.mk 文件,然后是共享库 Cube-Art-Project 的示例汇编文件:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

APP_STL := c++_static
APP_CPPFLAGS := -fexceptions
LOCAL_MODULE := CubeArtProject
LOCAL_C_INCLUDES := $(LOCAL_PATH)/src $(LOCAL_PATH)/../include $(LOCAL_PATH)/../include/FlameSteelCommonTraits/src/FlameSteelCommonTraits
LOCAL_EXPORT_C_INCLUDES = $(LOCAL_PATH)/src/

define walk
$(wildcard $(1)) $(foreach e, $(wildcard $(1)/*), $(call walk, $(e)))
endef

ALLFILES = $(call walk, $(LOCAL_PATH)/src)
FILE_LIST := $(filter %.cpp, $(ALLFILES))
$(info CubeArtProject source code files list)
$(info $(FILE_LIST))
LOCAL_SRC_FILES := $(FILE_LIST:$(LOCAL_PATH)/%=%)

LOCAL_SHARED_LIBRARIES += FlameSteelCore
LOCAL_SHARED_LIBRARIES += FlameSteelBattleHorn
LOCAL_SHARED_LIBRARIES += FlameSteelCommonTraits
LOCAL_SHARED_LIBRARIES += FlameSteelEngineGameToolkit
LOCAL_SHARED_LIBRARIES += FlameSteelEngineGameToolkitFSGL
LOCAL_SHARED_LIBRARIES += FSGL
LOCAL_SHARED_LIBRARIES += SDL2
LOCAL_SHARED_LIBRARIES += SDL2_image

LOCAL_LDFLAGS := -static-libstdc++
include $(BUILD_SHARED_LIBRARY)

任何有经验的 CMake 用户都会从第一行开始理解这个配置,格式非常相似,Android.mk 缺少 GLOB_RECURSIVE,所以你必须使用 walk 函数递归搜索源文件。

我们更改 Application.mk、Android.mk 来构建 C++ 而不是 C 代码:

APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
APP_PLATFORM=android-16
APP_STL := c++_static
APP_CPPFLAGS := -fexceptions

重命名YourSourceHere.c -> YourSourceHere.cpp,grep条目,更改程序集中的路径,例如:

app/jni/src/Android.mk:LOCAL_SRC_FILES := YourSourceHere.cpp

接下来,尝试构建项目,如果编译器看到缺少标头的错误,请检查 Android.mk 中路径的正确性;如果链接器出现“未定义引用”等错误,请检查程序集中的源代码文件是否指定正确;可以通过在 Android.mk 文件中指定 $(info $(FILE_LIST)) 来跟踪列表。不要忘记双重链接机制,使用 LOCAL_SHARED_LIBRARIES 键中的模块并通过 LD 进行正确链接,例如 FSGL:

LOCAL_LDLIBS := -lEGL -lGLESv2

适应和启动

我必须改变一些东西,例如,从 iOS 和 Android 的构建中删除 GLEW,重命名一些 OpenGL 调用,添加 EOS 后缀(glGenVertexArrays -> glGenVertexArraysOES),包含用于缺少的现代调试功能的宏,锦上添花的是隐式包含指示宏的 GLES2 标头GL_GLEXT_PROTOTYPES 1:

#define GL_GLEXT_PROTOTYPES 1
#include "SDL_opengles2.h"

我还观察到第一次启动时出现黑屏,并出现类似“E/libEGL: validate_display:255 error 3008 (EGL_BAD_DISPLAY)”的错误,更改了 SDL 窗口的初始化、OpenGL 配置文件,一切正常:

SDL_DisplayMode mode;
SDL_GetDisplayMode(0,0,&mode);
int width = mode.w;
int height = mode.h;

window = SDL_CreateWindow(
            title,
            0,
            0,
            width,
            height,
            SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_RESIZABLE
        );

SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES );

在模拟器上,应用程序默认安装为 SDL 图标和名称“Game”。

我只需探索基于CMake自动生成程序集文件的可能性,或者将所有平台的程序集迁移到Gradle即可;然而,CMake 仍然是正在进行的 C++ 开发的事实上的选择。

源代码

https://gitlab.com/demensdeum/android- sdl-test-app
https://gitlab.com/demensdeum/android-sdl-test-app/tree/master/cube-art-project-android

来源

https://developer.android.com/ ndk/guides/cpp-support.html
https://developer.android.com/ndk/guides/application_mk
https://developer.android.com/ndk/guides/android_mk
https://lazyfoo.net/tutorials/SDL/52_hello_mobile/android_windows/index.php
https://medium.com/androiddevelopers/getting-started-with-c-and-android-native-activities-2213b402ffff

颠倒的世界

为了开发一个新项目,Cube Art Project 采用了测试驱动开发方法。在这种方法中,首先实现对应用程序的特定功能的测试,然后再实现该特定功能。我认为这种方法的一大优点是最终接口的实现,在功能开发开始之前尽可能不涉及实现细节。通过这种方法,当接口是特定实现的契约时,测试指示进一步的实现,添加契约编程的所有好处。
立方体艺术项目–一种 3D 编辑器,用户可以在其中从立方体构建图形;不久前,这种类型非常流行。由于这是一个图形应用程序,我决定添加带有屏幕截图验证的测试。
要验证屏幕截图,您需要从 OpenGL 上下文中获取它们,这是使用 glReadPixels 函数完成的。函数参数的描述很简单–起始位置、宽度、高度、格式(RGB/RGBA/等)、指向输出缓冲区的指针;任何使用过 SDL 或具有 C 数据缓冲区经验的人都可以简单地替换必要的参数。然而,我认为有必要描述一下 glReadPixels 输出缓冲区的一个有趣的功能;像素从下到上存储在其中,而在 SDL_Surface 中,所有基本操作都是从上到下发生的。
也就是说,从 png 文件加载参考屏幕截图后,我无法直接比较两个缓冲区,因为其中一个缓冲区是颠倒的。
要从 OpenGL 翻转输出缓冲区,您需要通过减去 Y 坐标的屏幕截图高度来填充它。但是,值得考虑的是,如果在填充时不减去它,则有可能超出缓冲区限制。导致内存损坏。
由于我总是尝试使用“通过接口编程”的 OOP 范例,而不是通过指针直接进行类似 C 的内存访问,因此当我尝试在缓冲区外部写入数据时,由于方法中的边界验证,对象会通知我这一点.
自上而下的截图方法最终代码:

    auto width = params->width;
    auto height = params->height;

    auto colorComponentsCount = 3;
    GLubyte *bytes = (GLubyte *)malloc(colorComponentsCount * width * height);
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, bytes);

    auto screenshot = make_shared(width, height);

    for (auto y = 0; y < height; y++) {
        for (auto x = 0; x < width; x++) {
            auto byteX = x * colorComponentsCount;
            auto byteIndex = byteX + (y * (width * colorComponentsCount));
            auto redColorByte = bytes[byteIndex];
            auto greenColorByte = bytes[byteIndex + 1];
            auto blueColorByte = bytes[byteIndex + 2];
            auto color = make_shared(redColorByte, greenColorByte, blueColorByte, 255);
            screenshot->setColorAtXY(color, x, height - y - 1);
        }
    }

    free(bytes);

来源

https://community.khronos.org/ t/glreadpixels-fliped-image/26561
https://stackoverflow.com/questions/8346115/why-are-bmps-stored-upside-down

源代码

https://gitlab.com/demensdeum/cube-艺术项目引导

最长公共子串

在这篇文章中,我将描述一种解决最大公共子串问题的算法。假设我们正在尝试解密加密的二进制数据,首先让我们尝试通过搜索最大的子字符串来找到常见模式。
输入字符串示例:
adasDATAHEADER??jpjjwerthhkjbcvkDATAHEADER??kkasdf
我们正在寻找重复两次的字符串:
数据头??

前缀

首先,我们编写一个方法来比较两个字符串的前缀,让它返回结果字符串,其中左前缀的字符与右前缀的字符相等。
例如,对于以下行:

        val lhs = "asdfWUKI"
        val rhs = "asdfIKUW"

结果字符串 –阿斯达夫
Kotlin 中的示例:

fun longestPrefix(lhs: String, rhs: String): String {
        val maximalLength = min(lhs.length-1, rhs.length -1)
        for (i in 0..maximalLength) {
            val xChar = lhs.take(i)
            val yChar = rhs.take(i)
                if (xChar != yChar) {
                    return lhs.substring(0, i-1)
                }
        }
        return lhs.substring(0,maximalLength)
}

暴力破解

当事情进展不顺利时,你应该诉诸暴力。使用longestPrefix方法,我们将在两个循环中遍历字符串,第一个循环从i到末尾,第二个从i + 1到末尾,将它们传递以搜索最大的前缀。该算法的时间复杂度约为O(n^2) ~ O(n*^3)。
Kotlin 中的示例:

fun searchLongestRepeatedSubstring(searchString: String): String {
        var longestRepeatedSubstring = ""
        for (x in 0..searchString.length-1) {
            val lhs = searchString.substring(x)
            for (y in x+1..searchString.length-1) {
                val rhs = searchString.substring(y)
                val longestPrefix = longestPrefix(lhs, rhs)
                if (longestRepeatedSubstring.length < longestPrefix.length) {
                    longestRepeatedSubstring = longestPrefix
                }
            }
        }
        return longestRepeatedSubstring
}

后缀数组

为了更优雅的解决方案,我们需要一个工具——一种名为“后缀数组”的数据结构。该数据结构是一个循环填充的子字符串数组,其中每个子字符串从该行的下一个字符开始到末尾。
例如,对于该行:

adasDATAHEADER??

后缀数组如下所示:

adasDATAHEADER??
dasDATAHEADER??
asDATAHEADER??
sDATAHEADER??
DATAHEADER??
ATAHEADER??
TAHEADER??
AHEADER??
HEADER??
EADER??
ADER??
DER??
ER??
R??
??
?

我们通过排序来解决

让我们对后缀数组进行排序,然后循环遍历当前元素在左手(左)、下一个在右手(右)的所有元素,并使用longestPrefix计算最长前缀方法。
Kotlin 中的示例:

fun searchLongestRepeatedSubstring(searchString: String): String {
    val suffixTree = suffixArray(searchString)
    val sortedSuffixTree = suffixTree.sorted()

    var longestRepeatedSubstring = ""
    for (i in 0..sortedSuffixTree.count() - 2) {
        val lhs = sortedSuffixTree[i]
        val rhs = sortedSuffixTree[i+1]
        val longestPrefix = longestPrefix(lhs, rhs)
        if (longestRepeatedSubstring.length < longestPrefix.length) {
            longestRepeatedSubstring = longestPrefix
        }
    }
    return longestRepeatedSubstring
}

该算法的时间复杂度为 O(N log N),这比直接解决方案要好得多。

来源

https://en.wikipedia.org/wiki/Longest_common_substring_problem

源代码

https://gitlab.com/demensdeum/algorithms

插入排序、归并排序

插入排序

插入排序–每个元素都会与列表中的前一个元素进行比较,并将元素与较大的元素(如果有)交换,否则内部比较循环停止。由于元素是从第一个到最后一个排序的,因此每个元素都会与已经排序的列表进行比较,这“可能”减少了总体运行时间。该算法的时间复杂度为O(n^2),即与气泡多样性相同。

合并排序

合并排序–该列表被分成一个元素的组,然后这些组被成对地“合并”并同时进行比较。在我的实现中,当合并对时,将左侧的元素与右侧的元素进行比较,然后移动到结果列表;如果左侧的元素消失,则将右侧的所有元素添加到结果列表中; list(不需要进行额外的比较,因为组中的所有元素都会经过排序迭代)< br />该算法的工作非常容易并行化;合并对的阶段可以在线程中执行,等待调度程序中的迭代结束。
单线程执行算法的输出:

["John", "Alice", "Mike", "#1", "Артем", "20", "60", "60", "DoubleTrouble"]
[["John"], ["Alice"], ["Mike"], ["#1"], ["Артем"], ["20"], ["60"], ["60"], ["DoubleTrouble"]]
[["Alice", "John"], ["#1", "Mike"], ["20", "Артем"], ["60", "60"], ["DoubleTrouble"]]
[["#1", "Alice", "John", "Mike"], ["20", "60", "60", "Артем"], ["DoubleTrouble"]]
[["#1", "20", "60", "60", "Alice", "John", "Mike", "Артем"], ["DoubleTrouble"]]
["#1", "20", "60", "60", "Alice", "DoubleTrouble", "John", "Mike", "Артем"]

多线程执行算法的输出:

["John", "Alice", "Mike", "#1", "Артем", "20", "60", "60", "DoubleTrouble"]
[["John"], ["Alice"], ["Mike"], ["#1"], ["Артем"], ["20"], ["60"], ["60"], ["DoubleTrouble"]]
[["20", "Артем"], ["Alice", "John"], ["60", "60"], ["#1", "Mike"], ["DoubleTrouble"]]
[["#1", "60", "60", "Mike"], ["20", "Alice", "John", "Артем"], ["DoubleTrouble"]]
[["DoubleTrouble"], ["#1", "20", "60", "60", "Alice", "John", "Mike", "Артем"]]
["#1", "20", "60", "60", "Alice", "DoubleTrouble", "John", "Mike", "Артем"]

算法时间复杂度为O(n*log(n)),略优于O(n^2)

来源

https://en.wikipedia.org/wiki/Insertion_sort
https://en.wikipedia.org/wiki/Merge_sort

源代码

https://gitlab.com/demensdeum /algorithms/-/tree/master/sortAlgorithms/insertionSort
https://gitlab.com/demensdeum/algorithms/-/tree/master/sortAlgorithms/mergeSort

Erlang 中的冒泡排序

冒泡排序非常无聊,但是如果您尝试用电信的函数式语言来实现它,它就会变得更有趣——埃尔兰。

我们有一个数字列表,我们需要对其进行排序。冒泡排序算法遍历整个列表,逐对迭代和比较数字。在检查过程中,会发生以下情况:将较小的数字添加到输出列表中,或者在当前列表中交换数字;如果右侧的数字较小,则继续搜索迭代中的下一个数字。重复此遍历,直到列表中不再有替换为止。

实际上,由于该算法的时间复杂度很高,因此不值得使用。 O(n^2);我在 Erlang 中以命令式的方式实现了它,但如果你有兴趣,你可以寻找更好的选择:

-module(bubbleSort).
-export([main/1]).

startBubbleSort([CurrentHead|Tail]) ->
    compareHeads(CurrentHead, Tail, [], [CurrentHead|Tail]).

compareHeads(CurrentHead, [NextHead|Tail], [], OriginalList) ->   
    if
        CurrentHead < NextHead ->
            compareHeads(NextHead, Tail, [CurrentHead], OriginalList);
        true ->
            compareHeads(CurrentHead, Tail, [NextHead], OriginalList)
    end;
    
compareHeads(CurrentHead, [NextHead|Tail], OriginalOutputList, OriginalList) ->
    if
        CurrentHead < NextHead ->
            OutputList = OriginalOutputList ++ [CurrentHead],
            compareHeads(NextHead, Tail, OutputList, OriginalList);
        true ->
            OutputList = OriginalOutputList ++ [NextHead],
            compareHeads(CurrentHead, Tail, OutputList, OriginalList)
    end;
  
compareHeads(CurrentHead, [], OriginalOutputList, OriginalList) ->
    OutputList = OriginalOutputList ++ [CurrentHead],
    if
        OriginalList == OutputList ->
            io:format("OutputList: ~w~n", [OutputList]);
        true ->
            startBubbleSort(OutputList)
    end.
  
main(_) ->
    UnsortedList = [69,7,4,44,2,9,10,6,26,1],
    startBubbleSort(UnsortedList).

安装和启动

在 Ubuntu 中,Erlang 的安装非常简单;只需在终端中输入 sudo apt install erlang 即可。在这种语言中,每个文件必须是一个模块,具有可以外部使用的函数列表——出口。该语言的有趣特征包括没有变量,只有常量,没有 OOP 标准语法(这并不妨碍 OOP 技术的使用),当然还有基于参与者模型的无锁并行计算。

您可以通过交互式 erl 控制台运行该模块,一个接一个地运行命令,或者更简单地通过 escript bubbleSort.erl 运行该模块;对于不同的情况,文件看起来会有所不同,例如,对于 escript,您需要创建一个主函数来启动它。

来源

https://www.erlang.org/
https://habr.com/ru/post/197364/

源代码

https://gitlab.com/ demensdeum/algorithms/blob/master/bubbleSort/bubbleSort.erl

字典序比较算法

字典字符串比较算法的工作原理非常简单;循环比较字符代码,如果字符不相等则返回结果。

C 语言的示例可以在这里找到:
https://github.com/gcc-mirror/gcc/blob/master/libiberty/memcmp.c

应该考虑到您需要比较单个静态编码中的字符,例如在 Swift 中我在 UTF-32 中使用了逐字符比较。使用 memcmp 的数组排序选项将完全适用于单字节字符,在其他情况下(可变长度编码),顺序可能不正确。我不排除基于变长编码实现的可能性,但很可能会复杂一个数量级。

该算法的时间复杂度在最好情况下为 O(1),在平均和最坏情况下为 O(n)

来源

https://ru.wikipedia.org/wiki/Lexicography_order

来源

https://gitlab.com/demensdeum /algorithms/blob/master/lexiCompare/lexiCompare.swift

使用 C 语言进行 ZX Spectrum 游戏开发

这篇废话帖子专门为老ZX Spectrum电脑用C语言开发一个游戏,来看看帅哥吧:

1982年开始生产,一直生产到1992年。该机的技术特点:8位Z80处理器,16-128kb内存和其他扩展,例如AY-3-8910声音芯片。

作为Yandex Retro Games Battle 2019竞赛的一部分,我为这台机器编写了一个游戏叫做Interceptor 2020。由于我没有时间学习Z80的汇编语言,所以我决定用C来开发它。作为工具链,我选择了一套现成的工具链—— z88dk,其中包含 C 编译器和许多辅助库,可加快 Spectrum 应用程序的执行速度。它还支持许多其他 Z80 机器,例如 MSX、德州仪器计算器。

接下来,我将描述我对计算机体系结构、z88dk 工具链的浅薄了解,并展示我如何设法实现 OOP 方法和使用设计模式。

安装功能

z88dk 的安装应该根据存储库中的手册进行,但是,对于 Ubuntu 用户,我想注意一个功能:如果您已经从 deb 包安装了 Z80 编译器,那么您应该删除它们,因为 z88dk 默认情况下将从 bin 文件夹访问它们;由于工具链编译器版本不兼容,您很可能无法编译任何内容。 /p>

你好世界

编写 Hello World 非常简单:

#include 

void main()
{
    printf("Hello World");
}

编译 Tap 文件更加容易:

zcc +zx -lndos -create-app -o helloworld helloworld.c

要运行,请使用任何支持 Tap 文件的 ZX Spectrum 模拟器,例如在线模拟器:
http://jsspeccy.zxdemo.org/

全屏在图片上绘图

tl;dr 图片以图块形式绘制,图块大小为 8×8 像素,图块本身内置于 Spectrum 字体中,然后将图片打印为索引中的一条线。

精灵和图块输出库 sp1 使用 UDG 输出图块。图片被转换为一组单独的 UDG(图块),然后使用索引在屏幕上组装。应该记住,UDG 用于显示文本,如果您的图片包含非常大的图块集(例如,超过 128 个图块),那么您将不得不超出该集的边界并删除默认的 Spectrum字体。为了解决这个限制,我使用了 128 – 的基数。 255,通过简化图像同时保留原始字体。关于简化下面的图片。

要绘制全屏图像,您需要使用三个实用程序来武装自己:
瘸子
img2spec
png2c-z88dk

真正的 ZX 男人、真正的复古战士有一种方法,那就是使用 Spectrum 调色板打开图形编辑器,了解图像输出的特征,手动准备并使用 png2c-z88dk 或 png2scr 上传。< /p>

更简单的方法–拍摄一张32位图像,在Gimp中将颜色数量切换为3-4,稍微编辑一下,然后将其导入到img2spec中,以免手动处理颜色限制,导出png并使用png2c-将其转换为C数组z88dk。

您应该记住,为了成功导出,每个图块不能包含超过两种颜色。

因此,您将收到一个包含唯一图块数量的 h 文件,如果超过 ~128 个,则在 Gimp 中简化图像(增加重复性)并在新图块上执行导出过程.

导出后,您可以从字面上加载图块中的“字体”,并将图块索引中的“文本”打印到屏幕上。下面是渲染“类”的示例:

// грузим шрифт в память
    unsigned char *pt = fullscreenImage->tiles;

    for (i = 0; i < fullscreenImage->tilesLength; i++, pt += 8) {
            sp1_TileEntry(fullscreenImage->tilesBase + i, pt);
    }

    // ставим курсор в 0,0
    sp1_SetPrintPos(&ps0, 0, 0);

    // печатаем строку
    sp1_PrintString(&ps0, fullscreenImage->ptiles);

在屏幕上绘制精灵

接下来我将描述一种在屏幕上绘制 16×16 像素精灵的方法。我没有开始动画和改变颜色,因为……正如我所假设的,在这个阶段我已经耗尽了内存,这是微不足道的。因此,游戏仅包含透明的单色精灵。

我们在Gimp中绘制一个单色png图像16×16,然后使用png2sp1sprite将其转换为asm汇编文件,在C代码中我们从汇编文件中声明数组,并在汇编阶段添加该文件。< /p>

在声明精灵资源阶段之后,必须将其添加到屏幕上所需的位置,下面是游戏对象“类”的代码示例:

    struct sp1_ss *bubble_sprite = sp1_CreateSpr(SP1_DRAW_MASK2LB, SP1_TYPE_2BYTE, 3, 0, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2,    SP1_TYPE_2BYTE, col2-col1, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2RB,  SP1_TYPE_2BYTE, 0, 0);
    sp1_IterateSprChar(bubble_sprite, initialiseColour);

从函数名称就可以大致明白–的含义。为精灵分配内存,添加两个 8×8 列,为精灵添加颜色。

每帧中都会指示精灵的位置:

sp1_MoveSprPix(gameObject->gameObjectSprite, Renderer_fullScreenRect, gameObject->sprite_col, gameObject->x, gameObject->y);

模拟 OOP

C 中没有 OOP 语法,如果你真的想要的话该怎么办?您需要连接您的思想并受到以下想法的启发:不存在 OOP 设备之类的东西;一切最终都属于一种机器架构,其中根本不存在对象的概念以及与之相关的其他抽象。 /p>

这个事实让我在很长一段时间里都无法理解为什么需要 OOP,如果最终一切都变成了机器代码,为什么有必要使用它。

但是,在从事产品开发之后,我发现了这种编程范式的好处,当然主要是开发灵活性、代码保护机制、采用正确的方法、减少熵、简化团队工作。所有上述好处都源于三大支柱——多态、封装、继承。

还值得注意的是解决与应用程序架构相关的问题的简化,因为 80% 的架构问题是在上个世纪由计算机科学家解决的,并在设计模式的文献中进行了描述。接下来,我将描述向 C 添加类似 OOP 的语法的方法。

以C结构体作为存储类实例数据的基础更为方便。当然,您可以使用字节缓冲区,为类、方法创建自己的结构,但为什么要重新发明轮子呢?毕竟,我们已经在重新发明语法。

类数据

游戏对象“类”数据字段示例:

struct GameObjectStruct {
    struct sp1_ss *gameObjectSprite;
    unsigned char *sprite_col;
    unsigned char x;
    unsigned char y;
    unsigned char referenceCount;
    unsigned char beforeHideX;
    unsigned char beforeHideY;
};
typedef struct GameObjectStruct GameObject;

将我们的类保存为“GameObject.h”,在正确的位置执行#include“GameObject.h”并使用它。

类方法

考虑到 Objective-C 语言开发人员的经验,类方法的签名将是全局范围内的函数,第一个参数始终是数据结构,后面是方法参数。下面是“类”GameObject 的“方法”示例:

void GameObject_hide(GameObject *gameObject) {
    gameObject->beforeHideX = gameObject->x;
    gameObject->beforeHideY = gameObject->y;
    gameObject->y = 200;
}

方法调用如下所示:

GameObject_hide(gameObject);

构造函数和析构函数的实现方式相同。可以将构造函数实现为分配器和字段初始值设定项,但我更喜欢单独控制对象分配和初始化。

使用内存

使用 new 和 delete 宏中包含的 malloc 和 free 来手动管理表单内存以匹配 C++:

#define new(X) (X*)malloc(sizeof(X))
#define delete(X) free(X)

对于同时被多个类使用的对象,半手动内存管理是基于引用计数实现的,类似于旧的 Objective-C Runtime ARC 机制:

void GameObject_retain(GameObject *gameObject) {
    gameObject->referenceCount++;
}

void GameObject_release(GameObject *gameObject) {
    gameObject->referenceCount--;

    if (gameObject->referenceCount < 1) { sp1_MoveSprAbs(gameObject->gameObjectSprite, &Renderer_fullScreenRect, NULL, 0, 34, 0, 0);
        sp1_DeleteSpr(gameObject->gameObjectSprite);
        delete(gameObject);
    }
}

因此,每个类必须使用retain声明共享对象的使用,通过release释放所有权。现代版本的 ARC 使用自动保留/释放调用。

声音!

Spectrum 有一个能够再现 1 位音乐的高音扬声器;当时的作曲家能够同时再现多达 4 个声道。

Spectrum 128k包含一个独立的声音芯片AY-3-8910,可以播放跟踪器音乐。

提供了一个用于在 z88dk 中使用高音扬声器的库

还有什么需要学习

我有兴趣熟悉 Spectrum、使用 z88dk 实现游戏并学习很多有趣的东西。我还有很多东西需要学习,例如 Z80 汇编器,因为它使我能够充分利用 Spectrum 的功能、使用内存库以及使用 AY-3-8910 声音芯片。希望明年还能参加比赛!

链接

https://rgb.yandex
https://vk.com/sinc_lair
https://www.z88dk.org/forum/

源代码

https://gitlab.com/demensdeum/ zx-projects/tree/master/interceptor2020

二分查找

假设我们需要查明电子邮件地址“demensdeum@gmail.com”是否包含在允许接收信件的电子邮件地址列表中.

让我们从第一个元素到最后一个元素遍历整个列表,检查该元素是否等于指定的地址–让我们实现一个线性搜索算法。但这需要很长时间,不是吗?

要回答这个问题,请使用“算法的时间复杂度”,“O”符号。最坏情况下线性搜索的操作时间等于数组元素的第n个数量,我们用“O”符号来写它–在)。接下来,我们需要解释一下,对于任何已知的算法,都存在三个性能指标——最好情况、最坏情况和平均情况的执行时间。例如,邮件地址“demensdeum@gmail.com”位于数组的第一个索引中,那么就会在第一步中找到它根据该算法,执行时间至多是“最佳”。 O(1);如果在列表的末尾,那么这是最坏的情况– O(n)

但是软件实现的细节,硬件性能,它们应该影响大O吗?现在吸一口气,想象一下时间复杂度的计算是针对某种抽象的理想机计算的,其中只有这个算法,没有别的。

算法

好吧,事实证明线性搜索相当慢,让我们尝试使用二分搜索。首先,应该澄清的是,我们不会使用二进制数据;由于其工作的特殊性,因此给该方法起了这个名字。最初我们将数组排序为 字典顺序,然后算法取整个数组的范围,获取范围的中间元素,比较按字典顺序,并根据比较的结果,决定使用哪个范围来进一步搜索–当前的上半部分或下半部分。也就是说,在每个搜索步骤中,都会从两个可能的“结果”中做出决定。二元逻辑。重复此步骤,直到找到或未找到该单词(出现范围的下索引和上索引的交集)。

该算法的性能–最好的情况是立即在数组中间找到一个元素 O(1),枚举的最坏情况是 O(log n)

陷阱

在实现二分查找时,我不仅遇到了编程语言库中字典比较缺乏标准化这个有趣的问题,甚至还发现缺乏统一的实现标准JavaScript 内的 localeCompare。 ECMAScript 标准允许该函数有不同的实现,这就是为什么当使用 localeCompare 进行排序时,可以在不同的 JavaScript 引擎上观察到完全不同的结果。

因此,为了使算法正确工作,有必要排序并仅使用相同的字典序比较算法,否则什么都不起作用。但是,例如,如果您尝试在 Scala 中对数组进行排序,并使用 Nodejs 进行搜索,而不实现您自己的排序/排序,那么除了对人性的失望之外,什么也没有等待您。

来源

<一href="https://ru.stackoverflow.com/questions/489888/%D0%A7%D1%82%D0%BE-%D1%82%D0%B0%D0%BA%D0%BE%D0%B5 -% D0%BB%D0%B5%D0%BA%D1%81%D0%B8%D0%BA%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1% 87%D0%B5%D1%81%D0%BA%D0%BE%D0 %B5-%D1%81%D1%80%D0%B0%D0%B2%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B8-%D1%87% D1%82%D0%BE-%D0%BE%D0%BD%D0%BE- %D1%81%D0%BE%D0%B1%D0%BE%D0%B9-%D0%BF%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B0% D0%B2%D0%BB%D1%8F%D0%B5%D1%82" target="_blank" rel="noopener">什么是词典比较,它代表什么?
Почему для вычисления сложности алгоритмов используется log N вместо lb N?
Двоичный поиск
Знай сложности алгоритмов
https://stackoverflow.com/questions/52941016/sorting-in-localecompare-in-javascript

源代码

https://gitlab.com/demensdeum/algorithms

图案立面


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 FactoryAbstract 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 公司。洪都拉斯领先的咖啡机软件供应商。他们希望销售增强现实眼镜,能够冲泡咖啡、检查水/咖啡豆是否已满,并使用室内地图模式显示前往最近咖啡机的路线。

他们负责软件的开发;我们只需提供有关CreatorProduct界面的文档,以便正确显示应用程序列表及其进一步内容发射。

传输文档后,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 ProductsConcrete 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 是“立即更新!”按钮,它启动特定命令使用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

为 Ubuntu OSXCross CMake 构建 macOS 应用程序

在这篇文章中,我将描述如何使用 CMake 和 osxcross 在 Ubuntu 构建机器上为 macOS 构建跨平台 C++ 应用程序。
首先,安装osxcross工具链:
https://github.com/tpoechtrager/osxcross
安装分 3 个阶段进行,下载依赖项:

cd tools
./get_dependencies.sh

从Apple官方网站下载XCode.xip,然后从XCode下载SDK:

./gen_sdk_package_pbzx.sh /media/demensdeum/2CE62A79E62A4404/LinuxSupportStorage/xcode111.xip

我希望您阅读最后一步中的 XCode 许可协议?接下来,使用所需的前缀构建工具链:

INSTALLPREFIX=/home/demensdeum/Apps/osxcross ./build.sh 

现在您可以使用上一步的前缀目录中的 osxcross。让我们为 CMake 添加一个新的构建宏,编写所有必要的内容:

if (OSXCROSS)
SET(CMAKE_SYSTEM_NAME Darwin)
SET(CMAKE_C_COMPILER o64-clang)
SET(CMAKE_CXX_COMPILER o64-clang++)
SET(CMAKE_C_COMPILER_AR x86_64-apple-darwin19-ar)
SET(CMAKE_CXX_COMPILER_AR x86_64-apple-darwin19-ar)
SET(CMAKE_LINKER x86_64-apple-darwin19-ld)
SET(ENV{OSXCROSS_MP_INC} 1)
endif()

动态链接对我来说并不成功,因此我们静态导出库:

if (OSXCROSS)
add_library(FlameSteelCore STATIC ${SOURCE_FILES})
else()

接下来,您可能会遇到这样的情况:您没有 osxcross 所需的库,我在使用 SDL2 时遇到过这种情况。 osxcross 支持现成的库包 – macports。例如,安装SDL2-mixer:

osxcross-macports -v install libsdl2_mixer

此后,您可以像平常一样在 cmake-make 链接中开始构建库/应用程序,如果需要,请不要忘记指定库的静态链接。

手动组装库

目前,我在静态链接期间遇到了库归档不正确的问题;在构建最终应用程序时,我收到错误:

file was built for archive which is not the architecture being linked (x86_64)

这张票非常相似,我们设法实现了解决方法可导致装配正确完成。让我们解压缩静态库并使用 osxcross 归档程序重新构建它:

ar x ../libFlameSteelCore.a
rm ../libFlameSteelCore.a
x86_64-apple-darwin19-ar rcs ../libFlameSteelCore.a *.o

我个人也认为问题之一是缺乏直接在 Ubuntu 上运行 macOS 应用程序的能力(至少具有某些功能),当然有一个项目 亲爱的,但支持仍然有很多不足之处。

来源

https://github.com/tpoechtrager/osxcross

在 Ubuntu MinGW CMake 下构建 Windows

在这篇文章中,我将描述在 Ubuntu 上使用 MinGW32 工具链为 Windows 构建库和应用程序的过程。
安装wine、mingw:

sudo apt-get install wine mingw-w64

此后,您就可以为 Windows 构建 C/C++ 应用程序了:

# C
i686-w64-mingw32-gcc helloWorld.c -o helloWorld32.exe      # 32-bit
x86_64-w64-mingw32-gcc helloWorld.c -o helloWorld64.exe    # 64-bit
 
# C++
i686-w64-mingw32-g++ helloWorld.cc -o helloWorld32.exe     # 32-bit
x86_64-w64-mingw32-g++ helloWorld.cc -o helloWorld64.exe   # 64-bit

可以使用wine检查收集的exe。

接下来,让我们看看对 CMake 构建、CMakeLists.txt 文件的更改,将 MinGW 特定的内容添加到构建文件中:

if (MINGW32)
set(CMAKE_SYSTEM_NAME Windows)
SET(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
SET(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
SET(CMAKE_RC_COMPILER i686-w64-mingw32-windres)
set(CMAKE_RANLIB i686-w64-mingw32-ranlib)
endif()

// для сборки shared dll
elseif (MINGW32)
add_library(FlameSteelEngineGameToolkit.dll SHARED ${SOURCE_FILES})
else()

// обязательно линкуем со всеми зависимостями
if (MINGW32)
target_link_libraries(
                        FlameSteelEngineGameToolkit.dll 
                        -static-libgcc
                        -static-libstdc++
                        SDL2 
                        SDL2_mixer 
                        /home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelCore/FlameSteelCore.dll
                        /home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelBattleHorn/FlameSteelBattleHorn.dll
                        /home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelCommonTraits/FlameSteelCommonTraits.dll)

set_target_properties(FlameSteelEngineGameToolkit.dll PROPERTIES
        PREFIX ""
        SUFFIX ""
        LINK_FLAGS "-Wl,--add-stdcall-alias"
        POSITION_INDEPENDENT_CODE 0 # this is to avoid MinGW warning; 
        # MinGW generates position-independent-code for DLL by default
)
else()

收集:

cmake -DMINGW32=1 .
make

输出将是 dll 或 exe,具体取决于您要收集的内容。对于一个工作示例,您可以查看新的 Cube-Art-Project 及其库的存储库:
https://gitlab.com/demensdeum/cube-art-project
https://gitlab.com/demensdeum/FlameSteelEngineGameToolkitFSGL
https://gitlab.com/demensdeum/cube-art-project-bootstrap

来源
https://arrayfire.com/cross-compile-to-windows-from-linux/

ChromeDriver 的简单 Emscripten 自动测试

在这篇笔记中,我将描述为 Chrome 浏览器的 ChromeDriver 运行自动测试的实现,它运行使用 Emscripten 从 C++ 翻译而来的模块自动测试,读取控制台输出并返回测试结果。
首先你需要安装selenium,对于Python 3-Ubuntu,这是这样完成的:

pip3 install selenium

接下来,从官网下载ChromeDriver,例如将chromedriver放在/usr/local/bin下,然后就可以开始实现自动测试了。
下面我将给出自动测试代码,该代码启动 Chrome 浏览器,并在 Emscripten 上打开自动测试页面,检查是否存在文本“Window test successed”:

import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

capabilities = DesiredCapabilities.CHROME
capabilities['goog:loggingPrefs'] = { 'browser':'ALL' }
driver = webdriver.Chrome()
driver.get("http://localhost/windowInitializeTest/indexFullscreen.html")

time.sleep(2)

exitCode = 1

for entry in driver.get_log('browser'):
    if entry["source"] == "console-api":
        message = entry["message"]
        if "Window test succeded" in message:
            print("Test succeded")
            exitCode = 0

driver.close()
exit(exitCode)

将测试保存为 main.py 并运行 python3 main.py

构建一个具有 Emscripten 依赖项的项目

在这篇文章中,我将描述使用 Emscripten 构建一个由多个库组成的项目。
目前,Emscripten 不支持构建共享库,因此第一步是将所有库从 Shared 转移到 Static。 Emscripten 使用自己的包含文件,因此需要通过将符号链接从系统目录转发到 Emscripten 工具链来解决头文件的可见性问题:

ln -s /usr/local/include/FlameSteelFramework $EMSDK/fastcomp/emscripten/system/include/FlameSteelFramework

如果您使用的是 CMake,则需要在 add_library 方法的 CMakeLists.txt 文件中更改 SHARED->STATIC。您可以使用以下命令构建库/应用程序以进行进一步的静态链接:

emcmake cmake .
emmake make

接下来,您需要构建主应用程序,并在链接阶段指定 *.a 库文件。我无法指定相对路径;只有在 CMakeLists.txt 文件中指定完整路径后,构建才能正确完成:

elseif(EMSCRIPTEN)
target_link_libraries(${FSEGT_PROJECT_NAME} GL GLEW 
/home/demensdeum/Sources/cube-art-project-bootstrap/cube-art-project/sharedLib/libCubeArtProject.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelEngineGameToolkitFSGL/libFlameSteelEngineGameToolkitFSGL.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelEngineGameToolkit/libFlameSteelEngineGameToolkit.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelCore/libFlameSteelCore.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelBattleHorn/libFlameSteelBattleHorn.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FSGL/libFSGL.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelCommonTraits/libFlameSteelCommonTraits.a)
else()

来源

https://emscripten.org/文档/compiling/Building-Projects.html#using-libraries

共享库 CMake C++

我最近决定将 FlameSteelFramework 的所有部分设为单独的共享库,然后我将展示 FlameSteelCore

cmake_minimum_required(VERSION 3.5)

project (FlameSteelCore)
set(CMAKE_BUILD_TYPE Release)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)

file(GLOB_RECURSE SOURCE_FILES
    "src/FlameSteelCore/*.cpp"
)

add_library(FlameSteelCore SHARED ${SOURCE_FILES})

install(DIRECTORY "${CMAKE_SOURCE_DIR}/src/FlameSteelCore"
        DESTINATION include/FlameSteelFramework
        FILES_MATCHING
        PATTERN "*.h"
)

install(TARGETS FlameSteelCore DESTINATION lib)

CMake 执行的命令:将 src/FlameSteelCore/ 目录中所有带有 *.cpp 扩展名的文件收集到共享库中,将 src/FlameSteelCore 中带有 *.h 扩展名的所有标头复制到 include/FlameSteelFramework (在我的例子中)这是/usr/local/include/FlameSteelFramework),将共享lib复制到lib目录(/usr/local/lib)
安装后,可能需要更新LD缓存– sudo ldconfig。
要在 Ubuntu 上构建并安装(如果您有正确的构建工具链),只需运行以下命令:

cmake . && make && sudo make install

为了测试安装过程,我将 make 前缀传递到本地文件夹 makeInstallTestPlayground:

cmake -DCMAKE_INSTALL_PREFIX:PATH=/home/demensdeum/makeInstallTestPlayground . && make && make install

参考文献

https: //stackoverflow.com/questions/17511496/how-to-create-a-shared-library-with-cmake
https://stackoverflow.com/questions/6003374/what-is-cmake-equivalent-of-configure-prefix-dir-make-all-install

C++语言解释器–紧贴

不久前,我遇到了一个有趣的项目,名为Cling,它是一个 C++ 语言解释器,可以从控制台交互工作,等等。您可以通过以下链接查看该项目: https://github.com/root -项目/坚持
Ubuntu 的安装很简单 –下载所需版本的存档,解压它,转到 bin 文件夹并在终端中运行 cling。
下面是加载库 FlameSteelCore、初始化对象、打印 id 的示例:

丢失 Emscripten 异常和正则表达式问题

丢失异常

Emscripten 的一个有趣功能:当通过 emscripten_set_main_loop 启动游戏循环时,您应该记住必须在循环方法中直接通过 try catch 重新添加异常处理,因为运行时丢失来自外部的 try catch 块。
最简单的方法是使用 javascript 警报在浏览器中显示错误文本:

            catch (const std::exception &exc)
            {
                const char *errorText = exc.what();
                cout << "Exception: " << errorText << "; Stop execution" << endl;

                EM_ASM_(
                {
                    var errorText = UTF8ToString($0);
                    alert(errorText);

                }, errorText);

                abort();

正则表达式太复杂

如果 regex 的 std 实现认为正则表达式过于复杂,则可能会抛出 error_complexity 异常。目前emscripten的实现中就出现了这种情况,所以我建议您通过正则表达式来实现解析测试,或者使用第三方正则表达式实现。

模式生成器

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”。
假设我们正在开发一个应用程序 –相册。用户可以创建文件夹、在其中添加照片以及执行其他操作。您肯定需要能够显示文件夹中的文件数量、所有文件和文件夹的总数。
显然需要使用树,但是如何以简单方便的接口实现树架构呢?复合模式来救援。

希拉在月鸭

接下来在目录中我们实现 dataCount() 方法 –通过遍历组件数组中的所有元素,将它们的所有 dataCount 相加。
一切准备就绪!
下面是 Go 中的示例:
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

观察者模式

观察者模式是指行为设计模式。
该模式允许您使用通用接口将对象状态的更改发送给订阅者。
假设我们正在为程序员开发一个信使,我们在应用程序中有一个聊天屏幕。当您收到包含“问题”和“错误”或“出现问题”文本的消息时,您需要将错误列表屏幕和设置屏幕设置为红色。
接下来,我将描述解决该问题的两种选择,第一种很简单,但极难支持,第二种支持更稳定,但需要你在最初的实施过程中转过头来。

公共巴士

该模式的所有实现都包含数据更改时发送消息、订阅消息以及方法中的进一步处理。共享总线选项包含一个将消息分派给接收者的单个对象(通常是单例)。
实现的简单性如下:

  1. 对象向共享总线发送抽象消息
  2. 订阅共享总线的另一个对象捕获该消息并决定是否处理它。

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 模式,因为这通常会导致代码可读性和相关支持问题不必要的复杂性——凡事都要有个度。

MEAACT PHOTO / STUART PRICE.

旗帜之王

假设我们正在为民用飞机的媒体系统开发视频播放器屏幕,播放器必须能够加载视频流、播放视频、允许用户停止下载过程、倒带以及执行其他通常的操作一名玩家。
假设播放器缓存了视频流的下一个块,检查是否有足够的块用于播放,开始向用户播放片段,同时继续下载下一个。
此时,用户快退到视频的中间,即现在需要停止播放当前片段并从新位置开始加载。然而,在某些情况下这是无法做到的——用户在观看有关航空安全的视频时无法控制视频流的播放。让我们检查 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