Skip to content

پیوست ب — پاسخ تمرین‌ها

تمرین ۱: از راست‌گزینی، صفحه ۴۳

شما کلاسی به نام Split می‌نویسید که خطوط ورودی را به فیلدها تقسیم می‌کند. کدام یک از دو امضای کلاس Java زیر طراحی راست‌گزارتر است؟

java
class Split1 {
  public Split1(InputStreamReader rdr) { ...
  public void readNextLine() throws IOException { ...
  public int numFields() { ...
  public String getField(int fieldNo) { ...
}
class Split2 {
  public Split2(String line) { ...
  public int numFields()     { ...
  public String getField(int fieldNo) { ...
}

پاسخ ۱: به نظر ما، کلاس Split2 راست‌گزارتر است. روی وظیفه خود — تقسیم خطوط — تمرکز می‌کند و جزئیاتی مثل منبع خطوط را نادیده می‌گیرد. نه‌تنها توسعه کد را آسان‌تر می‌کند، بلکه انعطاف‌پذیرتر هم هست. Split2 می‌تواند خطوطی را که از فایل خوانده شده، توسط روتین دیگری تولید شده یا از محیط اجرا (environment) آمده‌اند تقسیم کند.

تمرین ۲: از راست‌گزینی، صفحه ۴۳

کدام به طراحی راست‌گزارتر منجر می‌شود: جعبه‌های گفتگوی بدون حالت (modeless) یا حالت‌دار (modal)؟

پاسخ ۲: اگر درست پیاده‌سازی شود، احتمالاً بدون حالت. سیستمی که از جعبه‌های گفتگوی بدون حالت استفاده می‌کند کمتر نگران آن است که در هر لحظه خاص چه اتفاقی می‌افتد. احتمالاً زیرساخت ارتباط بین‌ماژولی بهتری نسبت به سیستم حالت‌دار دارد؛ سیستم حالت‌دار ممکن است فرضیات درونی درباره وضعیت سیستم داشته باشد — فرضیاتی که به افزایش coupling و کاهش راست‌گزایی منجر می‌شوند.

تمرین ۳: از راست‌گزینی، صفحه ۴۳

زبان‌های رویه‌ای در برابر فناوری شیءگرا چطور؟ کدام به سیستم راست‌گزارتر منجر می‌شود؟

پاسخ ۳: این کمی پیچیده است. فناوری شیءگرا می‌تواند سیستم راست‌گزارتر بدهد، اما چون ویژگی‌های بیشتری برای سوءاستفاده دارد، در واقع ساختن سیستم غیرراست‌گزار با اشیاء از زبان رویه‌ای آسان‌تر است. ویژگی‌هایی مثل ارث‌بری چندگانه، استثناها، بارگذاری مجدد عملگر (operator overloading) و بازنویسی متد والد (از طریق subclassing) فرصت‌های فراوانی برای افزایش coupling به‌صورت غیرآشکار فراهم می‌کنند.

با فناوری شیءگرا و کمی تلاش اضافه می‌توانید سیستم بسیار راست‌گزارتر بسازید. اما در حالی که همیشه می‌توانید در زبان رویه‌ای «کد اسپaghetti» بنویسید، استفاده نادرست از زبان‌های شیءگرا می‌تواند به اسپaghetti شما «کوفته» هم اضافه کند.

تمرین ۴: از نمونه‌های اولیه و یادداشت Post-it، صفحه ۵۶

بازاریابی می‌خواهد با شما چند طراحی صفحه وب را طوفان فکری کند. به نقشه‌های تصویری قابل کلیک برای رفتن به صفحات دیگر فکر می‌کنند و غیره. اما نمی‌توانند روی مدل تصویر تصمیم بگیرند — شاید ماشین، شاید تلفن، شاید خانه. فهرستی از صفحات و محتوای هدف دارید؛ می‌خواهند چند نمونه اولیه ببینند. راستی، ۱۵ دقیقه وقت دارید. از چه ابزارهایی استفاده می‌کنید؟

پاسخ ۴: فناوری ساده نجات می‌دهد! با ماژیک روی تخته سفید چند کارتون بکشید — ماشین، تلفن و خانه. لازم نیست هنر عالی باشد؛ طرح‌های stick-figure کافی است. روی نواحی قابل کلیک یادداشت Post-it بچسبانید که محتوای صفحات هدف را توصیف می‌کند. با پیشرفت جلسه می‌توانید نقاشی‌ها و جای یادداشت‌ها را اصلاح کنید.

تمرین ۵: از زبان‌های دامنه، صفحه ۶۳

می‌خواهیم یک زبان کوچک برای کنترل بسته ترسیم ساده (شاید سیستم turtle-graphics) پیاده‌سازی کنیم. زبان از دستورات تک‌حرفی تشکیل شده. برخی دستورات با یک عدد دنبال می‌شوند. مثلاً ورودی زیر یک مستطیل می‌کشد.

P   2    #   select pen 2
D        #   pen down
W   2    #   draw west 2cm
N   1    #   then north 1
E   2    #   then east 2
S   1    #   then back south
U        #   pen up

کدی بنویسید که این زبان را parse کند. طوری طراحی شود که افزودن دستورات جدید ساده باشد.

پاسخ ۵: چون می‌خواهیم زبان قابل گسترش باشد، parser را جدول‌محور می‌سازیم. هر ورودی جدول شامل حرف دستور، پرچمی که می‌گوید آیا آرگومان لازم است، و نام روتینی است که باید برای آن دستور فراخوانی شود.

c
typedef struct {
  char cmd;               /* the command letter */
  int hasArg;             /* does it take an argument */
  void (*func)(int, int); /* routine to call */
} Command;
static Command cmds[] = {
   { 'P', ARG,     doSelectPen },
   { 'U', NO_ARG, doPenUp },
   { 'D', NO_ARG, doPenDown },
   { 'N', ARG,     doPenDir },
   { 'E', ARG,     doPenDir },
   { 'S', ARG,     doPenDir },
   { 'W', ARG,     doPenDir }
};

برنامه اصلی ساده است: یک خط بخوان، دستور را پیدا کن، در صورت نیاز آرگومان بگیر، سپس تابع handler را فراخوانی کن.

c
while (fgets(buff, sizeof(buff), stdin)) {
    Command *cmd = findCommand(*buff);
    if (cmd) {
      int   arg = 0;
        if (cmd->hasArg && !getArg(buff+1, &arg)) {
          fprintf(stderr, "'%c' needs an argument n", *buff);
          continue;
        }
        cmd->func(*buff, arg);
    }
}

تابعی که دستور را پیدا می‌کند جست‌وجوی خطی روی جدول انجام می‌دهد و یا ورودی منطبق یا NULL برمی‌گرداند.

c
Command *findCommand(int cmd) {
  int i;
    for (i = 0; i < ARRAY_SIZE(cmds); i++) {
      if (cmds[i].cmd == cmd)
        return cmds + i;
    }
    fprintf(stderr, "Unknown command '%c' n", cmd);
    return 0;
}

در پایان، خواندن آرگومان عددی با sscanf بسیار ساده است.

c
int getArg(const char *buff, int *result) {
  return sscanf(buff, "%d", result) == 1;
}

تمرین ۶: از زبان‌های دامنه، صفحه ۶۳

گرامر BNF طراحی کنید که مشخصه زمانی (time specification) را parse کند. همه نمونه‌های زیر باید پذیرفته شوند.

4pm, 7:38pm, 23:42, 3:16, 3:16am

پاسخ ۶: با BNF، مشخصه زمانی می‌تواند این‌گونه باشد:

time               hour       ampm
                   hour      : minute       ampm
                   hour      : minute
ampm              am pm
hour               digit
                   digit      digit
minute             digit      digit
digit             0 1 2 3 4 5 6 7 8 9

تمرین ۷: از زبان‌های دامنه، صفحه ۶۳

parserی برای گرامر BNF تمرین ۶ با yacc، bison یا مولد parser مشابه پیاده‌سازی کنید.

پاسخ ۷: مثال خود را با bison، نسخه GNU از yacc، کدنویسی کردیم. برای وضوح فقط بدنه parser را نشان می‌دهیم. برای پیاده‌سازی کامل به سورس در وب‌سایت ما مراجعه کنید.

yacc
time:         spec END_TOKEN
              { if ($1 >= 24*60) yyerror("Time is too large");
                printf("%d minutes past midnight n", $1);
                exit(0);
              }
          ;
spec:       hour       ':' minute
            { $$       = $1 + $3;
            }
          | hour       ':' minute ampm
            { if       ($1 > 11*60) yyerror("Hour out of range");
              $$       = $1 + $3 + $4;
            }
          | hour       ampm
            { if       ($1 > 11*60) yyerror("Hour out of range");
              $$       = $1 + $2;
            }
          ;
hour:         hour_num
              { if ($1 > 23) yyerror("Hour out of range");
                $$ = $1 * 60;
              };
yacc
minute:      DIGIT DIGIT
              { $$ = $1*10 + $2;
                if ($$ > 59) yyerror("minute out of range");
              };
ampm:       AM                    { $$ = AM_MINS; }
          | PM                    { $$ = PM_MINS; }
          ;
hour_num: DIGIT                   { $$ = $1; }
        | DIGIT DIGIT             { $$ = $1*10 + $2; }
        ;

تمرین ۸: از زبان‌های دامنه، صفحه ۶۳

time parser را با Perl پیاده‌سازی کنید. [راهنما: عبارات باقاعده parserهای خوبی می‌سازند.]

پاسخ ۸:

perl
$_ = shift;
/^( d d?)(am|pm)$/           && doTime($1, 0,    $2, 12);
/^( d d?):( d d)(am|pm)$/ && doTime($1, $2, $3, 12);
/^( d d?):( d d)$/           && doTime($1, $2,   0, 24);
die "Invalid time $_ n";
#
# doTime(hour, min, ampm, maxHour)
#
sub doTime($$$$) {
  my ($hour, $min, $offset, $maxHour) = @_;
  die "Invalid hour: $hour" if ($hour >= $maxHour);
  $hour += 12 if ($offset eq "pm");
  print $hour*60 + $min, " minutes past midnight n";
  exit(0);
}

تمرین ۹: از تخمین‌زنی، صفحه ۶۹

از شما پرسیده می‌شود: «کدام پهنای باند بیشتر است: خط ارتباطی ۱Mbps یا فردی که با نوار ۴GB پر در جیب بین دو رایانه راه می‌رود؟» چه محدودیت‌هایی روی پاسخ می‌گذارید تا دامنه پاسخ درست باشد؟ (مثلاً ممکن است بگویید زمان دسترسی به نوار نادیده گرفته می‌شود.)

پاسخ ۹: پاسخ ما باید در چند فرض بنا شود:

  • نوار حاوی اطلاعاتی است که باید منتقل شود.
  • سرعت راه رفتن فرد را می‌دانیم.
  • فاصله بین ماشین‌ها را می‌دانیم.
  • زمان انتقال اطلاعات به و از نوار را در نظر نمی‌گیریم.
  • سربار ذخیره داده روی نوار تقریباً برابر سربار ارسال آن روی خط ارتباطی است.

تمرین ۱۰: از تخمین‌زنی، صفحه ۶۹

پس کدام پهنای باند بیشتر است؟

پاسخ ۱۰: با توجه به محدودیت‌های پاسخ ۹: نوار ۴GB حاوی ۳۲×۱۰⁹ بیت است، بنابراین خط ۱Mbps باید حدود ۳۲٬۰۰۰ ثانیه — یا تقریباً ۹ ساعت — داده پمپ کند تا معادل همان مقدار اطلاعات منتقل شود. اگر فرد با سرعت ثابت ۳ مایل در ساعت راه برود، دو ماشین ما باید حداقل ۲۷ مایل از هم فاصله داشته باشند تا خط ارتباطی از پیک ما پیشی بگیرد. در غیر این صورت فرد برنده است.

تمرین ۱۱: از دستکاری متن، صفحه ۱۰۲

برنامه C شما از enum برای نمایش یکی از ۱۰۰ حالت استفاده می‌کند. می‌خواهید حالت را به‌صورت رشته (نه عدد) برای اشکال‌زدایی چاپ کنید. اسکریپتی بنویسید که از ورودی استاندارد فایلی بخواند حاوی:

name
state_a
state_b
 :

فایل name.h را تولید کند که شامل:

c
extern const char* NAME_names[];
typedef enum {
   state_a,
   state_b,
    :
 } NAME;

و فایل name.c که شامل:

c
const char* NAME_names[] = {
    "state_a",
    "state_b",
      :
 };

پاسخ ۱۱: پاسخ خود را با Perl پیاده‌سازی کردیم.

perl
my @consts;
my $name = <>;
die "Invalid format - missing name" unless defined($name);
chomp $name;
# Read in the rest of the file
while (<>) {
  chomp;
  s/^ s*//; s/ s*$//;
    die "Invalid line: $_" unless /^( w+)$/;
    push @consts, $_;
}
# Now generate the file
open(HDR, ">$name.h") or die "Can't open $name.h: $!";
open(SRC, ">$name.c") or die "Can't open $name.c: $!";
my $uc_name = uc($name);
my $array_name = $uc_name . "_names";
print HDR "/* File generated automatically - do not edit */ n";
print HDR "extern const char *$ {array_name}[];";
print HDR "typedef enum { n ";
print HDR join ", n        ", @consts;
print HDR " n} $uc_name; n n";
print SRC "/* File generated automatically - do not edit */ n";
print SRC "const char *$ {array_name}[] = { n         "";
print SRC join " ", n        "", @consts;
print SRC " " n}; n";
close(SRC);
close(HDR);

با اصل DRY، این فایل جدید را در کد cut-and-paste نمی‌کنیم. به‌جای آن #include می‌کنیم — فایل تخت منبع اصلی این ثابت‌هاست. یعنی وقتی فایل عوض شود makefile برای بازتولید header لازم است. قطعه زیر از test bed در درخت سورس ما (در وب‌سایت) است.

etest.c etest.h:       etest.inc enumerated.pl
                       perl enumerated.pl etest.inc

تمرین ۱۲: از دستکاری متن، صفحه ۱۰۲

نیمه راه نوشتن این کتاب فهمیدیم use strict را در بسیاری از مثال‌های Perl نگذاشته‌ایم. اسکریپتی بنویسید که فایل‌های .pl در یک پوشه را بگردد و use strict را در انتهای بلوک کامنت اولیه به همه فایل‌هایی که ندارند اضافه کند. از همه فایل‌هایی که تغییر می‌دهید پشتیبان بگیرید.

پاسخ ۱۲: پاسخ ما، نوشته‌شده با Perl:

perl
my $dir = shift or die "Missing directory";
for my $file (glob("$dir/*.pl")) {
    open(IP, "$file") or die "Opening $file: $!";
    undef $/;           # Turn off input record separator --
    my $content = <IP>; # read whole file as one string.
    close(IP);
    if ($content !~ /^use strict/m) {
      rename $file, "$file.bak" or die "Renaming $file: $!";
      open(OP, ">$file") or die "Creating $file: $!";
      # Put 'use strict' on first line that
      # doesn't start '#'
      $content =~ s/^(?!#)/ nuse strict; n n/m;
      print OP $content;
      close(OP);
      print "Updated $file n";
    }
    else {
      print "$file already strict n";
    }
}

تمرین ۱۳: از مولدهای کد، صفحه ۱۰۶

مولد کدی بنویسید که فایل ورودی شکل ۳.۴ (صفحه ۱۰۶) را بگیرد و خروجی را به دو زبان انتخابی‌تان تولید کند. سعی کنید افزودن زبان‌های جدید آسان باشد.

پاسخ ۱۳: برای پیاده‌سازی از Perl استفاده کردیم. ماژول تولید زبان درخواستی را به‌صورت پویا بار می‌کند، پس افزودن زبان جدید آسان است. روتین اصلی back end را (بر اساس پارامتر خط فرمان) بار می‌کند، سپس ورودی را می‌خواند و بر اساس محتوای هر خط روتین‌های تولید کد را فراخوانی می‌کند. درباره مدیریت خطا سخت‌گیر نیستیم — اگر مشکلی پیش بیاید زود متوجه می‌شویم.

perl
my $lang = shift or die "Missing language";
$lang .= "_cg.pm";
require "$lang" or die "Couldn't load $lang";
# Read and parse the file
my $name;
while (<>) {
  chomp;
  if    (/^ s*$/)         { CG::blankLine(); }
    elsif (/^ #(.*)/)     { CG::comment($1); }
    elsif (/^M s*(.+)/) { CG::startMsg($1); $name = $1; }
    elsif (/^E/)        { CG::endMsg($name); }
    elsif (/^F s*( w+) s+( w+)$/)
                        { CG::simpleType($1,$2); }
    elsif (/^F s*( w+) s+( w+) [( d+) ]$/)
                        { CG::arrayType($1,$2,$3); }
    else {
      die "Invalid line: $_";
    }
}

نوشتن back end زبان ساده است: ماژولی بدهید که شش entry point لازم را پیاده کند. این مولد C است:

perl
#!/usr/bin/perl -w
package CG;
use strict;
# Code generator for 'C' (see cg_base.pl)
sub blankLine() { print " n"; }
sub comment()        { print "/*$_[0] */ n"; }
sub startMsg()       { print "typedef struct { n"; }
sub endMsg()         { print "} $_[0]; n n"; }
sub arrayType() {
  my ($name, $type, $size) = @_;
  print " $type $name [$size]; n";
}
sub simpleType() {
  my ($name, $type) = @_;
  print " $type $name; n";
}
1;

و این یکی برای Pascal:

perl
#!/usr/bin/perl -w
package CG;
use strict;
# Code generator for 'Pascal' (see cg_base.pl)
sub blankLine() { print " n"; }
sub comment()        { print "{$_[0] } n"; }
sub startMsg()       { print "$_[0] = packed record n"; }
sub endMsg()         { print "end; n n"; }
sub arrayType() {
  my ($name, $type, $size) = @_;
  $size--;
  print " $name: array[0..$size] of $type; n";
}
sub simpleType() {
  my ($name, $type) = @_;
  print " $name: $type; n";
}
1;

تمرین ۱۴: از طراحی بر اساس قرارداد، صفحه ۱۱۸

چه چیزی قرارداد خوبی می‌سازد؟ هر کسی می‌تواند پیش‌ و پس‌شرط اضافه کند، اما آیا سودی دارند؟ بدتر، آیا واقعاً بیش از سود آسیب می‌زنند؟ برای مثال زیر و تمرین‌های ۱۵ و ۱۶ مشخص کنید قرارداد خوب، بد یا زشت است و چرا.

ابتدا مثال Eiffel. روتینی برای افزودن STRING به لیست دایره‌ای دوطرفه (پیش‌شرط‌ها با require و پس‌شرط‌ها با ensure برچسب خورده‌اند):

eiffel
-- Add a unique item to a doubly linked list,
-- and return the newly created NODE.
add_item (item : STRING) : NODE is
   require
      item /= Void                       -- '/=' is 'not equal'.
      find_item(item) = Void             -- Must be unique
   deferred                              -- Abstract base class.
   ensure
      result.next.previous = result      -- Check the newly
      result.previous.next = result      -- added node's links.
      find_item(item) = result           -- Should find it.
   end

پاسخ ۱۴: این مثال Eiffel خوب است. داده غیر null می‌خواهیم و تضمین می‌کنیم semantics لیست دایره‌ای دوطرفه رعایت شود. یافتن رشته ذخیره‌شده هم کمک می‌کند. چون کلاس deferred است، کلاس پیاده‌ساز از هر مکانیزم زیربنایی دلخواه استفاده می‌کند — اشاره‌گر، آرایه یا هر چیز دیگر — تا وقتی قرارداد را رعایت کند برای ما مهم نیست.

تمرین ۱۵: از طراحی بر اساس قرارداد، صفحه ۱۱۹

حالا مثالی در Java — تا حدی شبیه تمرین ۱۴. insertNumber عدد صحیح را در لیست مرتب درج می‌کند. پیش‌ و پس‌شرط‌ها مانند iContract برچسب خورده‌اند (به [URL 17] مراجعه کنید).

java
private int data[];
/**
  * @post data[index-1] < data[index] &&
  *       data[index] == aValue
  */
public Node insertNumber (final int aValue)
{
  int index = findPlaceToInsert(aValue);
  ...

پاسخ ۱۵: بد است. ریاضی بند index (index-1) روی شرایط مرزی مثل اولین ورودی کار نمی‌کند. پس‌شرط یک پیاده‌سازی خاص را فرض می‌کند؛ می‌خواهیم قراردادها انتزاعی‌تر از آن باشند.

تمرین ۱۶: از طراحی بر اساس قرارداد، صفحه ۱۱۹

قطعه‌ای از کلاس stack در Java. آیا قرارداد خوبی است؟

java
/**
  * @pre anItem != null   // Require real data
  * @post pop() == anItem // Verify that it's
  *                       // on the stack
  */
public void push(final String anItem)

پاسخ ۱۶: قرارداد خوب است، اما پیاده‌سازی بد. اینجا «Heisenbug» بدنام [URL 52] سر می‌کشد. برنامه‌نویس احتمالاً فقط اشتباه تایپی ساده کرد — pop به‌جای top. با وجود سادگی و ساختگی مثال، عوارض جانبی در assertionها (یا هر جای غیرمنتظره کد) تشخیص بسیار سخت می‌شوند.

تمرین ۱۷: از طراحی بر اساس قرارداد، صفحه ۱۱۹

مثال‌های کلاسیک DBC (مثل تمرین‌های ۱۴–۱۶) پیاده‌سازی ADT (Abstract Data Type) — معمولاً stack یا queue — را نشان می‌دهند. اما خیلی‌ها واقعاً چنین کلاس‌های سطح پایینی نمی‌نویسند.

پس برای این تمرین، رابطی برای مخلوط‌کن آشپزخانه طراحی کنید. در نهایت مخلوط‌کن مبتنی بر وب، Internet-enabled و CORBA-ified خواهد بود، اما فعلاً فقط رابط کنترل لازم است. ده تنظیم سرعت دارد (۰ یعنی خاموش). نمی‌توان خالی کار کرد، و سرعت فقط یک واحد در هر بار عوض می‌شود (یعنی از ۰ به ۱ و از ۱ به ۲، نه از ۰ به ۲).

این متدها را داریم. پیش‌ و پس‌شرط و invariant مناسب اضافه کنید.

int getSpeed()
void setSpeed(int x)
boolean isFull()
void fill()
void empty()

پاسخ ۱۷: امضاهای تابع را در Java با پیش‌ و پس‌شرط‌های برچسب‌خورده مانند iContract نشان می‌دهیم.

ابتدا invariant کلاس:

java
/**
  * @invariant getSpeed() > 0
  *        implies isFull()                      // Don't run empty
  * @invariant getSpeed() >= 0 &&
  *        getSpeed() < 10                       // Range check
  */

سپس پیش‌ و پس‌شرط‌ها:

java
/**
  * @pre Math.abs(getSpeed() - x) <= 1 // Only change by one
  * @pre x >= 0 && x < 10              // Range check
  * @post getSpeed() == x              // Honor requested speed
  */
public void setSpeed(final int x)
/**
  * @pre !isFull()                              // Don't fill it twice
  * @post isFull()                              // Ensure it was done
  */
void fill()
/**
  * @pre isFull()                               // Don't empty it twice
  * @post !isFull()                             // Ensure it was done
  */
void empty()

تمرین ۱۸: از طراحی بر اساس قرارداد، صفحه ۱۱۹

چند عدد در سری ۱..۲۱ وجود دارد؟

پاسخ ۱۸: ۲۱ عبارت در سری وجود دارد. اگر ۲۰ گفتید، تازه یک خطای fencepost (خطای «تعداد پایه‌ها») را تجربه کردید.

تمرین ۱۹: از برنامه‌نویسی قاطع، صفحه ۱۲۵

یک reality check سریع. کدام‌یک از این «غیرممکن»ها می‌تواند رخ دهد؟

  1. ماهی با کمتر از ۲۸ روز
  2. stat(".",&sb) == -1 (یعنی دسترسی به پوشه جاری ممکن نیست)
  3. در C++: a = 2; b = 3; if (a + b != 5) exit(1);
  4. مثلثی که مجموع زوایای داخلی آن ۱۸۰ درجه نباشد
  5. دقیقه‌ای که ۶۰ ثانیه نداشته باشد
  6. در Java: (a + 1) <= a

پاسخ ۱۹:

  1. سپتامبر ۱۷۵۲ فقط ۱۹ روز داشت. برای همگام‌سازی تقویم‌ها در اصلاح گریگوری انجام شد.
  2. پوشه ممکن است توسط فرایند دیگری حذف شده باشد، شاید اجازه خواندن نداشته باشید، &sb ممکن است نامعتبر باشد — تصویر را گرفتید.
  3. عمداً نوع a و b را مشخص نکردیم. operator overloading ممکن است +، = یا != را رفتار غیرمنتظره بدهد. همچنین a و b ممکن است alias یک متغیر باشند، پس انتساب دوم مقدار اول را بازنویسی می‌کند.
  4. در هندسه غیراقلیدسی، مجموع زوایای مثلث به ۱۸۰ درجه نمی‌رسد. مثلث روی سطح کره را تصور کنید.
  5. دقیقه‌های leap ممکن است ۶۱ یا ۶۲ ثانیه داشته باشند.
  6. overflow ممکن است نتیجه a + 1 را منفی کند (در C و C++ هم ممکن است).

تمرین ۲۰: از برنامه‌نویسی قاطع، صفحه ۱۲۵

یک کلاس ساده assertion checking برای Java توسعه دهید.

پاسخ ۲۰: کلاس بسیار ساده با یک متد static به نام TEST پیاده کردیم که اگر پارامتر condition false باشد پیام و stack trace چاپ می‌کند.

java
package com.pragprog.util;
import java.lang.System;             // for exit()
import java.lang.Thread;             // for dumpStack()
public class Assert {
    /** Write a message, print a stack trace and exit if
      * our parameter is false.
      */
    public static void TEST(boolean condition) {
      if (!condition) {
         System.out.println("==== Assertion Failed ====");
         Thread.dumpStack();
         System.exit(1);
      }
    }
    // Testbed. If our argument is 'okay', try an assertion that
    // succeeds, if 'fail' try one that fails
    public static final void main(String args[]) {
      if (args[0].compareTo("okay") == 0) {
        TEST(1 == 1);
      }
      else if (args[0].compareTo("fail") == 0) {
        TEST(1 == 2);
      }
      else {
        throw new RuntimeException("Bad argument");
      }
    }
}

تمرین ۲۱: از چه وقت از استثناها استفاده کنیم، صفحه ۱۲۸

هنگام طراحی کلاس container جدید، شرایط خطای زیر را شناسایی می‌کنید:

  1. حافظه برای عنصر جدید در روتین add موجود نیست
  2. ورودی درخواستی در روتین fetch پیدا نشد
  3. اشاره‌گر null به روتین add داده شده

با هر کدام چه کنید؟ خطا تولید شود، استثنا raise شود، یا شرط نادیده گرفته شود؟

پاسخ ۲۱: تمام شدن حافظه شرایط استثنایی است، پس حالت (۱) باید استثنا raise کند.

نیافتن ورودی احتمالاً کاملاً عادی است. برنامه‌ای که کلاس collection ما را فراخوانی می‌کند ممکن است قبل از افزودن duplicate احتمالی بررسی کند آیا ورودی هست. حالت (۲) را فقط خطا برگردانیم.

حالت (۳) مشکل‌دارتر است — اگر مقدار null برای برنامه معنا دارد، ممکن است منطقی به container اضافه شود. اما اگر ذخیره null بی‌معناست، احتمالاً باید استثنا پرتاب شود.

تمرین ۲۲: از چگونه منابع را متعادل کنیم، صفحه ۱۳۶

برخی توسعه‌دهندگان C و C++ پس از آزاد کردن حافظه‌ای که اشاره‌گر به آن اشاره می‌کند، اشاره‌گر را NULL می‌کنند. چرا این ایده خوبی است؟

پاسخ ۲۲: در بیشتر پیاده‌سازی‌های C و C++ راهی برای بررسی اینکه اشاره‌گر واقعاً به حافظه معتبر اشاره می‌کند نیست. اشتباه رایج آزاد کردن بلوک حافظه و بعداً در برنامه به آن ارجاع دادن است. تا آن زمان حافظه ممکن است برای هدف دیگری دوباره allocate شده باشد. با NULL کردن اشاره‌گر، برنامه‌نویسان امیدوارند از این ارجاعات rogue جلوگیری کنند — در بیشتر موارد dereference اشاره‌گر NULL خطای runtime تولید می‌کند.

تمرین ۲۳: از چگونه منابع را متعادل کنیم، صفحه ۱۳۶

برخی توسعه‌دهندگان Java پس از اتمام استفاده از شیء، متغیر شیء را null می‌کنند. چرا این ایده خوبی است؟

پاسخ ۲۳: با null کردن ارجاع، تعداد اشاره‌گرها به شیء referenced یکی کم می‌شود. وقتی این شمارش به صفر برسد، شیء واجد شرایط garbage collection است. null کردن ارجاع‌ها برای برنامه‌های long-running مهم است؛ برنامه‌نویسان باید مطمئن شوند مصرف حافظه با گذشت زمان افزایش نمی‌یابد.

تمرین ۲۴: از جداسازی و قانون دمتر، صفحه ۱۴۳

مفهوم decoupling فیزیکی را در جعبه صفحه ۱۴۲ بحث کردیم. کدام یک از فایل‌های header C++ زیر به بقیه سیستم tightly coupledتر است؟

person1.h:                           person2.h:
    #include "date.h"                    class Date;
    class Person1 {                      class Person2 {
    private:                             private:
      Date myBirthdate;                    Date *myBirthdate;
    public:                              public:
      Person1(Date &birthDate);            Person2(Date &birthDate);
      // ...                               // ...

پاسخ ۲۴: فایل header باید رابط بین پیاده‌سازی متناظر و بقیه دنیا را تعریف کند. خود header نیازی به دانستن درون Date ندارد — فقط باید به کامپایلر بگوید سازنده Date را پارامتر می‌گیرد. پس مگر در توابع inline از Date استفاده شود، قطعه دوم کافی است.

مشکل قطعه اول چیست؟ در پروژه کوچک شاید هیچ — جز اینکه غیرضروری همه چیزهایی که Person1 استفاده می‌کنند header Date را هم include می‌کنند. وقتی این الگو در پروژه رایج شود، include یک header عملاً بیشتر سیستم را include می‌کند — کندی جدی کامپایل.

تمرین ۲۵: از جداسازی و قانون دمتر، صفحه ۱۴۳

برای مثال زیر و تمرین‌های ۲۶ و ۲۷ مشخص کنید آیا فراخوانی‌های متد نشان‌داده‌شده طبق قانون دمتر مجازند. این اولی در Java است.

java
public void showBalance(BankAccount acct) {
  Money amt = acct.getBalance();
  printToScreen(amt.printFormat());
}

پاسخ ۲۵: متغیر acct به‌عنوان پارامتر داده شده، پس فراخوانی getBalance مجاز است. اما amt.printFormat() مجاز نیست. amt را «نمی‌خواهیم» و به ما داده نشده. می‌توانیم coupling showBalance به Money را با چیزی شبیه این حذف کنیم:

java
void showBalance(BankAccount b) {
  b.printBalance();
}

تمرین ۲۶: از جداسازی و قانون دمتر، صفحه ۱۴۳

این مثال هم در Java است.

java
public class Colada {
  private Blender myBlender;
  private Vector myStuff;
    public Colada() {
      myBlender = new Blender();
      myStuff = new Vector();
    }
    private void doSomething() {
      myBlender.addIngredients(myStuff.elements());
    }
}

پاسخ ۲۶: چون Colada هر دو myBlender و myStuff را می‌سازد و مالک آن‌هاست، فراخوانی‌های addIngredients و elements مجازند.

تمرین ۲۷: از جداسازی و قانون دمتر، صفحه ۱۴۳

این مثال در C++ است.

cpp
void processTransaction(BankAccount acct, int) {
  Person *who;
  Money amt;
    amt.setValue(123.45);
    acct.setBalance(amt);
    who = acct.getOwner();
    markWorkflow(who->name(), SET_BALANCE);
}

پاسخ ۲۷: در این حالت processTransaction مالک amt است — روی stack ساخته شده. acct پارامتر است، پس setValue و setBalance مجازند. اما processTransaction مالک who نیست، پس who->name() نقض است. قانون دمتر پیشنهاد می‌کند این خط را با

cpp
markWorkflow(acct.name(), SET_BALANCE);

جایگزین کنیم.

کد در processTransaction نباید بداند کدام subobject داخل BankAccount نام را نگه می‌دارد — این دانش ساختاری نباید از قرارداد BankAccount نمایان شود. از BankAccount نام حساب را می‌خواهیم. خودش می‌داند نام کجاست (شاید در Person، Business یا Customer چندریختی).

تمرین ۲۸: از فرامتابرنامه‌نویسی، صفحه ۱۴۹

کدام‌یک از موارد زیر بهتر است به‌صورت کد داخل برنامه و کدام به‌صورت metadata خارجی نمایش داده شوند؟

  1. تخصیص پورت ارتباطی
  2. پشتیبانی ویرایشگر از برجسته‌سازی syntax زبان‌های مختلف
  3. پشتیبانی ویرایشگر از دستگاه‌های گرافیکی مختلف
  4. ماشین حالت برای parser یا scanner
  5. مقادیر و نتایج نمونه برای unit testing

پاسخ ۲۸: پاسخ قطعی نیست — سؤالات بیشتر برای فکر کردن بودند. با این حال نظر ما:

  1. تخصیص پورت ارتباطی. واضح است باید metadata باشد. اما تا چه سطح جزئیات؟ برخی برنامه‌های ارتباطی Windows فقط baud rate و پورت (مثلاً COM1 تا COM4) می‌دهند. شاید word size، parity، stop bit و duplex هم لازم باشد. تا جایی که عملی است ریزترین سطح جزئیات را بدهید.
  2. برجسته‌سازی syntax. باید metadata باشد. نمی‌خواهید فقط به‌خاطر keyword جدید Java کد را دستکاری کنید.
  3. دستگاه‌های گرافیکی. پیاده‌سازی صرفاً metadata احتمالاً سخت است. نمی‌خواهید برنامه را با چند driver فقط برای انتخاب یکی در runtime سنگین کنید. اما می‌توانید با metadata نام driver را بدهید و کد را پویا بار کنید. استدلال دیگر برای metadata خوانا توسط انسان: اگر برنامه driver ویدیوی ناکارآمد تنظیم کند، شاید نتوانید با همان برنامه برگردانید.
  4. ماشین حالت parser/scanner. بستگی به آنچه parse یا scan می‌کنید دارد. اگر داده‌ای سخت‌گیرانه توسط body استاندارد تعریف شده و بدون «قانون کنگره» عوض نمی‌شود، hard code کردن خوب است. اما در وضعیت volatileتر، تعریف جداول حالت خارجی مفید است.
  5. داده و نتیجه unit test. بیشتر برنامه‌ها این مقادیر را inline در harness تست تعریف می‌کنند، اما با بیرون بردن داده تست و تعریف نتایج قابل قبول از خود کد انعطاف بهتری می‌گیرید.

تمرین ۲۹: از فقط یک نماست، صفحه ۱۶۴

فرض کنید سیستم رزرو پرواز دارید که مفهوم پرواز را شامل می‌شود:

java
public interface Flight {
  // Return false if flight full.
  public boolean addPassenger(Passenger p);
  public void addToWaitList(Passenger p);
  public int getFlightCapacity();
  public int getNumPassengers();
}

اگر مسافری به wait list اضافه شود، وقتی جا باز شود خودکار روی پرواز قرار می‌گیرد.

گزارش‌گیری سنگینی همه پروازهای overbook یا پر را برای پیشنهاد پرواز اضافه بررسی می‌کند. خوب کار می‌کند اما ساعت‌ها طول می‌کشد.

می‌خواهیم انعطاف بیشتری در پردازش wait list داشته باشیم و باید کاری برای آن گزارش بزرگ بکنیم — خیلی طول می‌کشد. با ایده‌های این بخش این رابط را بازطراحی کنید.

پاسخ ۲۹: Flight را می‌گیریم و چند متد برای نگه‌داری دو فهرست listener اضافه می‌کنیم: یکی برای اطلاع wait list و دیگری برای پرواز پر.

java
public interface Passenger {
  public void waitListAvailable();
}
public interface Flight {
  ...
  public void addWaitListListener(Passenger p);
  public void removeWaitListListener(Passenger p);
    public void addFullListener(FullListener b);
    public void removeFullListener(FullListener b);
    ...
}
public interface BigReport extends FullListener {
  public void FlightFullAlert(Flight f);
}

اگر Passenger اضافه کنیم و به‌خاطر پر بودن پرواز ناموفق باشیم، اختیاری می‌توانیم Passenger را به wait list بگذاریم. وقتی جا باز شود waitListAvailable فراخوانی می‌شود. این متد می‌تواند Passenger را خودکار اضافه کند، نماینده خدمات مشتری را برای تماس بگیرد، یا هر کار دیگر — انعطاف رفتار per-customer داریم.

بعد می‌خواهیم BigReport هزاران رکورد را برای پروازهای پر نگردد. با ثبت BigReport به‌عنوان listener روی Flightها، هر Flight می‌تواند وقتی پر — یا تقریباً پر — شد گزارش دهد. کاربران گزارش زنده و به‌روز از BigReport می‌گیرند بدون ساعت‌ها انتظار مثل قبل.

تمرین ۳۰: از تخته سیاه، صفحه ۱۷۰

برای هر کاربرد زیر، سیستم blackboard مناسب است یا نه؟ چرا؟

  1. پردازش تصویر. چند فرایند موازی تکه‌های تصویر را بگیرند، پردازش کنند و تکه تمام‌شده را برگردانند.
  2. تقویم گروهی. افراد پراکنده در جهان، منطقه‌های زمانی و زبان‌های مختلف، برای جلسه هماهنگ می‌کنند.
  3. ابزار پایش شبکه. آمار عملکرد جمع می‌شود و گزارش مشکل. می‌خواهید agentهایی این اطلاعات را برای یافتن مشکل استفاده کنند.

پاسخ ۳۰:

  1. پردازش تصویر. برای زمان‌بندی ساده workload بین فرایندهای موازی، صف کار مشترک کافی است. اگر feedback دخیل باشد — نتایج یک تکه روی بقیه اثر بگذارد، مثل بینایی ماشین یا تبدیل 3D پیچیده — blackboard را در نظر بگیرید.
  2. تقویم گروهی. شاید مناسب باشد. جلسات و availability را روی blackboard می‌گذارید. موجودیت‌های مستقل، feedback تصمیم مهم است، شرکت‌کنندگان می‌آیند و می‌روند. شاید blackboard را partition کنید: کارکنان junior فقط دفتر نزدیک، HR فقط دفاتر انگلیسی‌زبان جهان، CEO همه چیز. انعطاف فرمت داده: فرمت یا زبانی که نمی‌فهمیم نادیده می‌گیریم. فقط برای دفاتی که با هم جلسه دارند فرمت‌های مختلف لازم است، نه transitive closure همه فرمت‌ها برای همه — coupling فقط جایی لازم کم می‌شود و محدودیت مصنوعی نمی‌گذاریم.
  3. پایش شبکه. شبیه برنامه وام/رهن صفحه ۱۶۸. گزارش مشکل کاربر و آمار خودکار روی blackboard. انسان یا agent blackboard را برای تشخیص خرابی شبکه تحلیل می‌کند: دو خطا شاید پرتو کیهانی، ۲۰٬۰۰۰ خطا یعنی سخت‌افزار. مثل کارآگاهان که معما را حل می‌کنند، چند موجودیت می‌توانند تحلیل کنند و ایده بدهند.

تمرین ۳۱: از برنامه‌نویسی بر اساس تصادف، صفحه ۱۷۶

آیا در قطعه C زیر تصادف‌هایی می‌بینید؟ فرض کنید این کد عمیق در یک روتین کتابخانه‌ای است.

c
fprintf(stderr,"Error, continue?");
gets(buf);

پاسخ ۳۱: چند مشکل بالقوه وجود دارد. فرض محیط tty. اگر از GUI فراخوانی شود که نه stderr و نه stdin باز است چه؟

دوم، gets مشکل‌دار است — هر تعداد کاراکتر دریافتی را در buffer می‌نویسد. کاربران مخرب از این برای buffer overrun در سیستم‌های مختلف استفاده کرده‌اند. هرگز از gets() استفاده نکنید.

سوم، فرض می‌کند کاربر انگلیسی می‌فهمد.

در پایان، هیچ کس عاقل چنین تعامل کاربری را در روتین کتابخانه‌ای دفن نمی‌کند.

تمرین ۳۲: از برنامه‌نویسی بر اساس تصادف، صفحه ۱۷۶

این قطعه C شاید گاهی روی بعضی ماشین‌ها کار کند. باز هم شاید نه. مشکل چیست؟

c
/* Truncate string to its last maxlen chars */
void string_tail(char *string, int maxlen) {
  int len = strlen(string);
  if (len > maxlen) {
    strcpy(string, string + (len - maxlen));
  }
}

پاسخ ۳۲: strcpy POSIX برای رشته‌های overlapping تضمین کار نمی‌کند. شاید روی بعضی معماری‌ها اتفاقاً کار کند — فقط بر اساس تصادف.

تمرین ۳۳: از برنامه‌نویسی بر اساس تصادف، صفحه ۱۷۷

این کد از suite ردیابی Java عمومی است. تابع رشته‌ای در log file می‌نویسد. unit test را پاس می‌کند اما وقتی یکی از توسعه‌دهندگان وب استفاده می‌کند fail می‌شود. به چه تصادفی تکیه دارد؟

java
public static void debug(String s) throws IOException {
  FileWriter fw = new FileWriter("debug.log", true);
  fw.write(s);
  fw.flush();
  fw.close();
}

پاسخ ۳۳: در context applet با محدودیت امنیتی نوشتن روی دیسک محلی کار نمی‌کند. وقتی انتخاب بین GUI و غیر-GUI دارید، شاید بخواهید پویا محیط فعلی را بررسی کنید. اینجا شاید log را جای دیگری غیر از دیسک محلی بگذارید اگر در دسترس نیست.

تمرین ۳۴: از سرعت الگوریتم، صفحه ۱۸۳

مجموعه‌ای از روتین‌های مرتب‌سازی ساده کدنویسی کردیم که از وب‌سایت ما قابل دانلود است. روی ماشین‌های در دسترس اجرا کنید. آیا اعداد منحنی مورد انتظار را دنبال می‌کنند؟ درباره سرعت نسبی ماشین‌ها چه برمی‌آورید؟ اثر تنظیمات بهینه‌سازی کامپایل چیست؟ آیا radix sort واقعاً linear است؟

پاسخ ۳۴: نمی‌توانیم پاسخ مطلق بدهیم. چند نکته:

اگر نتایج منحنی صاف نیست، ببینید فعالیت دیگری بخشی از پردازنده را مصرف نمی‌کند. روی سیستم multiuser اعداد خوب نمی‌گیرید؛ حتی تنها کاربر باشید فرایندهای پس‌زمینه گاهی cycle می‌گیرند. حافظه را هم بررسی کنید: اگر برنامه swap استفاده کند، عملکرد سقوط می‌کند.

با کامپایلرها و سطح بهینه‌سازی مختلف آزمایش کنید. بهینه‌سازی تهاجمی گاهی speed-up چشمگیر می‌دهد. روی معماری‌های RISC پهن‌تر، کامپایلر سازنده اغلب از GCC قابل حمل پیشی می‌گیرد — احتمالاً به راز تولید کد کارآمد روی آن ماشین‌ها دسترسی دارد.

تمرین ۳۵: از سرعت الگوریتم، صفحه ۱۸۳

روتین زیر محتوای درخت دودویی را چاپ می‌کند. فرض کنید درخت متعادل است؛ تقریباً چقدر فضای stack هنگام چاپ درخت یک میلیون عنصری مصرف می‌شود؟ (فرض کنید فراخوانی subroutine سربار stack قابل توجهی ندارد.)

c
void printTree(const Node *node) {
  char buffer[1000];
    if (node) {
      printTree(node->left);
        getNodeAsString(node, buffer);
        puts(buffer);
        printTree(node->right);
    }
}

پاسخ ۳۵: printTree حدود ۱۰۰۰ بایت stack برای buffer مصرف می‌کند. برای پایین رفتن درخت خودش را بازگشتی فراخوانی می‌کند و هر فراخوانی تو در تو ۱۰۰۰ بایت دیگر اضافه می‌کند. روی برگ‌ها هم خودش را فراخوانی می‌کند اما وقتی pointer NULL است فوراً خارج می‌شود. اگر عمق درخت d باشد، حداکثر نیاز stack تقریباً (d + 1) × 1000 بایت است.

درخت دودویی متعادل در هر سطح دو برابر عنصر دارد. درخت عمق d حدود 2^d - 1 عنصر دارد. درخت یک میلیون عنصری به عمق ۲۰ نیاز دارد.

پس انتظار داریم روتین حدود ۲۱٬۰۰۰ بایت stack مصرف کند.

تمرین ۳۶: از سرعت الگوریتم، صفحه ۱۸۳

آیا راهی برای کاهش نیاز stack روتین تمرین ۳۵ می‌بینید (جز کوچک کردن buffer)؟

پاسخ ۳۶: چند بهینه‌سازی:

printTree روی برگ‌ها خودش را فراخوانی می‌کند فقط برای خروج چون فرزندی نیست. آن فراخوانی عمق stack را حدود ۱۰۰۰ بایت زیاد می‌کند. tail recursion دوم (فراخوانی بازگشتی دوم) را هم حذف می‌کنیم، اگرچه بدترین حالت stack عوض نمی‌شود.

c
while (node) {
  if (node->left) printTree(node->left);
    getNodeAsString(node, buffer);
    puts(buffer);
    node = node->right;
}

بزرگ‌ترین سود: یک buffer مشترک برای همه فراخوانی‌ها. buffer را پارامتر به فراخوانی‌های بازگشتی بدهید — فقط ۱۰۰۰ بایت allocate می‌شود، هر عمقی.

c
void printTreePrivate(const Node *node, char *buffer) {
  if (node) {
    printTreePrivate(node->left, buffer);
        getNodeAsString(node, buffer);
        puts(buffer);
        printTreePrivate(node->right, buffer);
    }
}
void newPrintTree(const Node *node) {
  char buffer[1000];
    printTreePrivate(node, buffer);
}

تمرین ۳۷: از سرعت الگوریتم، صفحه ۱۸۳

در صفحه ۱۸۰ گفتیم binary chop از O(log n) است. می‌توانید ثابت کنید؟

پاسخ ۳۷: چند راه وجود دارد. یکی: اگر آرایه فقط یک عنصر دارد، حلقه اجرا نمی‌شود. هر iteration اضافه اندازه آرایه قابل جست‌وجو را دو برابر می‌کند. فرمول کلی اندازه آرایه n = 2^d است که d تعداد iterationهاست. log به پایه ۲ هر طرف: log₂(n) = d، پس O(log n).

تمرین ۳۸: از بازسازی، صفحه ۱۸۸

کد زیر چند بار به‌روز شده اما ساختار بهتر نشده. آن را refactor کنید.

c
if (state == TEXAS) {
  rate = TX_RATE;
  amt = base * TX_RATE;
  calc = 2*basis(amt) + extra(amt)*1.05;
}
else if ((state == OHIO) || (state == MAINE)) {
  rate = (state == OHIO) ? OH_RATE : ME_RATE;
  amt = base * rate;
  calc = 2*basis(amt) + extra(amt)*1.05;
  if (state == OHIO)
    points = 2;
}
else {
  rate = 1;
  amt = base;
  calc = 2*basis(amt) + extra(amt)*1.05;
}

پاسخ ۳۸: بازسازی ملایم: هر آزمون فقط یک بار، محاسبات مشترک. اگر 2*basis(...)*1.05 جای دیگر هم هست، شاید تابع بسازیم — اینجا نکردیم.

آرایه rate_lookup اضافه کردیم؛ ورودی‌های غیر از Texas، Ohio و Maine مقدار ۱ دارند. افزودن ایالت‌های دیگر آسان می‌شود. بسته به الگوی استفاده، شاید points هم lookup آرایه‌ای باشد.

c
rate = rate_lookup[state];
amt = base * rate;
calc = 2*basis(amt) + extra(amt)*1.05;
if (state == OHIO)
  points = 2;

تمرین ۳۹: از بازسازی، صفحه ۱۸۸

کلاس Java زیر باید چند شکل دیگر را پشتیبانی کند. کلاس را refactor کنید تا برای افزودن‌ها آماده شود.

java
public class Shape {
    public static final int SQUARE   = 1;
    public static final int CIRCLE   = 2;
    public static final int RIGHT_TRIANGLE = 3;
    private int    shapeType;
    private double size;
    public Shape(int shapeType, double size) {
      this.shapeType = shapeType;
      this.size      = size;
    }
    // ... other methods ...

    public double area() {
      switch (shapeType) {
      case SQUARE:   return size*size;
      case CIRCLE:   return Math.PI*size*size/4.0;
      case RIGHT_TRIANGLE: return size*size/2.0;
      }
      return 0;
    }
}

پاسخ ۳۹: وقتی enum (یا معادل Java) برای تفکیک variant نوع می‌بینید، اغلب با subclassing بهتر می‌شود:

java
public class Shape {
    private double size;
    public Shape(double size) {
      this.size = size;
    }
    public double getSize() { return size; }
}
public class Square extends Shape {
  public Square(double size) {
    super(size);
  }
    public double area() {
      double size = getSize();
      return size*size;
    }
}
public class Circle extends Shape {
  public Circle(double size) {
    super(size);
  }
    public double area() {
      double size = getSize();
      return Math.PI*size*size/4.0;
    }
}
// etc...

تمرین ۴۰: از بازسازی، صفحه ۱۸۹

این کد Java بخشی از framework است که در سراسر پروژه استفاده می‌شود. آن را عمومی‌تر و قابل گسترش‌تر refactor کنید.

java
public class Window {
    public Window(int width, int height) { ... }
    public void setSize(int width, int height) { ... }
    public boolean overlaps(Window w) { ... }
    public int getArea() { ... }
}

پاسخ ۴۰: در نگاه اول منطقی است پنجره width و height داشته باشد. اما آینده را تصور کنید: پنجره‌های با شکل دلخواه (سخت اگر Window همه چیز مستطیل را بداند).

شکل پنجره را از خود کلاس Window بیرون می‌آوریم:

java
public abstract class Shape {
    // ...
    public abstract boolean overlaps(Shape s);
    public abstract int getArea();
}
public class Window {
    private Shape shape;
    public Window(Shape shape) {
      this.shape = shape;
      ...
    }
    public void setShape(Shape shape) {
      this.shape = shape;
      ...
    }
    public boolean overlaps(Window w) {
      return shape.overlaps(w.shape);
    }
    public int getArea() {
      return shape.getArea();
    }
}

در این رویکرد delegation به‌جای subclassing: پنجره «نوعی» shape نیست — shape «دارد». هنگام refactor اغلب delegation مفید است.

می‌توانستیم interface Java هم بگذاریم که متدهای لازم برای shape را مشخص کند. ایده خوبی است: وقتی مفهوم shape را گسترش می‌دهید، کامپایلر درباره کلاس‌های affected هشدار می‌دهد. وقتی همه توابع کلاس دیگر را delegate می‌کنید، interface را این‌طور توصیه می‌کنیم.

تمرین ۴۱: از کدی که تست آن آسان است، صفحه ۱۹۷

برای رابط blender توصیف‌شده در پاسخ تمرین ۱۷ (صفحه ۲۸۹) یک test jig طراحی کنید. اسکریپت shell بنویسید که آزمایش رگرسیون blender را انجام دهد. عملکرد پایه، خطا و شرایط مرزی و تعهدات قراردادی را آزمایش کند. محدودیت تغییر سرعت چیست؟ رعایت می‌شوند؟

پاسخ ۴۱: ابتدا main به‌عنوان driver unit test اضافه می‌کنیم. آرگومان زبان بسیار کوچک و ساده می‌پذیرد: «E» برای خالی کردن، «F» برای پر کردن، رقم ۰–۹ برای سرعت و غیره.

java
public static void main(String args[]) {
    // Create the blender to test
    dbc_ex blender = new dbc_ex();
    // And test it according to the string on standard input
    try {
      int a;
      char c;
      while ((a = System.in.read()) != -1) {
        c = (char)a;
        if (Character.isWhitespace(c)) {
          continue;
        }
        if (Character.isDigit(c)) {
          blender.setSpeed(Character.digit(c, 10));
        }
        else {
          switch (c) {
            case 'F': blender.fill();
                       break;
            case 'E': blender.empty();
                       break;
            case 's': System.out.println("SPEED: " +
                                          blender.getSpeed());
                       break;
            case 'f': System.out.println("FULL " +
                                          blender.isFull());
                       break;
            default: throw new RuntimeException(
                       "Unknown Test directive");
          }
        }
      }
    }
    catch (java.io.IOException e) {
      System.err.println("Test jig failed: " + e.getMessage());
    }
    System.err.println("Completed blending n");
    System.exit(0);
}

سپس اسکریپت shell برای اجرای تست‌ها:

sh
#!/bin/sh
CMD="java dbc.dbc_ex"
failcount=0
expect_okay() {
    if echo "$*" | $CMD #>/dev/null 2>&1
    then
      :
    else
      echo "FAILED! $*"
      failcount=`expr $failcount + 1`
    fi
}
expect_fail() {
    if echo "$*" | $CMD >/dev/null 2>&1
    then
      echo "FAILED! (Should have failed): $*"
      failcount=`expr $failcount + 1`
    fi
}
report() {
  if [ $failcount -gt 0 ]
  then
     echo -e " n n*** FAILED $failcount TESTS n"
     exit 1 # In case we are part of something larger
  else
     exit 0 # In case we are part of something larger
  fi
}
#
# Start the tests
#
expect_okay F123456789876543210E # Should run thru
expect_fail F5    # Fails, speed too high
expect_fail 1     # Fails, empty
expect_fail F10E1 # Fails, empty
expect_fail F1238 # Fails, skips
expect_okay FE    # Never turn on
expect_fail F1E   # Emptying while running
expect_okay F10E # Should be ok
report            # Report results

تست‌ها بررسی می‌کنند تغییرات سرعت غیرمجاز تشخیص داده شود، خالی کردن در حین کار و غیره. در makefile گذاشتیم تا با تایپ

% make
% make test

کامپایل و تست رگرسیون اجرا شود.

تست با ۰ یا ۱ خارج می‌شود تا در تست بزرگ‌تر هم استفاده شود.

در نیازمندی‌ها چیزی درباره کنترل این component با script یا حتی زبان نبود. کاربر نهایی هرگز نمی‌بیند. اما ابزار قدرتمندی برای تست سریع و exhaustive داریم.

تمرین ۴۲: از گودال نیازمندی‌ها، صفحه ۲۱۱

کدام‌یک احتمالاً نیازمندی واقعی هستند؟ آن‌هایی که نیستند را تا جایی که ممکن است مفیدتر بازنویسی کنید.

  1. زمان پاسخ باید کمتر از ۵۰۰ ms باشد.
  2. جعبه‌های گفتگو پس‌زمینه خاکستری دارند.
  3. برنامه به‌صورت چند فرایند front-end و یک سرور back-end سازماندهی می‌شود.
  4. اگر کاربر در فیلد عددی کاراکتر غیرعددی وارد کند، سیستم beep می‌زند و نمی‌پذیرد.
  5. کد و داده برنامه باید در ۲۵۶kB جا بگیرند.

پاسخ ۴۲:

  1. این بیانیه شبیه نیازمندی واقعی است: محیط برنامه ممکن است محدودیت بگذارد.
  2. حتی اگر استاندارد سازمانی باشد، نیازمندی نیست. بهتر: «پس‌زمینه dialog باید توسط کاربر نهایی قابل پیکربندی باشد. در حالت پیش‌فرض خاکستری است.» بهتر از آن: «همه عناصر بصری (رنگ، فونت و زبان) باید توسط کاربر نهایی قابل پیکربندی باشند.»
  3. این نیازمندی نیست، معماری است. با چنین چیزی باید عمیق بروید بفهمید کاربر چه فکر می‌کند.
  4. نیازمندی زیربنایی احتمالاً نزدیک‌تر است: «سیستم از ورود نامعتبر در فیلدها جلوگیری می‌کند و هنگام چنین ورودی‌هایی کاربر را هشدار می‌دهد.»
  5. احتمالاً نیازمندی سخت است.