Blogs

重要的编程概念(即使在嵌入式系统上)第四部分:单身人士

杰森萨赫斯 2014年11月11日 2评论

本系列的其他文章:

今天’s topic is the 单身 。本文是独一无二的(双关语),因为与本系列中的其他人不同,我试图弄清楚要使用的一句话,这将是巴黎人送28元积极的概念来鼓励,作为单例的替代品,但是 看起来真的很棒’t one。我最接近的是 重新入口 或者 模块化 ,但这些都不是相同的主题。

无论如何,一世’我将以几种不同的方式覆盖单身。

单身,设计模式和gof

1994年回来,巴黎人送28元叫做有影响力的书 设计模式 是由埃里希伽玛,理查德·赫尔姆,拉尔夫约翰逊和约翰·韦斯德(John Vlissides)撰写的,被称为四个(GOF)。当时,java,ruby和javascript didn’T还存在,面向对象的编程(OOP)是几年的流行语,C ++在上升,微软推动了重新打包为COM的OLE,以及 标准模板库 是C ++标准的新部分。设计模式是一本充满食谱的醒目书,如何在OOP中使用各种常见图案—不仅仅是一种具有噪声的方法的动物的成绩层次,而且是如何使用OOP的新鲜看法。对象Weren.’只需与它们一起存储应用程序数据和耦合方法的方法。他们也是方法 脱钩 软件设计的各个方面,彼此真正正交—你必须弄清楚如何寻找那些正交性。 (当然,它有 图片 !! UML图!我们了解到了’如果你认真对待OOP,你画了什么。)

在任何情况下,其中巴黎人送28元设计模式被称为 单身 。 Singleton(注意大小化)是软件应用程序中的有意唯一的对象;通过设计,该软件仅允许巴黎人送28元实例存在。设计模式书籍,以与其描述所有各种模式的方法,解释了单例模式,包括其特征和整体讨论在C ++中实现巴黎人送28元的各种方式。根据GOF的情况,单身模式的动机如下:

It’对于某些课程来说,对于有巴黎人送28元实例来说很重要。虽然系统中可以有许多打印机,但应该只有巴黎人送28元打印机假脱机程序。应该只有巴黎人送28元文件系统和巴黎人送28元窗口管理器。数字滤波器将有巴黎人送28元A / D转换器。会计系统将致力于为一家公司服务。

我们如何确保巴黎人送28元类只有巴黎人送28元实例,并且实例很容易访问?全局变量使对象可访问,但它不可访问’t让您免于实例化多个对象。

更好的解决方案是使班级本身负责跟踪其唯一的实例。该类可以确保无法创建其他实例(通过拦截请求来创建新对象),并且它可以提供访问实例的方法。这是单身模式。

所以现在,如果你在软件工程背景下提到单词单例,那么与设计模式书有巴黎人送28元持久的关联。

另一方面,我的方法将是一般的单身人士(注意缺乏资本化)。除了单身图案—这具体是指管理自己实例并阻止创建附加实例的OOP类—在编程和软件工程中有一些唯一对象的其他方面。

单身作为实际的概念

关于单身图案的所有这些喧哗都倾向于被困在OOP说话中。实例,工厂,构造函数,亚类,Blah Blah Blah。

如果你’VE一直在C而不是更高级别的语言编程,您可能更熟悉这个术语 全局变量,这是单身的巴黎人送28元例子。两者都有否定的内涵,我’请谈论为什么。但是,基本的想法是单身是模块化杀手。模块化通常是好的,因此单身普遍不错。

对单身人士的讨论有点不同于高水平和低级别语言。一世’我将首先让高级的东西脱离;那些对嵌入式系统感兴趣的人应该是患者(或跳过下一节,如果必须)。

以高级语言的单身人士

I’我将在这里使用Java作为巴黎人送28元例子,因为我’m更熟悉它而不是其他语言。 (本节中讨论的想法也在C ++中有效,在某种程度上。)在Java中,有几种方法可以成为单身:

  • 静态成员
  • 只允许巴黎人送28元类实例
  • 紧密耦合到巴黎人送28元特定的课程

静态班成员

因为在Java中一切都与巴黎人送28元类关联,所以唯一可以是单身的数据类型是类的静态成员。所以在这里’使用静态成员数据的非常糟糕的技术的巴黎人送28元很好的例子。一世’我将把这个例子归咎于我’LL Call Ornery Mungecraft,谁可能或可能不是前同事。

public class MyRandomGenerator
{
   static private int state = initialize();
   static public int getInt() { 
     state = iterate(state);
     return state;
   }

   /* Initialize random state */
   static private int initialize() {
     /* details omitted */
   }

   /* Compute next state from current state */
   static private int iterate(int oldState) {
     /* details omitted */
   }
}

