Post

CS2103T Week 10 & 11

CS2103T Week 10 Topics

Design Patterns

Design pattern: An elegant reusable solution to a commonly recurring problem within a given context in software design.

After repeated attempts at solving such problems, better solutions are discovered and refined over time. These solutions are known as design patterns, a term popularized by the seminal book Design Patterns: Elements of Reusable Object-Oriented Software by the so-called “Gang of Four” (GoF) written by Eric Gamma, Richard Helm, Ralph Johnson, and John Vlissides.

The common format to describe a pattern consists of the following components:

  • Context: The situation or scenario where the design problem is encountered.
  • Problem: The main difficulty to be resolved.
  • Solution: The core of the solution. It is important to note that the solution presented only includes the most general details, which may need further refinement for a specific context.
  • Anti-patterns (optional): Commonly used solutions, which are usually incorrect and/or inferior to the Design Pattern.
  • Consequences (optional): Identifying the pros and cons of applying the pattern.
  • Other useful information (optional): Code examples, known uses, other related patterns, etc.

Singleton Pattern (Class with no more than One Instance)

Context: Certain classes should have no more than just one instance (e.g. the main controller class of the system). These single instances are commonly known as singletons.

Problem: A normal class can be instantiated multiple times by invoking the constructor.

Solution: Make the constructor of the singleton class private, because a public constructor will allow others to instantiate the class at will. Provide a public class-level method to access the single instance.

Pros: easy to apply, effective in achieving its goal with minimal extra work, provides an easy way to access the singleton object from anywhere in the code base Cons: The singleton object acts like a global variable that increases coupling across the code base. In testing, it is difficult to replace Singleton objects with stubs (static methods cannot be overridden). In testing, singleton objects carry data from one test to another even when you want each test to be independent of the others.

Facade Pattern (Use overarching class to bring out functionality)

Context: Components need to access functionality deep inside other components.

Problem: Access to the component should be allowed without exposing its internal details. e.g. the UI component should access the functionality of the Logic component without knowing that it contains a Book class within it.

Solution: Include a Façade class that sits between the component internals and users of the component such that all access to the component happens through the Facade class.

Command Pattern (Have a general object e.g. Command for polymorphism to pass around without specifics)

Context: A system is required to execute a number of commands, each doing a different task. For example, a system might have to support Sort, List, Reset commands.

Problem: It is preferable that some part of the code executes these commands without having to know each command type. e.g., there can be a CommandQueue object that is responsible for queuing commands and executing them without knowledge of what each command does.

Solution: The essential element of this pattern is to have a general «Command» object that can be passed around, stored, executed, etc without knowing the type of command (i.e. via polymorphism).

MVC Pattern (Model, View, Controller)

Context: Most applications support storage/retrieval of information, displaying of information to the user (often via multiple UIs having different formats), and changing stored information based on external inputs.

Problem: The high coupling that can result from the interlinked nature of the features described above.

Solution: Decouple data, presentation, and control logic of an application by separating them into three different components: Model, View and Controller.

  • View: Displays data, interacts with the user, and pulls data from the model if necessary.
  • Controller: Detects UI events such as mouse clicks and button pushes, and takes follow up action. Updates/changes the model/view when necessary.
  • Model: Stores and maintains data. Updates the view if necessary.

Typically, the UI is the combination of View and Controller. In a simple UI where there’s only one view, Controller and View can be combined as one class. There are many variations of the MVC model used in different domains. For example, the one used in a desktop GUI could be different from the one used in a web application.

Observer Pattern

Context: An object (possibly more than one) is interested in being notified when a change happens to another object. That is, some objects want to ‘observe’ another object.

Problem: The ‘observed’ object does not want to be coupled to objects that are ‘observing’ it.

Solution: Force the communication through an interface known to both parties.

Here is the generic description of the observer pattern:

  • Observer is an interface: any class that implements it can observe an Observable.
  • The Observable maintains a list of Observer objects.
  • Whenever there is a change in the Observable, the notifyObservers() operation is called that will call the update() operation of all Observers in the list.

