Skip to main content

End of University Year 2 Sem 2

ยท 4 min read

Thoughtsโ€‹

This is a retrospective of the second semester of my second year at NUS. It was a relatively relaxed semester, with me taking 5 modules that resulted in a very manageable workload. In some sense, I would also say that perhaps I got used to what needed to be done.

Module Reviewโ€‹

CS3281 Thematic Systems Project I (aka Open Source Mod)โ€‹

This was my highlight-of-the-semester module, and I truly enjoyed it ๐Ÿ˜„ (Kudos to Prof Damith for keeping the module alive by volunteering his time to deliver this module!) It taught me a lot about open-source development. Even though the project that I worked on is by no means a large-scale, well-established one, in some way that provided autonomy and a whole range of tasks to tackle. As someone who has taken CS3216 (aka Go build software projects mod), I would say the learning outcome is different, but this module is equally worth doing. Summarizing some of my thoughts on the module:

  • You get to work on an open-source project!
  • You get to participate in the routine tasks of an open-source project, such as raising (and triaging) issues, fixing bugs, reviewing PRs, improving documentation, proposing new features, and discussing implementation details etc.
  • The projects are generally well documented; or have rich context from the git history and public discussion in issues and PRs.
  • The project mentors will be very helpful and you will get to learn from them through PR reviews and discussions.
  • When you spot the not-so-good parts of the projects, you have the chance to improve them.
  • Working on school-based projects also lends you the opportunity to work with other students, as well as external contributors and even on external projects (especially upstream dependencies).

I spent a fairly consistent amount of time working on MarkBind, as you can see in the contribution graph: [graph]

You can find out about what I have done (my progress and knowledge-learned log) here.

One thing I learned about OSS: if you want to be a contributor, first become a user. That leads to so many opportunities to contribute, and new perspectives to look at the project.

CS3230 Design and Analysis of Algorithmsโ€‹

This module is what you would expect in an advanced data structure and algorithms class. While the concepts may be difficult, they turned out to be pretty interesting to know. I enjoyed learning and analyzing the algorithms, which were all quite fundamental. There's some stress from the weekly graded assignments, but in general, it was manageable.

CS3240 Interaction Designโ€‹

This module provides a good introduction to the field of interaction design. It covers topics such as user-centered design, usability, and accessibility. It's a good survey of the field, and it was a more design-oriented module than the other CS modules I have taken. Workload wise if you don't like working on wireframes, and prototypes on tools like Figma, it can be a bit of a drag. I personally had many occasions where I opened Figma and just can't get myself to work on the assignment. But I did enjoy the module and my output, which you can find in the write-up here.

ES2660 Communicating In The Information Ageโ€‹

This required module focuses on the theories, techniques, and skills related to effective communication in the context of Information Technology. It covers topics such as critical thinking, public speaking, and writing, and provides opportunities to practice these skills through tutorial activities and assignments. The workload is manageable, and the classroom atmosphere is relaxed.

One thing that I remember most about this module: the challenge of speaking impromptu on a given topic (Not that easy if you want to do it well).