这对许多级别都很糟糕。首先,一切都与全球国家相同;那里’没有办法实例化私人副本,这意味着随机数发生器的不同消费者将相互影响。共享状态也意味着此随机数生成器不是Threadsafe。例如,假设我们有这一序列:

  • 线程1读取Integer #k的本地副本 state
  • 线程2读取Integer #k的本地副本 state
  • Thread 1 uses its local copy of integer #k to compute integer #k+1, writes it back to state, and uses it
  • 线程1读取整数#k + 1的本地副本 state
  • Thread 1 uses its local copy of integer #k+1 to compute integer #k+2, writes it back to state, and uses it
  • Thread 2 uses its local copy of integer #k to compute integer #k+1, writes it back to state, and uses it
  • 线程1读取整数#k + 1的本地副本 state
  • Thread 1 uses its local copy of integer #k+1 to compute integer #k+2, writes it back to state, and uses it

哦哦—线程1生成两次相同的随机数!

我们可以通过添加同步来处理此类的线程安全性,但是’S只是把口红放在猪上。还有其他缺点:

  • 我们不’t have much control over the initialization of the static state variable: initialize() gets called 当课程首次加载时。这意味着它’课程之间的比赛。

  • 我们可以’t test this class without affecting the other places it is used. Testing techniques often utilize controlled setup of state variables: we might wish to go in and set state to something we choose, then run getInt() a few times and look at the output. Yeah, we could save the initial value of state, do our dirty work for testing, and then put state back the way we found it. And we could do this all in a synchronized block to keep things threadsafe. But that’对这个丑陋的猪来说更唇膏。

这里越重要的问题是,为什么静态成员和方法都使用?为什么地狱没有’作者用这种方式写下:

public class MyRandomGenerator
{
   private int state = initialize();
   public int getInt() { 
     state = iterate(state);
     return state;
   }

   /* Initialize random state */
   private int initialize() {
     /* details omitted */
   }

   /* Compute next state from current state */
   private int iterate(int oldState) {
     /* details omitted */
   }
}

This is the exact same code, but with the keyword 静止的 omitted. Now anyone can use their own private instance of the random number generator. It’s仍然不是线索快速分享巴黎人送28元实例,但这’无论如何,有巴黎人送28元奇怪的事情。

这里的外带是,随时您正在编写具有静态会员数据的代码,请问自己为什么’重新做到这一点。在大多数情况下,最好将其更改为实例数据,如果要区分单个对象的实例数据和对应于一组对象的私有数据,请将该私人数据放入单独的工厂类中。

实例单身

现在在某些情况下,程序员坚持能够确保只有巴黎人送28元课程的巴黎人送28元实例。 (在这里我们再次与单身模式。)在这里’■Java的示例主要有效:

public class MyClass
{
   static private MyClass INSTANCE = createInstance();
   static public MyClass getInstance() { return INSTANCE; }
   static private MyClass createInstance() { ... }

   /* constructor */
   private MyClass() { ... }
}

为什么这不是创建单例对象的防弹方式是奥术的一种,并且在Joshua Bloch讨论’s book 有效的Java. 本文 .

更大的问题是为什么无论如何都需要单例?如果它’■防止使用单个资源(如文件或计算机外围设备),使用同步技术仅允许对该资源的巴黎人送28元对象访问。如果它’S在B类的许多实例中共享巴黎人送28元类A类的巴黎人送28元对象,然后使用工厂类来创建所有B类对象,并且该出厂将相同的类A实例提供给这些B类对象中的每巴黎人送28元类。可能有巴黎人送28元真正的单身实例,但我没有’知道他们是什么,如果你这样做,那么你’重复可能足够努力实现它。

紧密耦合到特定的课程

记住这条规则高级语言:总是,总是, 总是 在特定接口围绕特定接口而不是特定实现写下您的程序。该接口定义了对象的行为方式。但是允许该界面的不同实现使您可以模块化,并防止大量在复杂程序中产生的紧密耦合和依赖性问题。换句话说,而不是这个:

// MyRandomGenerator.java
public class MyRandomGenerator
{
   public int getInt() { ... }
   /* implementation omitted */
}

// RGConsumer.java
public class RGConsumer
{
   final private MyRandomGenerator rgen = new MyRandomGenerator();
   void someMethod()
   {
      int i = this.rgen.getInt();
      ...
   }
}

你应该这样做:

// RandomGenerator.java
public interface RandomGenerator
{
   public int getInt();
}

// MyRandomGenerator.java
public class MyRandomGenerator implements RandomGenerator
{
   @Override public int getInt() { ... }
   /* implementation omitted */
}

// RGConsumer.java
public class RGConsumer
{
   // constructor call (see text below)
   final private RandomGenerator rgen = new MyRandomGenerator();
   void someMethod()
   {
      int i = this.rgen.getInt();
      ...
   }
}

