CAB201 – Programming Principles Study Notes

CAB201 – Programming Principles

TEQSA Provider Information

  • TEQSA Provider ID: PRV12079

  • Australian University: CRICOS No. 00213J


Acknowledgement of Traditional Owners

  • QUT acknowledges the Turrbal and Yugara as the First Nations owners of the lands where QUT now stands.

  • Respect is paid to their Elders, lores, customs, and creation spirits.

  • Recognition is given that these lands have always served as places of teaching, research, and learning.

  • Importance of Aboriginal and Torres Strait Islander people in the QUT community is acknowledged.


Refactoring

Definition
  • Refactoring is the process of improving or cleaning code while preserving behavior.

    • Involves small transformations that keep the behavior intact to external systems.

    • Each transformation is minor but collectively has a significant impact on the overall code structure.

    • Reduced likelihood of severe system failures due to smaller incremental changes.

    • The system remains operational after each transformation.

Benefits of Refactoring
  • Initial development can be rapid (e.g., rapid prototyping), allowing for quick functionality.

  • Refactoring enhances code by:

    • Making it more readable via friendly identifier names, self-documenting methods, and reducing duplication.

    • Increasing maintainability through the elimination of redundant code.

    • Allowing easier detection of bugs and errors.

    • Optimizing performance by substituting inefficient logic with efficient alternatives.

Refactoring to Mitigate Technical Debt
  • Over time, software needs to be maintained and extended, potentially leading to a degradation of design quality.

    • Adding new features or fixing bugs can lead to changes that were not in the original design, resulting in technical debt.

    • Technical debt increases maintenance difficulty, time consumption, and the potential for errors.

    • Refactoring serves to address this issue by preserving existing functionality while improving the underlying code and design.

Understanding Duplication
  • A primary goal of refactoring is to eliminate duplication, which can occur on two levels:

    • Duplicated code (syntax level).

    • Duplicated functionality or information (semantic level).

  • Instances of duplication should be replaced with method invocations or loops.

  • Issues arise from duplication as if a bug is found in the duplicated logic, all instances must be rectified.


Selections from the Refactoring Menu

  • Various methods of refactoring, as catalogued by Martin Fowler’s refactoring.com and refactoring.guru:

    • Some refactorings are minimal, while others embody solid software engineering practices.

    • Examples of trivial refactorings include:

    • Add Parameter to Method

    • Rename Method

    • More significant refactorings include:

    • Extract Method

    • Collapse Hierarchy

    • All refactorings focus on cleaning up the code to improve quality.


Refactoring in Visual Studio

  • Visual Studio provides a limited yet source-aware set of refactorings through its Edit -> Refactor menu, which modify code without altering behavior.

  • The Undo feature (Ctrl+Z) is available if a refactoring change does not yield the desired effect.

Example Refactorings
Decompose Conditional
  • Example of a complicated conditional statement:

  if (date.Before(SUMMER_START) || date.After(SUMMER_END))
      charge = quantity * _winterRate + _winterServiceCharge;
  else
      charge = quantity * _summerRate;
  • This could be refactored to:

  if (NotSummer(date))
      charge = WinterCharge(quantity);
  else
      charge = SummerCharge(quantity);
Extract Method
  • Refactored code fragment:

  void PrintOwing() {
      PrintBanner();
      Console.WriteLine("name: {0}", _name);
      Console.WriteLine("amount {0}", GetOutstanding());
  }
  • Becomes:

  void PrintOwing() {
      PrintBanner();
      PrintDetails(GetOutstanding());
  }
  void PrintDetails(double outstanding) {
      Console.WriteLine("name: {0}", _name);
      Console.WriteLine("amount: {0}", outstanding);
  }
Encapsulate Collection
  • Refactoring collections for enhanced encapsulation:

    • Transforming a method to return a read-only view, e.g.:

  class Student {
      private List<Unit> _units;
      public IList<Unit> Units => _units.AsReadOnly();
      public void AddUnit(Unit c) { _units.Add(c); }
      public void RemoveUnit(Unit c) { _units.Remove(c); }
  }
Replace Error Code with Exception
  • Refactoring to improve error handling:

  int Withdraw(int amount) {
      if (amount > _balance)
          return -1;
      else {
          _balance -= amount;
          return 0;
      }
  }
  • Becomes:

  void Withdraw(int amount) {
      if (amount > _balance)
          throw new OverdrawException();
      _balance -= amount;
  }
Split Loop
  • When a loop performs multiple operations:

  void PrintValues() {
      double averageAge = 0.0;
      double totalSalary = 0.0;
      for (int i = 0; i < people.Length; i++) {
          averageAge += people[i].Age;
          totalSalary += people[i].Salary;
      }
      averageAge /= people.Length;
      Console.WriteLine(averageAge);
      Console.WriteLine(totalSalary);
  }
  • This can be split into two separate loops:

  void PrintValues() {
      double averageAge = 0.0;
      for (int i = 0; i < people.Length; i++) {
          averageAge += people[i].Age;
      }
      averageAge /= people.Length;
      double totalSalary = 0.0;
      for (int i = 0; i < people.Length; i++) {
          totalSalary += people[i].Salary;
      }
      Console.WriteLine(averageAge);
      Console.WriteLine(totalSalary);
  }

Summary of Refactoring

  • Refactor to enhance code quality, simplify structure, increase elegance, optimize performance, and reduce memory footprint.

  • Refactoring reduces the risk of errors by maintaining clear code structure and functionality as defined by the API.

  • Note that refactoring can be complex and should only be performed with safety measures such as unit tests in place.


Generics