Defensive Programming

A defensive programmer codes under the assumption “if you leave room for things to go wrong, they will go wrong”. Therefore, a defensive programmer proactively tries to eliminate any room for things to go wrong.

  • Enforcing compulsory associations
  • Enforcing 1-to-1 associations
  • Enforcing referential integrity

It is not necessary to be 100% defensive all the time. While defensive code may be less prone to be misused or abused, such code can also be more complicated and slower to run.

The suitable degree of defensiveness depends on many factors such as:

  • How critical is the system?
  • Will the code be used by programmers other than the author?
  • The level of programming language support for defensive programming
  • The overhead of being defensive

In contrast, consider Design By Contract:

Design by contract (DbC) is an approach for designing software that requires defining formal, precise and verifiable interface specifications for software components.

Suppose an operation is implemented with the behavior specified precisely in the API (preconditions, post conditions, exceptions etc.). When following the defensive approach, the code should first check if the preconditions have been met. Typically, exceptions are thrown if preconditions are violated. In contrast, the Design-by-Contract (DbC) approach to coding assumes that it is the responsibility of the caller to ensure all preconditions are met. The operation will honor the contract only if the preconditions have been met. If any of them have not been met, the behavior of the operation is “unspecified”.

Quality Assurance: Test Cases

Except for trivial SUTs, exhaustive testing is not practical because such testing often requires a massive/infinite number of test cases.

Every test case adds to the cost of testing. In some systems, a single test case can cost thousands of dollars e.g. on-field testing of flight-control software. Therefore, test cases need to be designed to make the best use of testing resources. In particular:

  • Testing should be effective i.e., it finds a high percentage of existing bugs e.g., a set of test cases that finds 60 defects is more effective than a set that finds only 30 defects in the same system.

  • Testing should be efficient i.e., it has a high rate of success (bugs found/test cases) a set of 20 test cases that finds 8 defects is more efficient than another set of 40 test cases that finds the same 8 defects.

For testing to be E&E, each new test you add should be targeting a potential fault that is not already targeted by existing test cases. There are test case design techniques that can help us improve the E&E of testing.

Positive versus negative test cases: A positive test case is when the test is designed to produce an expected/valid behavior. On the other hand, a negative test case is designed to produce a behavior that indicates an invalid/unexpected situation, such as an error message.

Black box versus glass box: Test case design can be of three types, based on how much of the SUT’s internal details are considered when designing test cases:

  • Black-box (aka specification-based or responsibility-based) approach: test cases are designed exclusively based on the SUT’s specified external behavior.
  • White-box (aka glass-box or structured or implementation-based) approach: test cases are designed based on what is known about the SUT’s implementation, i.e. the code.
  • Gray-box approach: test case design uses some important information about the implementation.

Testing based on use cases: Use cases can be used for system testing and acceptance testing. For example, the main success scenario can be one test case while each variation (due to extensions) can form another test case. However, note that use cases do not specify the exact data entered into the system.

To increase the E&E of testing, high-priority use cases are given more attention. For example, a scripted approach can be used to test high-priority test cases, while an exploratory approach is used to test other areas of concern that could emerge during testing.

Test Cases: Equivalence Partitioning

In general, most SUTs do not treat each input in a unique way. Instead, they process all possible inputs in a small number of distinct ways. That means a range of inputs is treated the same way inside the SUT.

Equivalence partitioning (EP) is a test case design technique that uses the above observation to improve the E&E of testing.

Equivalence partition (aka equivalence class): A group of test inputs that are likely to be processed by the SUT in the same way.

By dividing possible inputs into equivalence partitions you can,

  • avoid testing too many inputs from one partition. Testing too many inputs from the same partition is unlikely to find new bugs. This increases the efficiency of testing by reducing redundant test cases.
  • ensure all partitions are tested. Missing partitions can result in bugs going unnoticed. This increases the effectiveness of testing by increasing the chance of finding bugs.

Identifying EPs:

Equivalence partitions (EPs) are usually derived from the specifications of the SUT.

