避免在 Genie 中使用全局变量

Avoiding global variables in Genie

下面是 Genie 中 ToolbarButton 的工作代码。 objective 是获取所选文件的 uri,return 返回到 class 的 construct/init。问题是在我遇到的所有示例中都使用了全局变量(如下面的代码所示)。它看起来不直观,我担心每当代码变大时,消除错误就会变得更加困难,因为这些变量会开始累积。是否有任何其他方法可以使函数 openfile return 成为 class 的 construct/init 中的常规变量的 uri?

代码如下:

uses
    Granite.Widgets
    Gtk

init
    Gtk.init (ref args)

    var app = new Application ()
    app.show_all ()
    Gtk.main ()

// This class holds all the elements from the GUI
class Application : Gtk.Window

    _view:Gtk.TextView
    _uri:string

    construct ()

        // Prepare Gtk.Window:
        this.window_position = Gtk.WindowPosition.CENTER
        this.destroy.connect (Gtk.main_quit)
        this.set_default_size (400, 400)


        // Headerbar definition
        headerbar:Gtk.HeaderBar = new Gtk.HeaderBar()
        headerbar.show_close_button = true
        headerbar.set_title("My text editor")

        // Headerbar buttons
        open_button:Gtk.ToolButton = new ToolButton.from_stock(Stock.OPEN)
        open_button.clicked.connect (openfile)

        // Add everything to the toolbar
        headerbar.pack_start (open_button)
        show_all ()
        this.set_titlebar(headerbar)

        // Box:
        box:Gtk.Box = new Gtk.Box (Gtk.Orientation.VERTICAL, 1)
        this.add (box)

        // A ScrolledWindow:
        scrolled:Gtk.ScrolledWindow = new Gtk.ScrolledWindow (null, null)
        box.pack_start (scrolled, true, true, 0)

        // The TextView:
        _view = new Gtk.TextView ()
        _view.set_wrap_mode (Gtk.WrapMode.WORD)
        _view.buffer.text = "Lorem Ipsum"
        scrolled.add (_view)

    def openfile (self:ToolButton)

        var dialog = new FileChooserDialog ("Open file",
                                        this,
                                        FileChooserAction.OPEN,
                                        Stock.OK,     ResponseType.ACCEPT,
                                        Stock.CANCEL, ResponseType.CANCEL)
        //filter.add_pixbuf_formats ()
        //dialog.add_filter (filter)

        case dialog.run()
            when ResponseType.ACCEPT
                var filename = dialog.get_filename()
                //image.set_from_file(filename)

        if (dialog.run () == Gtk.ResponseType.ACCEPT)
            _uri = dialog.get_uri ()
            stdout.printf ("Selection:\n %s", _uri)

        dialog.destroy ()

或者我根本不应该担心 _variables 的累积吗?

首先是术语说明,然后是概括。

A "global variable" 可以在程序的任何地方访问,所以它的作用域是全局的。您在问题中提到的 _variables 是 object 范围内的私有字段。它们只能通过 object 中定义的代码访问。但是,您对 object 中私有工作变量的累积感到担忧是正确的。

设计 object 很难做到,技术和想法在几十年的实践和研究中不断发展。 SOLID acronym, introduced by Michael Feathers, sums up five principles for object oriented design that provide useful criteria for evaluating your design. Also the book, Design Patterns: Elements of Reusable Object-Oriented Software, by Gamma et al. and first published in 1994, provides a good summary and categorisation of designs in object oriented programming. That book uses a document editor as a case study for demonstrating the use of such patterns. Both the SOLID principles and the design patterns in the book are abstractions, they won't tell you how to write a program but they do give a set of common ideas that allows programmers to discuss and evaluate. So I will use both of those tools in my answer, but be aware in recent years additional techniques have been developed to further enhance the software development process, specifically test driven development and behaviour driven development.

SOLID 中的 S 代表 Single Responsibility Principle 并且是查看示例的良好起点。通过调用您的 object、Application 并将私有工作变量视为全局变量,这表明您在单个 object 中编写整个应用程序。您可以做的是开始将 Application 分成许多不同的 object,这些 object 更专注于单一的责任领域。首先,虽然我认为我会重命名 Application object。我去了 EditorWindow。在我下面的示例中,EditorWindow 也有一个 Header 和一个 DocumentView

使用以下代码编译以下代码:

valac -X -DGETTEXT_PACKAGE --pkg gtk+-3.0 text_editor_example.gs

-X -DGETTEXT_PACKAGE 的用法在本回答的末尾进行了解释。

[indent=4]
uses
    Gtk

init
    Intl.setlocale()
    Gtk.init( ref args )

    var document = new Text( "Lorem Ipsum" )

    var header = new Header( "My text editor" )
    var body = new DocumentView( document )
    var editor = new EditorWindow( header, body )

    var document_selector = new DocumentFileSelector( editor )
    var load_new_content_command = new Load( document, document_selector )
    header.add_item( new OpenButton( load_new_content_command ) )

    editor.show_all()
    Gtk.main()