Definition
  • Generics allow classes, interfaces, methods, and delegates to be designed to work with various types, enhancing code reusability, especially in collections and algorithms.

  • They involve specifying one or more customizable types when creating classes or methods, leading to improved type safety and reduced necessity for downcasting.

Generic Classes
  • While declaring a generic class/interface, type placeholders (unbound types) are specified.

  • When utilized, those types need to be bound to actual types, which replaces all instances of the unbound types throughout the class (including in method arguments, return types, and fields).

  • Operations can only be performed on instances of the bound type that could logically be executed on an Object type.

Generic Methods
  • Methods can also be defined generically:

  public static T GetMinimum<T>(T first, T second)
  • Type inference allows the compiler to ascertain types from parameters, eliminating the need for explicit type specification in many cases.

  • Delegates can leverage generics similarly to methods.


Generics & Inheritance

  • Generic classes can inherit from non-generic classes and vice-versa, requiring unbound types from parent classes to be specified when inheriting.

  • For example:

  class MyClass : GenericClass<int>
  class MyGeneric<T> : OtherClass
  class MyGeneric<T> : OtherGeneric<T>
  • This is instrumental in creating generic collection types by inheriting interfaces such as IEnumerable.

Substitutability with Generics
  • Generic types can be substituted for ancestor types in similar fashions to any other types, though the rules may be complex.

  • Example:

  class A<T>
  class B<T> : A<T>
  A<int> value = new B<int>();
Generic Type Constraints
  • The 'where' clause provides constraints on unbound types within a generic definition. This ensures members of constrained types can be utilized:

  class SortedList<T> where T : IComparable<T>
  • Constraints can include multiple interfaces, further unbound types can have unique clauses:

  class BSearch<A, B> where A : IComparable<A> where B : IEnumerable<A>

Run-Time Type Information (RTTI) and Downcasting

RTTI
  • Run-Time Type Information provides mechanisms for inspecting runtime types of objects, utilizing the 'is' operator, the 'as' operator for downcasting, and the GetType() method for type retrieval.

  • While sometimes useful, RTTI often violates OOP principles and should be a last resort.

Using 'is' Operator
  • The 'is' operator checks if an instance is of a particular type:

  if (value is string) { /* ... */ }
'as' Operator
  • The 'as' operator attempts to downcast an object, returning either the object if the type matches or null:

  string? s = value as string;
Downcasting with Cast Operator
  • The cast operator attempts downcasting and raises InvalidCastException if unsuccessful:

  string s = (string)value;
  • Consider utilizing 'is' before downcasting to ensure type compatibility.

Using typeof and GetType()
  • GetType() returns the runtime type of an instance:

  if (value.GetType() == typeof(string))
  • The typeof operator retrieves System.Type of a type, allowing for detailed type interrogation.


Collections

Definition
  • Collections serve as powerful abstractions where data type and storage methodology are decoupled.

  • Collections can impose constraints on stored object types and are often defined generically.

  • Non-generic versions (found in System.Collections) should be avoided due to lack of type safety.

Types of Collections
  1. Array

    • Special nature within the framework, typed but rigid (fixed size).

    • Fast access via index; cannot be resized post-creation.

  2. List

    • Generic, resizable array implementation. Supports appending items efficiently.

    • Size indicates actual item count; capacity reflects the allocated space.

  3. Dictionary

    • Functions as an associative array, utilizing a hash table.

    • Ensures keys are unique and facilitates rapid data retrieval.

  4. HashSet

    • Similar to a dictionary but retains only keys to ensure uniqueness.

  5. Other collections: LinkedList, Queue, Stack, PriorityQueue, SortedList, SortedDictionary, SortedSet.


Custom Collections

Creating Custom Collection Types
  • Although standard collections often suffice, there may be cases necessitating a custom solution due to specific performance or memory limitations.

Collection Interfaces
  • Implementing existing System.Collections.Generic interfaces aids in creating collections that can seamlessly integrate with existing code, providing varying levels of capabilities. Common interfaces include:

    • IEnumerable

    • ICollection

    • IList

    • ISet

    • IDictionary

Underlying Data Structures
  • Utilize arrays, collections (e.g., List), or linked list structures for storing collection items, selected based on specific requirements.


Collection Initializers and Indexers

Collection Initializers
  • C# syntax allows brief initialization of collections when fulfilling specific criteria (e.g., implementing IEnumerable and an appropriate Add method):

  List<int> count = new List<int>() { 1, 2, 3 };
Indexers
  • Enables collection access akin to arrays using the [ ] syntax, facilitating parameterized access similar to properties:

  public T this[int index] {
      get => array[index];
      set => array[index] = value;
  }

Enumerators and Coroutines

Enumerators
  • Custom collections implement IEnumerable and feature a method GetEnumerator(). This allows traversal of collections via foreach loops.

  • Enumerators maintain state to track the current and next items within a collection, impacting design choices based on collection type.

Implementing IEnumerable
  • Implementing this interface includes defining IEnumerator GetEnumerator() and handling the IEnumerator methods to provide accurate enumeration behavior.

Enumerator Invalidation
  • Enumerators may lose validity upon collection modifications. Caution is advised when altering collections during iterations.

Coroutines
  • Coroutines simplify enumerator handling by maintaining method continuation states via yield statements, allowing for streamlined enumerator construction.

  public static IEnumerable<T> ArrayIterate<T>(T[] array) {
      for (int i = 0; i < array.Length; i++) {
          yield return array[i];
      }
  }

Conclusion

  • Refactoring is essential for maintaining code quality throughout its lifecycle while employing generics enhances reusability, safety, and maintainability. Knowledge of collections, their implementations, and enumerators forms the foundation of organized programming practices in C#.