In the first example, every time rgen is used, the compiler has to look at the MyRandomGenerator class, and you have tight coupling with that class. In the second example, the only place we are coupled to the MyRandomGenerator implementation is in the constructor call. When we use the variable rgen, all we have to know is that it’s an instance of some class implementing the RandomGenerator interface and we don’T必须关注实施细节。但构造函数呼叫仍然是巴黎人送28元弱点。即使在那里’s no global state here, this is still a kind of singleton, since RGConsumer has to know that the MyRandomGenerator class is a special single instance of a RandomGenerator.

那里’java中的一系列思想学院说,您应该避免直接在您的程序中调用类构造函数,因为这将您连接到巴黎人送28元接口的特定类实现,并且可以在单独的库之间导致依赖项。如果你想绕过这个,请阅读 依赖注入 和谷歌 gu 。具有依赖项注入,一种方法是在构造函数中的特定实例中传递:

// RGConsumer.java
public class RGConsumer
{
   final private RandomGenerator rgen;
   public RGConsumer(RandomGenerator rgen)
   {
      this.rgen = rgen;
   }

   void someMethod()
   {
      int i = this.rgen.getInt();
      ...
   }
}

这使得在申请中更高级别选择特定实现的责任。它’如果您有大量状态变量,则也有点笨拙。

也许我’有一天思考的方式,请达到这种思考。我理解动机,但是在我写的java程序的小规模,似乎有点极端。

单身人士以低级别语言

好的,现在让’在嵌入式系统中使用C或可能C ++的开关齿轮。以下是我们要注意的单身人士的类型:

  • 静态成员变量(在C ++中)
  • 函数中的静态变量
  • 全局变量
  • 机器寄存器(包括外围寄存器)

让’S按顺序处理这些。

静态成员变量(在C ++中)

这与Java中的确切用例相同,它具有相同的问题。随时您发现自己使用静态成员变量,请问自己是否真的需要它。

函数中的静态变量

这里’函数中的静态变量的示例:

int randomIntInitializer()
{
   return someComplicatedFunctionOfTimeAndOtherStuff();
}
int getNextRandomInt()
{
   static int state = randomIntInitializer();

   int newState = someComplicatedFunctionOfOldState(state);
   state = newState;
   return newState;
}

y!我们有静态成员变量的所有问题,但它’s even worse. At least in C++ or Java, we can access static variables from anywhere in their enclosing class. When you have a 静止的 variable in the body of a C function, the only place that can access that variable is within that function itself. We have no way of reading or writing state directly. OK, so we could add a mess of pig lipstick here and do this:

enum RandomOp { R_NORMAL, R_WRITE, R_READ };

int getNextRandomInt(enum RandomOp op, int forcedValue)
{
   static int state = randomIntInitializer();
   int newState;

   switch (op)
   {
      case R_READ:
         return state;
      case R_NORMAL:
         newState = someComplicatedFunctionOfOldState(state);
         break;
      case R_WRITE:
         newState = forcedValue;
         break;
   }
   state = newState;
   return newState;
}

和 then call getNextRandomInt(R_NORMAL, 0) during normal usage, getNextRandomInt(R_WRITE, desiredState) when we want to write state, and getNextRandomInt(R_READ, 0) when we want to read state. Got it?

Seriously, you think that is ok? I hope not. It looks like some ramshackle hodgepodge function someone threw together. This version of getNextRandomInt() serves three different purposes, and requires you to pass in extra arguments that you won’T使用99%的时间。哎呀。

The use of static variables in C functions is one of those things that should probably be deprecated. Your compiler should warn about it, or even stop and report it as an error, unless you go out of your way to bless a static variable with the right incantations of #pragma 或者 __attribute__ 或者 something.

如果你 must use static variables, at least keep them out of functions.

全局变量

在C ISN中使用全局变量’t so bad; there’有很多关于它的辩论,但在那里’如果您打算使用变量的全局定义,则没有任何内在错误,特别是如果所有全局定义都在一起,并且记录良好。它’基本上是顶级系统状态变量的库存:

enum MyState mystate;          // current state of the state machine
int number_of_fools;           // current number of fools in the ship of fools
char *application_name =       // what application are we again? 
        "Reindeer Games";      // oh yeah.
struct BeerBottle bottles[99]; // Ninety-nine bottles of beer on the wall,
                               // ninety-nine bottles of beer;
                               // take one down, pass it around,
                               // ninety-eight bottles of beer on the wall.
Reindeer_t dasher, dancer, prancer, vixen,
           comet, cupid, donner, blitzen,
           rudolph;            // 9 cute little reindeer!

int main()
{
   ... top-level application logic goes here ...
}

如果你这样看,它’S真的很容易确定是否需要成为全局变量。如果它’整体系统状态的一部分,它应该是全局变量。如果它’s not, and it’刚刚在部分程序中使用的临时变量,它应该’t是巴黎人送28元全局变量。那’s not so hard.

还是呢?

这里’是关键点:如果您真的有一组顶级系统状态变量, 为什么可以’t you just move them inside main()?