When deciding EPs of OOP methods, you need to identify the EPs of all data participants that can potentially influence the behaviour of the method, such as,

  • target object of the method call
  • input parameters of the method call
  • other data/objects accessed by the method such as global variables. This category may not be applicable if using the black box approach (because the test case designer using the black box approach will not know how the method is implemented).

Test Cases: Boundary Value Analysis

Boundary Value Analysis (BVA) is a test case design heuristic that is based on the observation that bugs often result from incorrect handling of boundaries of equivalence partitions. This is not surprising, as the end points of boundaries are often used in branching instructions, etc., where the programmer can make mistakes.

BVA suggests that when picking test inputs from an equivalence partition, values near boundaries (i.e. boundary values) are more likely to find bugs.

Boundary values are sometimes called corner cases.

Typically, you should choose three values around the boundary to test: one value from the boundary, one value just below the boundary, and one value just above the boundary. The number of values to pick depends on other factors, such as the cost of each test case.

CS2103T Week 11 Topics

Architectural Styles

Software architectures follow various high-level styles (aka architectural patterns), just like how building architectures follow various architecture styles.

Most applications use a mix of these architectural styles.

n-tier style, client-server style, event-driven style, transaction processing style, service-oriented style, pipes-and-filters style, message-driven style, broker style, …

n-tier Style

In the n-tier style, higher layers make use of services provided by lower layers. Lower layers are independent of higher layers. Other names: multi-layered, layered.

Client-Server Style

The client-server style has at least one component playing the role of a server and at least one client component accessing the services of the server. This is an architectural style used often in distributed applications.

Event-Driven Style

Event-driven style controls the flow of the application by detecting events from event emitters and communicating those events to interested event consumers. This architectural style is often used in GUIs.

Transaction Processing Style

The transaction processing style divides the workload of the system down to a number of transactions which are then given to a dispatcher that controls the execution of each transaction. Task queuing, ordering, undo etc. are handled by the dispatcher.

Service-Oriented Style

The service-oriented architecture (SOA) style builds applications by combining functionalities packaged as programmatically accessible services. SOA aims to achieve interoperability between distributed services, which may not even be implemented using the same programming language. A common way to implement SOA is through the use of XML web services where the web is used as the medium for the services to interact, and XML is used as the language of communication between service providers and service users.

Transaction Processing Style

Test Cases: Combining Multiple Inputs

An SUT can take multiple inputs. You can select values for each input (using equivalence partitioning, boundary value analysis, or some other technique).

Testing all possible combinations is effective but not efficient. If you test all possible combinations for the above example, you need to test 6x5x2x6=360 cases. Doing so has a higher chance of discovering bugs (i.e. effective) but the number of test cases will be too high (i.e. not efficient). Therefore, you need smarter ways to combine test inputs that are both effective and efficient.

  • The all combinations strategy generates test cases for each unique combination of test inputs.
  • The at least once strategy includes each test input at least once.
  • The all pairs strategy creates test cases so that for any given pair of inputs, all combinations between them are tested.
    • It is based on the observation that a bug is rarely the result of more than two interacting factors. The resulting number of test cases is lower than the all combinations strategy, but higher than the at least once approach.
  • The random strategy generates test cases using one of the other strategies and then picks a subset randomly (presumably because the original set of test cases is too big).

Useful Heuristics

Heuristic: Each valid input at least once in a positive test case Heuristic: No more than one invalid input in a test case

Other QA Techniques

Software Quality Assurance (QA) is the process of ensuring that the software being built has the required levels of quality.

Quality Assurance = Validation + Verification

QA involves checking two aspects:

  • Validation: are you building the right system i.e., are the requirements correct?
  • Verification: are you building the system right i.e., are the requirements implemented correctly?

While testing is the most common activity used in QA, there are other complementary techniques such as static analysis, code reviews, and formal verification.

Formal Verification

