25-Comp-A6 Software Engineering · Undated paper
Nivaar worked solution (AI-drafted; not reviewed by a licensed engineer)
National Exams — 17-Comp-A6 Software Engineering. Three-hour, closed-book exam, no calculator permitted. Format: eight questions, candidates answer any five (all questions equal weight — each of the five counted questions is worth 20%; any five questions constitute a complete paper, and only the first five as they appear in the answer book are marked). All eight questions are solved below for completeness. The page-1 heading reads "National Exams.
Reference texts: Sommerville, Software Engineering (10th ed., Pearson) — software process models, object-oriented and function-oriented design, software reuse and portability, dependable/critical systems, distributed software engineering, configuration management, reliability metrics; Pressman, Software Engineering: A Practitioner's Approach (9th ed.) — supplementary process and testing coverage; Leveson, Safeware — hazard and fault-tree analysis for safety-critical software.
Question text not reproduced: the examination questions are © Engineers and Geoscientists BC. Open the official past paper (linked at the top of this page) to read the question, then follow the worked solution below.
Information hiding means a component exposes only a well-defined public interface and conceals its internal data representation and algorithms behind it, so that client code can depend on the interface's contract without knowing (or being able to depend on) how it is implemented. For reuse, its advantage is robustness to change: the component's internals can be re-implemented — for performance, for a new platform, or to fix a defect — without breaking any client, so a well-hidden component can be reused across many contexts as a stable black box. Its disadvantage is inflexibility at the margins: if a new context needs slightly different internal behaviour that the interface was never designed to expose, the component often cannot be adapted at all without modifying it directly, forcing either a fork or an awkward wrapper, and a strictly hidden representation can cost some run-time efficiency because every access goes through an operation call.
Inheritance lets a new class reuse an existing class's implementation and interface by deriving from it and overriding or adding only what differs. Its advantage is speed of reuse: a large body of correct, tested behaviour is obtained essentially for free, and polymorphism lets client code manipulate the base type while actually running the derived behaviour. Its disadvantages come from the coupling it creates between subclass and superclass. The first is the "fragile base class" problem: a change to the superclass's internals, even one that preserves its public interface, can silently break subclasses that depended on some undocumented aspect of the original implementation, and deep hierarchies become hard to understand and to modify safely. The second is message leakage: a subclass inherits every public operation its superclass defines, not only the ones that fit its own abstraction. Implementing a Stack by inheriting from a general-purpose List purely to reuse its storage code also hands the stack List's insertAt(i, x) and removeAt(i), so client code can bypass push/pop and break the LIFO discipline the stack exists to guarantee. In practice, information hiding is the safer default for reuse across module or team boundaries; inheritance works best for genuine "is-a" relationships inside a single, well-understood hierarchy, and composition (holding a private instance of the reused class and forwarding only chosen operations) is preferred whenever the reused class's full interface would otherwise leak through.
Portability from an 8-bit microcontroller to a 64-bit special-purpose processor is achieved by hiding the actual bit-width and internal representation of time behind an abstract interface expressed only in terms of calendar/clock concepts (year, month, day, hour, minute, second) — never in terms of a specific integer width, register layout, or hardware timer. The client code is written once against this interface; only the small implementation file behind it changes per target.
/* calendar_clock.h -- portable, machine-independent interface */
typedef struct CalendarClock CalendarClock; /* opaque handle: internal
representation is never
exposed to client code */
CalendarClock *cc_create(void);
void cc_destroy(CalendarClock *cc);
void cc_set(CalendarClock *cc, int year, int month, int day,
int hour, int minute, int second);
void cc_tick(CalendarClock *cc, int elapsed_seconds); /* advance time */
int cc_year(const CalendarClock *cc);
int cc_month(const CalendarClock *cc);
int cc_day(const CalendarClock *cc);
int cc_hour(const CalendarClock *cc);
int cc_minute(const CalendarClock *cc);
int cc_second(const CalendarClock *cc);
The implementation below needs nothing wider than a 16-bit unsigned int, so it compiles and runs unchanged on every target from the 8-bit micro upward. Time is held as the six calendar fields, and cc_tick advances one second at a time, carrying into the next field with a small compare-and-reset; this suits a clock driven by a once-per-second timer interrupt.
/* calendar_clock.c -- portable implementation, 8-bit and 16-bit arithmetic only */
#include <stdlib.h>
#include "calendar_clock.h"
struct CalendarClock {
unsigned char second, minute, hour; /* 0-59, 0-59, 0-23 */
unsigned char day, month; /* 1-31, 1-12 */
unsigned int year; /* only field wider than 8 bits */
};
static const unsigned char days_in[12] =
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
static int is_leap(unsigned int y)
{
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
static unsigned char month_length(const CalendarClock *cc)
{
if (cc->month == 2 && is_leap(cc->year))
return 29;
return days_in[cc->month - 1];
}
CalendarClock *cc_create(void)
{
CalendarClock *cc = malloc(sizeof *cc);
if (cc != NULL)
cc_set(cc, 2000, 1, 1, 0, 0, 0);
return cc;
}
void cc_destroy(CalendarClock *cc) { free(cc); }
void cc_set(CalendarClock *cc, int year, int month, int day,
int hour, int minute, int second)
{
cc->year = year; cc->month = month; cc->day = day;
cc->hour = hour; cc->minute = minute; cc->second = second;
}
static void tick_one_second(CalendarClock *cc)
{
if (++cc->second < 60) return;
cc->second = 0;
if (++cc->minute < 60) return;
cc->minute = 0;
if (++cc->hour < 24) return;
cc->hour = 0;
if (++cc->day <= month_length(cc)) return;
cc->day = 1;
if (++cc->month <= 12) return;
cc->month = 1;
++cc->year;
}
void cc_tick(CalendarClock *cc, int elapsed_seconds)
{
while (elapsed_seconds-- > 0)
tick_one_second(cc);
}
int cc_year(const CalendarClock *cc) { return cc->year; }
int cc_month(const CalendarClock *cc) { return cc->month; }
int cc_day(const CalendarClock *cc) { return cc->day; }
int cc_hour(const CalendarClock *cc) { return cc->hour; }
int cc_minute(const CalendarClock *cc) { return cc->minute; }
int cc_second(const CalendarClock *cc) { return cc->second; }
On a 64-bit special-purpose processor the same header can instead be satisfied by an implementation file that stores a single 64-bit signed "seconds since epoch" count, since 64-bit arithmetic is native and cheap there: cc_tick becomes one addition however many seconds have passed, and the six accessors decode the count with ordinary civil-calendar arithmetic. Because every client of the ADT calls only cc_set, cc_tick, and the six accessors, choosing between the two implementation files (or keeping the portable one everywhere) is invisible to client code: no client code anywhere in the system changes when the calendar/clock moves from machine to machine.
cc_create would return a pointer to a statically allocated instance instead of calling malloc; this is again invisible to client code. cc_set is assumed to receive a valid date; a production version would reject out-of-range fields.