int main()
{
   enum MyState mystate;          // current state of the state machine
   int number_of_fools;           // current number of fools in the ship of fools
   char *application_name =       // what application are we again? 
           "Reindeer Games";      // oh yeah.
   struct BeerBottle bottles[99]; // Ninety-nine bottles of beer on the wall,
                                  // ninety-nine bottles of beer;
                                  // take one down, pass it around,
                                  // ninety-eight bottles of beer on the wall.
   Reindeer_t dasher, dancer, prancer, vixen,
              comet, cupid, donner, blitzen,
              rudolph;            // 9 cute little reindeer!

   ... top-level application logic goes here ...
}

现在他们属于应用程序中的最顶层逻辑。有一些原因不这样做。其中两个是半合法的,巴黎人送28元是真实的,但巴黎人送28元是巴黎人送28元不好的原因,它说明了为什么全局变量是“bad”. Let’以糟糕的原因开始。

But it breaks my code if you move them into main()!

让’说我做了上面的变化。第二天,我的同事们ornery mungecraft跑到了我的办公室,脸上愤怒的外观。“You broke my code!” he said. Here’s what he was using:

void add_new_fool()
{
   blink_nose(&rudolph);
   ++number_of_fools;       
}

When rudolphnumber_of_fools were global variables, everything was fine. As soon as we moved them inside main(), we introduced a compile error.

“在您重新检查之前,您应该尝试首先编译它!”ornery说。好的,他是对的。但这里的问题是’t the fact that we moved these variables; the problem is that Ornery was expecting to access them directly. He was hardcoding the use of rudolphnumber_of_fools into his code. It’更好地间接进入它们:

void add_new_fool(Reindeer_t *preindeer, int *pnumber_of_fools)
{
   blink_nose(preindeer);
   ++*pnumber_of_fools;       
}

现在我们有巴黎人送28元优势:我们已经将代码与其行为作用的数据分离。应用程序代码,尤其是图书馆代码应该是模块化的,这样我们就可以在任何驯鹿行动,而不仅仅是鲁道夫,而且我们可以编写测试代码:

void normal_app_code()
{
   ...
   add_new_fool(&rudolph, &number_of_fools);
   add_new_fool(&blitzen, &number_of_fools);
   ...
}

void test_add_new_fool()
{
   Reindeer_t test_reindeer;
   int number_of_fools = 33;

   init_reindeer(&test_reindeer);
   add_new_fool(&test_reindeer, &number_of_fools);
   assert (number_of_fools == 34);
   check_that_nose_has_blinked(&test_reindeer);
}

所以全局变量的问题是’真的在我们的时候造成的 global variables; it’s when we 使用权 他们直接。更改您的代码以拍摄指针,以及您’re fine.

但我有图书馆代码!你可以’t put my global variables in main()!

如果你’编写库代码或功能模块,以及您’重新负责顶级应用程序代码,然后它不起作用’t make sense to move associated state from your library or module, and into main(). But the real question is why does your library need to keep around state variables?

在某些情况下,库状态变量(类似于Java中的静态类成员变量)是合法的需求。和更好的方法是宣布他们 静止的 在巴黎人送28元汇编单元的内部,所以他们不’T危险在全局命名空间中发生冲突,以及最少的函数可直接访问它们:

 静止的  int reindeer_count;

int __get_reindeer_count() { return reindeer_count; }

Reindeer_t *allocate_reindeer()
{
  ++reindeer_count;
  Reindeer_t *result = (Reindeer *)malloc(sizeof(Reindeer_t));
  if (result != NULL)
  {
     init_reindeer(result);
  }
  return result;
}

但我想调试我的系统和它’当我使用全局变量时更可靠!

调试是其中有时会改变编程规则的事情之一。那不是’当您在调试器内运行代码时,T的问题是不同的,并且除了软件通常的内容之外,您现在关心您在使用调试器逐步浏览它时所看到的。

One of them is that statically-allocated variables are rock-solid. Your debugger can always see them, no matter where you happen to be in the program. If rudolphblitzen are global variables, and you’在堆栈中深入37级,您仍然可以将它们添加到调试器中的手表窗口中。他们’RE还保证在固定位置存在于数据存储器中。另一方面,如果堆栈上的本地变量,并且调高了编译器的优化级别,则有时编译器足够智能,以将它们放入CPU寄存器或甚至完全优化它们,这可能会留下一些调试器。

volatile int count1 = 0;
void myfunc(*psum)
{
   int x = count1 + 77;
   int y = count1 + 34;

   *psum += x;
   ++count1;
}

In this case, count1 exists in memory. The compiler might decide to put x on the stack, or it might decide to use a CPU register for x. If it does use a CPU register for x, and you stop the debugger on the ++count1 line, the value of x might not exist anymore because the compiler has used that CPU register for other purposes. And the value of y isn’在任何地方使用,所以编译器可能会决定它’t actually need to allocate storage for y 或者 compute count1 + 34.