Formal verification uses mathematical techniques to prove the correctness of a program.

  • Formal verification can be used to prove the absence of errors. In contrast, testing can only prove the presence of errors, not their absence.
  • It requires highly specialized notations and knowledge which makes it an expensive technique to administer. Therefore, formal verifications are more commonly used in safety-critical software such as flight control systems.

Reuse

Reuse is a major theme in software engineering practices. By reusing tried-and-tested components, the robustness of a new software system can be enhanced while reducing the manpower and time requirement.

Reusing External Code

While you may be tempted to use many libraries/frameworks/platforms that seem to crop up on a regular basis and promise to bring great benefits, note that there are costs associated with reuse. Here are some:

  • The reused code may be an overkill, increasing the size of, and/or degrading the performance of, your software.
  • The reused software may not be mature/stable enough to be used in an important product. That means the software can change drastically and rapidly, possibly in ways that break your software.
  • Non-mature software has the risk of dying off as fast as they emerged, leaving you with a dependency that is no longer maintained.
  • The license of the reused software (or its dependencies) restrict how you can use/develop your software.
  • The reused software might have bugs, missing features, or security vulnerabilities that are important to your product, but not so important to the maintainers of that software, which means those flaws will not get fixed as fast as you need them to.
  • Malicious code can sneak into your product via compromised dependencies.

API: Application Programming Interface

An Application Programming Interface (API) specifies the interface through which other programs can interact with a software component. It is a contract between the component and its clients.

An API should be well-designed (i.e. should cater for the needs of its users) and well-documented.

When you write software consisting of multiple components, you need to define the API of each component.

One approach is to let the API emerge and evolve over time as you write code.

Another approach is to define the API up-front. Doing so allows us to develop the components in parallel.

You can use UML sequence diagrams to analyze the required interactions between components in order to discover the required API.

Libraries

A library is a collection of modular code that is general and can be used by other programs.

These are the typical steps required to use a library:

  • Read the documentation to confirm that its functionality fits your needs.
  • Check the license to confirm that it allows reuse in the way you plan to reuse it. For example, some libraries might allow non-commercial use only.
  • Download the library and make it accessible to your project. Alternatively, you can configure your dependency management tool to do it for you.
  • Call the library API from your code where you need to use the library’s functionality.

Frameworks

The overall structure and execution flow of a specific category of software systems can be very similar. The similarity is an opportunity to reuse at a high scale.

A software framework is a reusable implementation of a software (or part thereof) providing generic functionality that can be selectively customized to produce a specific application.

Some frameworks provide a complete implementation of a default behavior which makes them immediately usable.

A framework facilitates the adaptation and customization of some desired functionality.

JavaFX is a framework for creating Java GUIs. Tkinter is a GUI framework for Python.

Frameworks versus libraries

Although both frameworks and libraries are reuse mechanisms, there are notable differences:

  • Libraries are meant to be used ‘as is’ while frameworks are meant to be customized/extended. e.g., writing plugins for Eclipse so that it can be used as an IDE for different languages (C++, PHP, etc.), adding modules and themes to Drupal, and adding test cases to JUnit.

  • Your code calls the library code while the framework code calls your code. Frameworks use a technique called inversion of control, aka the “Hollywood principle” (i.e. don’t call us, we’ll call you!). That is, you write code that will be called by the framework, e.g. writing test methods that will be called by the JUnit framework. In the case of libraries, your code calls libraries.

Platforms

A platform provides a runtime environment for applications. A platform is often bundled with various libraries, tools, frameworks, and technologies in addition to a runtime environment but the defining characteristic of a software platform is the presence of a runtime environment.

Two well-known examples of platforms are JavaEE and .NET, both of which sit above the operating systems layer, and are used to develop enterprise applications. Infrastructure services such as connection pooling, load balancing, remote code execution, transaction management, authentication, security, messaging etc. are done similarly in most enterprise applications. Both JavaEE and .NET provide these services to applications in a customizable way without developers having to implement them from scratch every time.

Cloud Computing

Refer to course website on cloud computing.

Other UML Models

Refer to course website on other UML models.

This post is licensed under CC BY 4.0 by the author.