class EditorWindow:Window
    construct( header:Header, body:DocumentView )
        this.window_position = WindowPosition.CENTER
        this.set_default_size( 400, 400 )
        this.destroy.connect( Gtk.main_quit )

        this.set_titlebar( header )

        var box = new Box( Gtk.Orientation.VERTICAL, 1 )
        box.pack_start( body, true, true, 0 )
        this.add( box )

class Header:HeaderBar
    construct( title:string = "" )
        this.show_close_button = true
        this.set_title( title )

    def add_item( item:Widget )
        this.pack_start( item )

class OpenButton:ToolButton
    construct( command:Command )
        this.icon_widget = new Image.from_icon_name(
                                                 "document-open",
                                                 IconSize.SMALL_TOOLBAR
                                                 )
        this.clicked.connect( command.execute )

class DocumentView:ScrolledWindow
    construct( document:TextBuffer )
        var view = new TextView.with_buffer( document )
        view.set_wrap_mode( Gtk.WrapMode.WORD )
        this.add( view )

interface Command:Object
    def abstract execute()

interface DocumentSelector:Object
    def abstract select():bool
    def abstract get_document():string

class Text:TextBuffer
    construct ( initial:string = "" )
        this.text = initial

class DocumentFileSelector:Object implements DocumentSelector

    _parent:Window
    _uri:string = ""

    construct( parent:Window )
        _parent = parent

    def select():bool
        var dialog = new FileChooserDialog( "Open file",
                                            _parent,
                                            FileChooserAction.OPEN,
                                            dgettext( "gtk30", "_OK"),
                                            ResponseType.ACCEPT,
                                            dgettext( "gtk30", "_Cancel" ),
                                            ResponseType.CANCEL
                                           )

        selected:bool = false
        var response = dialog.run()
        case response
            when ResponseType.ACCEPT
                _uri = dialog.get_uri()
                selected = true

        dialog.destroy()
        return selected

    def get_document():string
        return "Reading the text from a URI is not implemented\n%s".printf(_uri)

class Load:Object implements Command

    _receiver:TextBuffer
    _document_selector:DocumentSelector

    construct( receiver:TextBuffer, document_selector:DocumentSelector )
        _receiver = receiver
        _document_selector = document_selector

    def execute()
        if _document_selector.select()
            _receiver.text = _document_selector.get_document()

图形用户界面的常见 high-level 模式是 model-view-controller (MVC)。这是关于 de-coupling 你的 object 的,所以它们可以很容易地 re-used 和改变。在示例中 document 已成为代表模型的 object。通过将其设为单独的 object,它允许为同一数据提供多个视图。例如,在编写 Whosebug 问题时,您有一个编辑器 window,还有一个 pre-view。两者都是对同一数据的不同看法。

在示例中,header 工具栏已使用 command pattern 进一步分为不同的 object。工具栏中的每个按钮都有一个关联的命令。通过将命令作为单独的 objects,命令可以是 re-used。例如键绑定 Ctrl-O 也可以使用 Load 命令。这样,附加到打开文档按钮的命令代码不需要 re-written 即可将其附加到 Ctrl-O.

命令模式使用接口。只要 object 实现了 execute() 方法,它就可以用作命令。 Load 命令还使用 object 的接口,询问用户打开哪个 URI。 Gtk+ 还提供了一个FileChooserNative。因此,如果您想切换到使用 FileChooserNative 对话框而不是 FileChooserDialog,您只需要编写一个新的 object 来实现 DocumentSelector 接口并将其传递给Load 命令代替。 de-coupling object 以这种方式使您的程序更加灵活,并且私有字段的使用仅限于每个 object.

附带说明一下,在编译您的示例时出现了一些警告:warning: Gtk.Stock has been deprecated since 3.10。此答案中的示例使用较新的方式:

  • 打开文档图标the GNOME developer documentation for Stock Items states "Use named icon "document-open" or the label "_Open"." So I've used document-open. These names are from the freedesktop.org Icon Naming Specification
  • 对于文件选择器对话框中的“确定”按钮,GNOME Developer documentation 表示 "Do not use an icon. Use label "_OK“。”前面的下划线表示它被gettext国际化和翻译。 gettext 使用 'domains' 是翻译文件。对于 GTK+3,该域称为 gtk30。要在编译程序时启用 gettext,需要将默认域的宏传递给 C 编译器。这就是为什么需要 -X -DGETTEXT_PACKAGE 的原因。同样在 Genie 程序中,需要 Intl.setlocale() 将语言环境设置为 运行 时间环境。当使用 LC_ALL="zh_CN" ./text_editor_example 到 运行 之类的东西完成此操作时,如果您安装了该语言环境,您的程序将以中文显示“确定”按钮