Some debuggers are better than others. A good debugger will do the right thing: if x is stored in a register, and the value of x still exists there, and you are showing x in a watch window, the good debugger will obtain the value of x from the register; if x no longer exists, the good debugger will tell you that by displaying a message like No longer available 或者 No longer in scope. A mediocre debugger might display ??? because it’■只能在堆栈或全局内存中观看变量,它可以’t reliably use the value of registers. A bad debugger might display the wrong answer and leave you wondering why x is some completely unexpected value; this wastes your time, and you’ll开始想出你可以的伏都教规则’在转动Optimization时运行调试器。

所以,是的,有时候它’s appropriate to use global variables for important program state during debugging. But this is only for debugging; if all you care about is what happens to a program during normal operation, it should make no difference whether variables are statically allocated, or whether they are local variables allocated inside main().

但是我’m使用中断服务程序!

丁丁丁丁!我们有巴黎人送28元胜利者!我们所有其他投诉’ve是一种愿望的洞。是的,我们可以在没有全球变量的情况下逃脱,我们只是唐’t really want to.

中断服务例程 是不同的。定义了大多数CPU架构的方式,ISR的工作方式如下:您的应用程序一次执行一步,然后BAM!一些外部信号进入并触发特定的ISR。正常执行停止,程序计数器和可能会保存几个核心机器寄存器,并且执行跳转到所讨论的ISR。 ISR通常需要函数没有参数,也没有返回值。硬件设计没有’T似乎拥抱功能编程,并采用最小的方法,而是改变程序计数器,保存最少的东西,让ISR执行它想要的东西,然后恢复我们的Merrily执行程序。

这意味着如果您希望ISR做任何有用的东西,您必须安排将数据放在ISR可以获得的地方。因此,至少有一条数据必须放在固定地址上,如a 死亡掉落 等待巴黎人送28元间谍代理人来捡起它。您可以让这一条数据包含巴黎人送28元地址,该地址指向位于堆栈中的其他数据,或堆积,或者巴黎人送28元地方的一些魔术可移动盛宴,其中巴黎人送28元地方与Unicorns和Jimmy Hoffa和Andy Kaufman以及可恶的雪人和可恶的雪人。我们不’真的很关心。但是,ISR可以找到此其他数据的唯一方法是,如果我们将指针放在ISR了解的固定位置。

所以,如果你的话,很客’重新使用微控制器,迟早或以后’在使用ISR时,重新将使用全局变量。

机器寄存器(包括外围寄存器)

在微控制器中,我们经常使用机器寄存器。我不’t意味着与ALU相关的数值。一世’m谈论控制某些设备功能或外设的那些。在Microchip的设备中,这些都被称为 特殊功能寄存器 或者 SFRs.

这些寄存器始终是单例,因为它们中的每巴黎人送28元都是内存映射到特定的固定地址。

使用它们的正确方法是封装它们 硬件抽象层 (HAL) — instead of using some register TMR1 directly, use a library function. The library function will take care of the ugly register-specific logic.

如果你’在微控制器中处理C代码,您需要编写自己的软件以使用硬件寄存器而不是使用预先存在的库函数,确保引用硬件寄存器’T分散在代码周围:通过制作一系列功能来在孤立的文件中创建自己的硬件抽象层。

/*
 * BAD CODE
 * courtesy of Ornery Mungecraft, who doesn't work here anymore
 */
void frobozznicate()
{
    // read the glob count registers
    int32_t x = GLOBCTH;
    x <<= 16;
    x |= (uint32_t)GLOBCTL;

    x += rudolph.antler_point_count;

    // write them back
    GLOBCTH = x >> 16;
    GLOBCTL = x & 0xffff;
}

// point of use:
frobozznicate();

啊。 ornery真的很喜欢全局变量。

/* 
 * Better code! Yay!
 */

/*
 * === begin 硬件抽象层 === 
 */

int32_t get_glob_count()
{
    int32_t x = GLOBCTH;
    x <<= 16;
    x |= (uint32_t)GLOBCTL;
    return x;
}

void set_glob_count(int32_t x)
{
    GLOBCTH = x >> 16;
    GLOBCTL = x & 0xffff;
}

/*
 * === end 硬件抽象层 ===
 */

int32_t frobozznicate(const Reindeer_t *preindeer, int32_t old_count)
{
    return old_count + preindeer->antler_point_count;
}

// point of use:
set_glob_count(frobozznicate(&rudolph, get_glob_count()));

Much better. Here the use of hardware registers is confined to the get_glob_count()set_glob_count() variables, and the frobozznicate() function is a pure function that acts only on its inputs and returns an output.

申请单身人士