(bonus: here's the guideline I used for impromptu speaking)

  • Essence of the prompt (Context, audience, purpose)
  • Stand (Agree, Disagree)
  • Key terms
  • Reasons for my stand
  • Evidence/Examples/Implications/applications/ramifications
  • Delve deeper(Consider alternative, consequences)
  • Conclusion

LSM1303 Animal Behaviourโ€‹

Pretty chill and fun module with an awesome prof (The Otterman!). It was a great gateway to learning more about animals, and even got to observe them out in the wild.

animal1 animal2 animal3 animal4

Crossing abstraction barrier between parent and child class

ยท 5 min read

Motivationโ€‹

This article is inspired by a question I received in a programming methodology class. In this class, in which we write Java code to solve programming exercises, we have the constraint that every attribute of a class should be private and final. It means there is no access to the field outside of the class, and no modification is allowed once this field is initialized. This strict requirement is put in place to enforce immutability when constructing a class object in Java.

Sooner or later, when the exercises get more complex, we tend to move on to an OOP solution whereby multiple classes are constructed and organized with the help of inheritance. The problem then arises when there is a need to access this private final field in the parent class from a subclass. What should we do then?

To give a concrete example, let's say we have the following classes:

class Parent {
private final int value;

Parent(int value) {
this.value = value;
}
}

class Child extends Parent {
Child(int value) {
super(value);
}

int add(int another) {
return super.value + another; // UNABLE TO ACCESS!
}
}

What should we do if the child class wants to access value from the parent?

Solutionsโ€‹

Change modifierโ€‹

The simplest way to deal with that is to change the access modifier from private to something else - perhaps public or protected. This solution can be legitimate depending on the context. In some cases, perhaps it is perfectly normal to expose this value to other classes.

Add a getter methodโ€‹

From the Oracle's Java tutorial on inheritance

A subclass does not inherit the private members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.

So, another possible solution is to have a getter method in the parent class and make that method public. This way child classes (and technically other classes) will have access via the getter. So a quick example will be:

class Parent {
private final int value;

Parent(int value) {
this.value = value;
}

public int getValue() {
return this.value;
}
}

class Child extends Parent {
Child(int value) {
super(value);
}

int add(int another) {
return super.getValue() + another; // CAN ACCESS!
}
}

Having a getter method can be beneficial in the sense that even though now a "private" field is exposed, you still have one layer of abstraction over it. The users of the getter method do not need to know how that value is generated, which can be manipulated (if needed) by some complex preprocessing steps in the getter method. Also, the underlying private field could change drastically and yet the users of the getter method are unaware.

Rethink code designโ€‹

Lastly, this problem may be a signal to rethink if there is a legitimate need to access a private final field. Given a parent-child relationship, sometimes it's difficult to be clear about which field/method should reside in which classes.

  • Would it be better to have the field in the child class instead?
  • Can we shift what the child class wanted to do with value into the parent class as a general method that the child class can inherit and possibly override?

A better code design might suggest that the private final field can stay as is, maintaining an abstraction barrier between the parent and the child class. One example solution is then:

class Parent {
private final int value;

Parent(int value) {
this.value = value;
}

int add(int another) {
return this.value + another;
}
}

class Child extends Parent {
Child(int value) {
super(value);
}

int add(int another) { // will work if this method is omitted as well,
return super.add(another); // as it will be inherited
}
}

Anti patternโ€‹

A problematic walkaround that some might come up with is to redeclare the same field in the child class.

class Parent {
private final int value;

Parent(int value) {
this.value = value;
}
}

class Child extends Parent {
private final int value;

Child(int value) {
super(value);
this.value = value;
}

int add(int another) {
return this.value + another; // will work but not recommended
}
}

This works but is arguably a bad design because it does not make use of inheritance to reduce any duplicates between shared properties. It also could result in the values (that meant to represent the same thing) going out of sync, especially if these fields were not declared as final.

Conclusionโ€‹

When I was asked the motivating question, my immediate response was: "make a public getter method". To which I was then asked a follow-up question:

  • Why do we resort to using a public getter method, when we want to keep the field private?

Which got me thinking:

  • Why can't private fields be inherited?

This article is a reminder for me to ask the "why" questions more often, and explore the reasons for the answers.

Intermediate GitHub CI Workflow Walk Through

ยท 8 min read

Motivationโ€‹

There are many interesting use cases out there for GitHub Actions. One of the main use cases is to create custom continuous integration (CI) workflows that are hosted and executed in your GitHub repository. I should add that that it's FREE for public repositories, with some usage limits/policies :)

With that introduction out of the way, here I am going to walk through a slightly more complex workflow that I am using in the open-source project MarkBind - A tool for generating static websites from Markdown-like syntax. P.S I am an active dev there so feel free to show your support by giving it a star or using it to create course/documentation websites today!

Requirementsโ€‹

The reason why I would like to explain the workflow in detail here is that I think some requirements are fairly typical and it could serve as an example of a common CI script. Of course, the main reference that I would recommend is the official documentation by GitHub Also, this goes without saying that what I presented here is not the only way to do things.

So, let's talk about what I want the workflow to do:

  • Run tests
    • Run it whenever someone makes a PR against the master branch
    • Run it in all major OSes
      • To ensure that it ain't just working on my machine
    • Also run it when a PR is merged, or when someone commits directly to the master branch
      • Sometimes senior devs might directly commit a quick fix to the master branch and this should still trigger the tests
  • Deploy developer guide
    • Run it whenever a PR is merged, or when someone commits directly to the master branch
      • This is similar to the test runs but it is only triggered when a PR is merged (not when it is created)
      • The rationale for updating the developer guide per PR merge is so that our developer guide is always up-to-date with the latest development of the project, which could be slightly ahead of the released version
  • Deploy user guide
    • Run it whenever we release a new version
      • Some context: when we release a new version we will push a tag of the master branch to GitHub. Hint: this is how we are going to trigger this step

Codeโ€‹

Now we know the what, let me share the how. First, here's a brief summary of what we need to know about workflows:

  • A workflow can have multiple jobs that can run sequentially or in parallel (this is the default)
  • Each job can have multiple steps that run sequentially.
  • Both jobs and steps can be configured to run under certain conditions.

Testโ€‹

So to achieve just what we need for the test, we can do something like this:

name: CI
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: npm i -g npm@8.3.1
- run: npm run setup
- run: npm run test

Some explanations of the syntax used:

  • on.push.branches says that the workflow is only triggered when pushing to master, which is what happens when a PR is merged
  • on.pull_request says that the workflow will run when someone sends over a PR, which is nice to ensure that the changes don't break the build
  • the use of strategy and matrix is pretty much boilerplate code that is used to specify that the job named test will run on all three OSes.
    • This will run the tests in Ubuntu, macOS, and Windows, in parallel. It will by default fail-fast to stop the other two test runs if one of them failed unexpectedly.

Dev Guideโ€‹

To achieve just what we need for the developer guide update, we can do something like the following:

name: CI
on:
push:
branches:
- master
jobs:
deploy-docs:
# disabled on forks
if: github.repository == 'MarkBind/markbind'
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: npm i -g npm@8.3.1
- run: npm run setup
- name: Deploy DG on any commit to master, to markbind.org/devdocs
run: >-
npm run build:web &&
npm run build:dg &&
npm run deploy:dg

Now we reach the fun part... how to include the above with the test job so that it only runs when all tests have passed?

Here's my approach:

# code for test job omitted
deploy-docs:
needs: test
# disabled on forks
if: github.event_name == 'push' && github.repository == 'MarkBind/markbind'
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: npm i -g npm@8.3.1
- run: npm run setup
- name: Deploy DG on any commit to master, to markbind.org/devdocs
run: >-
npm run build:web &&
npm run build:dg &&
npm run deploy:dg

There is quite a bit of stuff here, so here's a summary:

  • I have defined another job named deploy-docs
  • I specified it to only run if the previous job test is done and successful by doing needs: test
  • I added a check to ensure that this job, unlike the test, will not run for pending PRs.
    • if: github.event_name == 'push' && github.repository == 'MarkBind/markbind'
    • it first checks if it is a push event (and not PR)
    • it then checks if the repository is the root repository
      • this is added to ensure that forks of the repo do not execute this job because they have no permission/access to publish the developer/user guides.

The rest is just set up and deploy commands that you may ignore.

User Guideโ€‹

Lastly, let's deal with the user guide which only needs to run per release.

To achieve just what we need for the user guide update, we can do something like the following:

name: CI
on:
push:
branches:
- master
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
deploy-docs:
# disabled on forks
if: github.repository == 'MarkBind/markbind'
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: npm i -g npm@8.3.1
- run: npm run setup
- name: Deploy UG on release, to markbind.org
if: github.ref_type == 'tag'
run: >-
npm run build:ug &&
npm run deploy:ug

To integrate it into the entire workflow, the following changes are required (full script at the end):

name: CI
on:
push:
branches:
- master
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches:
- master

# some code in between
- name: Deploy UG on release, to markbind.org
if: github.ref_type == 'tag'
run: >-
npm run build:ug &&
npm run deploy:ug
  • The addition of on.push.tags ensures that when a new tag on the master branch is pushed to GitHub, as part of making a new release, will trigger the workflow.
    • This runs the test job and the developer guide deployment step as well.
      • It could easily be turned off such that only the user guide step is run.
    • the v[0-9]+.[0-9]+.[0-9]+ is a glob pattern to match semantic versioning tags.
  • The if: github.ref_type == 'tag' in the user guide step will ensure that if this is just a PR merge or a push event to master, the step will be skipped.
    • The details of the github object that I am accessing here is available here

Full Scriptโ€‹

Putting everything together:

name: CI
on:
push:
branches:
- master
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches:
- master
jobs:
test:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: npm i -g npm@8.3.1
- run: npm run setup
- run: npm run test
deploy-docs:
needs: test
# disabled on forks
if: github.event_name == 'push' && github.repository == 'MarkBind/markbind'
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: npm i -g npm@8.3.1
- run: npm run setup
- name: Deploy DG on any commit to master, to markbind.org/devdocs
run: >-
npm run build:web &&
npm run build:dg &&
npm run deploy:dg
- name: Deploy UG on release, to markbind.org
if: github.ref_type == 'tag'
run: >-
npm run build:ug &&
npm run deploy:ug

Conclusionโ€‹

Now that CI script is written and workflow is automated, robots can finally take my job.