到目前为止,所有这些都是关于Singletons在软件实施中的。这些类型的单身人士几乎肯定会被使用该软件的人员忽视。单身的完全不同的方面是软件用户与唯一对象交互时。这里’s an example from my 花园耙文件:

  • 当您在Microsoft Word 2007中打开两个单独的文档时,它们会显示为单独的窗口,您可以放置​​您想要的任何地方。如果我有两个显示器,我可以在监视器A中查看巴黎人送28元文档,并在监视器B中查看另巴黎人送28元文档。
  • 当您在Microsoft Excel 2007或PowerPoint 2007中打开两个单独的文档时,它们会显示为Singleton应用程序窗口的子窗口,因为Microsoft的一些笨蛋决定了 MDI. 仍然是优选的。 (嘿微软!1990叫,它想要它的MDI回来!)如果您想将它们与单独的监视器进行比较’re out of luck.

让’s ask the question: 只允许巴黎人送28元应用程序的巴黎人送28元实例吗?

我不’知道有巴黎人送28元答案。我知道只允许巴黎人送28元 窗户 是巴黎人送28元糟糕的设计决定。我不’真的关心软件运行的一份副本,还是有几个副本。但我不’T想要陷入困境。 (除了管理巴黎人送28元或多个窗口的巴黎人送28元过程之外,还可以通过多个进程管理巴黎人送28元窗口。Google Chrome浏览器是巴黎人送28元很好的例子:为了隔离崩溃和无响应的线程,Chrome的设计人员决定给每个浏览器标签自己的进程,并通过一些神奇的壮举,多个进程一起共享巴黎人送28元窗口。)

在相反的极端,有时你 singletons. Here’这个例子。我在工作中运行matlab,它可能需要20-30秒,以便启动。 Matlab脚本称为M文件。每一次偶尔,当我想编辑m-file时,我误错了一下我的文件系统资源管理器中的m-file,它打开了另巴黎人送28元matlab的实例,这需要很长时间,并且它令人困惑的是我的电脑上有两套Matlab窗口。我真的只想在我已经运行的matlab实例中打开编辑器中的m-file。但是我可以’要这样做。因此,在这种情况下,我的单例对我来说真的更容易。至少这应该是用户偏好。

这里的那一点是你应该考虑你的软件的用户是否希望使用巴黎人送28元中央的东西,因为他们看到它,或几个单独的东西。

单身人士从一万米

现在我们’LL返回软件设计内部的问题。让’S从另巴黎人送28元角度返回另一步。这里’s巴黎人送28元非常简单的图表:

分配 用法
数据 全局与局部变量直接与间接访问
代码 ???? 直接与间接访问具体课程与接口

单身概念以四种不同的方式显示出来。无论你是否意识到它,我们’谈到了三个。

查看Mungecraft先生的这个代码片段:

Reindeer_t rudolph;

void frobozznicate()
{
    // read the glob count registers
    int32_t x = GLOBCTH;
    x <<= 16;
    x |= (uint32_t)GLOBCTL;

    x += rudolph.antler_point_count;

    // write them back
    GLOBCTH = x >> 16;
    GLOBCTL = x & 0xffff;
}

The global variable rudolph is an example of static allocation. And I said that global variables themselves weren’必然糟糕。什么’s bad with the above snippet is that the frobozznicate() function reaches out and directly accesses rudolph. If you look at its function signature, frobozznicate() has no inputs and no outputs. Blechh! The interfaces in your software should be directly visible in the function signatures, rather than some back-door hidden things that make you look at each line of code to figure out where the real inputs and outputs are.

全局变量 分配 单身人士。或者更确切地说:全局变量分配存储有潜力的存储。这里更糟糕的错误是编写代码 访问 全球变量,基本上使它们进入单身人士—就像杰克尔博士一样转变为海德先生 —通过直接引用所讨论的变量。我们可以通过打破直接访问来解决此问题:

int32_t frobozznicate(const Reindeer_t *preindeer, int32_t old_count)
{
    return old_count + preindeer->antler_point_count;
}

塔达!不再直接访问全局变量。现在我们可以将此功能与所有其他驯鹿一起使用。再见,海德先生!

正如数据有可能用作单身的潜力一样,我们也可以使用代码作为单身。在更高级别的语言中,我们有几种方法可以访问代码。让’s说我们有几个看起来像这样的java类:

/**
 * horn-bearing mammals of the order Artiodactyla
 * and the infraorder Pecora
 */
interface Pecoran
{
    public String getFamily();
    public String getGenus();
    public String getSpecies();
    public String getCommonName();
    public String getAnimalName();
    public String getHornName();
    public int getPointCount();
}

abstract class AbstractPecoran implements Pecoran
{
    final private String name;
    final private int pointCount;
    public AbstractPecoran(String name, int pointCount)
    {
        this.name = name;
        this.pointCount = pointCount;
    }
    @Override public String getAnimalName() { return this.name; }
    @Override public int getPointCount() { return this.pointCount; }        
}

abstract class Cervid extends AbstractPecoran
{
    public Cervid(String name, int pointCount)
    {
        super(name, pointCount);
    }
    @Override public String getFamily() { return "Cervidae"; }
    @Override public String getHornName() { return "antler"; }
}

class Reindeer extends Cervid
{
    public Reindeer(String name, int pointCount)
    {
        super(name, pointCount);
    }
    @Override public String getGenus()  { return "Rangifer"; }
    @Override public String getSpecies() { return "tarandus"; }
    @Override public String getCommonName() { return "reindeer"; }
}

class ReindeerGame
{
    public void play(Reindeer reindeer)
    {
        System.out.printf("Reindeer %s has antlers with %d points. Ha ha!\n",
            reindeer.getAnimalName(),
            reindeer.getPointCount());
    }
}

The ReindeerGame class has tight coupling to two other classes. Can you spot them?

One of them is the Reindeer class. What if we wanted to play games with giraffes or oryxes or sheep or moose or antelope, and not just reindeer? Well, we couldn’t if we use the ReindeerGame class. But we could change things to be more general:

class PecoranGame
{
    private String capitalize(String s)
    {
        if (s.isEmpty())
            return s;
        return s.substring(0, 1).toUpperCase() + s.substring(1);
    }
    public void play(Pecoran pecoran)
    {
        System.out.printf("%s (%s %s) %s has %ss with %d points. Ha ha!\n",
            capitalize(pecoran.getCommonName()),
            pecoran.getGenus(),
            pecoran.getSpecies,
            pecoran.getAnimalName(),
            pecoran.getHornName(),
            pecoran.getPointCount());
    }
}

The other class is java.lang.System! (And actually, we have a third class, java.lang.String, but this is such a fundamental class to Java that you can forget about the whole tight coupling thing. Just assume everyone knows about String.) It would be better to pass in an outside object:

class PecoranGame
{
    private String capitalize(String s)
    {
        if (s.isEmpty())
            return s;
        return s.substring(0, 1).toUpperCase() + s.substring(1);
    }
    public void play(Pecoran pecoran, PrintStream ps)
    {
        ps.printf("%s (%s %s) %s has %ss with %d points. Ha ha!\n",
            capitalize(pecoran.getCommonName()),
            pecoran.getGenus(),
            pecoran.getSpecies,
            pecoran.getAnimalName(),
            pecoran.getHornName(),
            pecoran.getPointCount());
    }
}

Ha ha, indeed! Now we can pass in mock objects to PecoranGame.play() 和 test them to our 哈特 ’s heart’s content. The standard stream System.out is a widely-used singleton, and it’■你应该小心在你的代码中使用– much better to use a PrintStream so that if you want to change the behavior, you can make it a choice of the calling application rather than something hard-coded in your class. Again, this is an example of changing direct access to a singleton to indirect access.

The other aspect of singletons in code is referring to interfaces (or at least general abstract classes) rather than concrete classes. This decouples your code from a particular implementation. And here we did this by changing the Reindeer class reference to the more general Pecoran interface.

第四广场:代码分配?

到目前为止,这只是我们文章第一部分中的概念的重述。

但我们在我们的图表中拥有讨厌的广场“???” —这代表了代码的分配;它是什么?

代码(而不是数据)真正分配?

在具有一流的功能的高级语言中,那里’没有代码和数据之间的差异。我可以在Python模块中做这样的事情:

# module " 或者 nery.py"

foo = 3
def bar(x):
   return x+7

def makeBaz(k):
   def baz(x):
      return x+k
   return baz

那里’s much less of a distinction between data and code here. The module 或者 nery contains three attributes:

  • foo 绑定到整数3
  • bar bound to a function
  • makeBaz bound to a function.

In addition, makeBaz is a function that returns a function. There’对于返回值的功能,没有任何神奇的神奇。它’只是我们可以的东西’T真的用语言如C,或在早期版本的C ++和Java中’T支持一流的功能;像JavaScript和Python和Haskell这样的语言以及所有LISP衍生品所做的。

无论如何,回到分配问题。在我们的时候“create” a function, what we’re really doing is defining it within a namespace at compile time; the compiler churns away and boils it down to machine code stored in an object file and associated with a symbol that has the name we gave it. C only has two choices of namespace: there is the global namespace, which is the default case, and there is file-static scope using the 静止的 keyword. This applies both to variable and function definitions. In C, if you want your function to be visible to other compilation units, it has to be a definition in the global namespace. Unlike the makeBaz() function above, in C you can’T在运行时或以任何其他方式创建功能,而不是在源代码中定义它,除非您使用一些疯狂的非易成字节的黑客。 (Java和C ++都有名称空间,两者都在通过C ++ 11和Java 8中使用Lambda语法来支持动态定义的函数。)

想一想:当您在C中定义其他编译单元可见的函数时,您正在创建单例!

int32_t frobozznicate(const Reindeer_t *preindeer, int32_t old_count)
{
    return old_count + preindeer->antler_point_count;
}

在这里,我们正试图通过以预测的方式传递指针并使用输入和输出来完成所有事情,但我们’re still creating a unique function called frobozznicate in the global C namespace, and clients of this function typically go and access it directly via its name:

Reindeer_t rudolph = ...;
int x = frobozznicate(&rudolph, 3);

是的,你可以去与函数指针一起工作

typedef int32_t (*reindeer_func)(const Reindeer_t *, int32_t);
reindeer_func f1 = &frobozznicate;
int x2 = f1(&rudolph, 3);

和 this way you could support alternative functions acting on Reindeer_t objects, but unless you want to go to that extent of modularity, it’不是真的值得麻烦。我们不’无论是全球范围内,都要大量的功能和课程’真正在像C中的全局命名空间中,或者它们是否在java中的包系统或C ++命名空间系统中的一些子命名空间中的家庭。如果我们需要抽象出来 用法 函数或类,在C ++或Java中的接口中使用C或Virtual Base类中的函数指针,即’另巴黎人送28元故事。这让我们概括并接受对适合我们需求的东西的抽象引用,而不是要求使用特定功能或类。但是在那里’除了名称碰撞问题之外,否则没有真正的理由估算全局命名空间中的函数或类的定义。

无论如何,那’想到的东西。真的没有’代码和数据之间的差异很大。当我们学习像C这样的语言时,我们才接受过培训,以这种方式。

包起来

单身 s是软件设计中的独特对象。有时他们是有意而且独一无二的–GOF设计模式书中的单身模式是其中的巴黎人送28元例子。其他时间他们’作为独特的,因为我们使用支持唯一性的语言功能,我们是否打算。通常,它们是巴黎人送28元值得令人沮丧的功能,因为它们打破了模块化并增加了类之间的紧密耦合。

我们可以 have singletons at a user-visible level (the one Excel window that can be opened at a time) or at the software implementation level.

数据和代码都可以是单例。我们通常会将单例视为数据,但是给出的名称的函数和类,绑定到全局命名空间中的巴黎人送28元唯一位置,并按名称引用,也是单例。这是Java中的巴黎人送28元原因’S推荐以通过名称引用更多常规接口,并在运行时的那些普通引用的特定实例中传递,以便我们的课程客户唐’真正了解或关心我们的课程被命名或所在的位置,只有他们符合我们所需要的界面要求。

此外,制作单例需要两个行动:分配或在全球范围内定义它只是朝着单身人士的一部分,并不是一件坏事。使单身的主要行为是对其的硬编码直接引用。如果我们编写接受间接引用的代码(C / C ++中的指针,C ++中的引用或Java中的对象),那么我们的代码就没有’保留数据实际位置的位置。

非常低级代码,使用硬件寄存器和中断服务例程可以’T绕过使用直接引用单例数据,但良好的软件设计在一种容纳区中封装了这一点,因此这些单例的知识仅限于软件应用程序的一小部分。

下次我们’ll be discussing 国家机器.


©2014年杰森M. Sachs,保留所有权利。


[]
评论 安德鲁56. 2015年1月5日
两件事情:
1)您发现传递给顶级状态的引用使功能签名持续多久?我一直在几乎每个函数中包含一些重要数据的情况(有关仿真的详细信息,例如网格尺寸)。移动到全局变量非常明显清理代码。

2)我使用函数(C ++)中的静态变量以几种方式,并讨厌看到它们。特别是我用它们在很多数字代码中使用它们是:我的并行性是用mpi完成的(所以每个过程的单线线),在同一堆栈上不会调用两次函数(buildstiffymatrix(..)不会最终呼叫自己),最后,在MB的顺序上存在大的临时对象,不应重复分配和解除(例如计算中介或MPI缓冲区)。我确实认识到这种用法是非典型的。
[]
评论 JMS_NH. 2015年1月6日
1)结构允许您将相关数据组合在一起。在工作中,我们有巴黎人送28元具有数十个状态变量的项目,但它们被组织成两个结构(其中巴黎人送28元结构与另巴黎人送28元有指针)。如果您在几乎每个函数中包含重要数据,为什么不定义巴黎人送28元名为SimulationDetails的结构,并将指针传递给您的功能?

2)选择你的毒药。如果是我,我宁愿将这些静态全局变量移动到静态全局变量,并将其传递给他们。重点是将变量分配与对它们的访问分配。在具有静态变量的函数中,您有紧密的耦合,这对其使用限制(即使在您所说,在您的情况下,您将不会调用两次)。如果在函数之外移动变量,但仍然使它们静态分配,则避免重复的分配/释放,但允许松散耦合。

要发布回复评论,请单击连接到每个注释的“回复”按钮。发布新的评论(不是回复评论),请在评论的顶部查看“写评论”选项卡。

注册将允许您参加所有相关网站的论坛,并为您提供所有PDF下载。

注册

我同意 使用条款 隐私政策.

尝试我们偶尔但流行的时事通讯。非常容易取消订阅。
或登录