Mar '18
14
Refactoring, Now With Generics!
Refactoring is one of the most satisfying programming tasks. It can be difficult, especially in a large or unfamiliar codebase, but I believe thinking critically about your code is beneficial. There is almost always a low-hanging fruit -- fix formatting, naming convention, remove duplication, etc. If you aren't careful however, you can easily refactor in circles and never actually improve anything.
"Quality" is completely subjective, so you should also spend time pondering where your opinions lay and why you think that way. Lively banter with coworkers about design choices and their reasons can be very informative, but I've found that there is no singular Correct Way, especially in coding. Each choice comes with trade-offs that may not be completely evident until much later. And what do you do when you realize six-months-ago You made a mistake? Refactor!
The Beast
Around the middle of last year, my team inherited a massive monolithic Java product and was tasked with stabilizing and improving the platform. My goal was to not only improve the perceived functionality, but also to clean up the code in as many ways as possible.
This project is made up of several layers of co-dependent libraries, and the data model (entities, whatever you like calling them) consists of several related types. One major eyesore is that each layer defines a specialized subclass for each of these types. Imagine a Website, which might have multiple Campaigns, and both are defined in the base data access library. A tracking library then builds on top of both of those types, introducing its own subclass of each. Then a messaging library builds on top of those, adding another pair of subclasses. Repeat about 7 times and welcome to my reality!
The immediate issue I have with these kinds of "shared" inheritance hierarchies, especially in a language like Java, is that you end up casting things everywhere. For example, given a core.Campaign
with a public core.Website getWebsite()
method, every time you use that method in a subclass of core.Campaign
, you get a core.Website
. Concretely, if you call getWebsite()
inside the tracking.Campaign
subclass, you will receive a core.Website
. Because of the shared hierarchy, it is expected that the actual instance you get will be an instance of tracking.Campaign
. You just have to cast it:
package com.acme.tracking; class Campaign extends com.acme.core.Campaign { public void contrivedExample() { // We are calling the getWebsite method defined in the core.Campaign superclass. // It returns a core.Website! But we "know" its really a tracking Website. We hope. Website website = (Website) getWebsite(); // do something worthwhile... } }
Right off the bat your olfactories are assaulted with the pungent aroma of wonky code. See, Java is a statically- and strongly-typed language, so this means that the compiler can generally do a great job alerting you to incompatible types. But the moment you're forced to cast, you remove any compile-time guarantees. You have introduced a possible runtime fault. Neat, huh?
The Beauty
Now, as you can imagine, this platform has a ton of baggage. We can't just go around changing APIs, we have upstream and downstream dependencies that rely on our code. That is, we can't just fix the issue at hand by removing the crazy cross-project inheritance hierarchy. That would break an uncountable (literally, we can't be sure) number of other projects. This design decision really is baked-in to the platform at this point, and trying to fight it too much is probably a waste of time.
But that doesn't mean you can't improve things!
A Short Aside
Back in prehistoric times, the folks working on Java decided to implement non-reified generics. Reification, in this context, describes the compiler's and runtime's ability to determine, track, and enforce type information. Java's implementation of generics is non-reified because the compiler actually strips type information from the generics-- the runtime cannot know what these types were originally!
In a nutshell, when you write List<String>
, the compiler can use the type parameter (String
in this case) to ensure that no tomfoolery occurs with types at compile-time. But the runtime itself has no knowledge of this information and therefore cannot make any assurances about what a generic type contains when the code is actually executed. You may yourself have had the pleasure of experiencing an Integer
in your List<String>
in not-very-unusual circumstances.
You might imagine generics as sort of the "evil" twin of a type-cast operation. Instead of checking types at runtime with a cast, we do it at compile time and skip the runtime check. But, if you're like me, you vastly prefer knowing about incorrect types at compile-time, not at run-time (sometimes known as "production"). So in my opinion, this trade-off is worth it.
Back to the Beauty
Is there a way we can utilize generics to help ease the situation and provide stronger compile-time guarantees? In this next example, we are looking at another type that represents normalized information about an incoming HTTP request. This CampaignContext
is passed around to various services that need to be able to get the current request's core.Campaign
(or a subclass thereof).
package com.acme.app.context; public abstract class CampaignContext implements com.acme.core.CampaignContext { private com.acme.core.Campaign campaign; public com.acme.core.Campaign getCampaign() { return campaign; } public void setCampaign(com.acme.core.Campaign campaign) { this.campaign = campaign; } }
Notice that this class exists in an application-- very obviously denoted by the com.acme.app
package name-- nothing else is using it beyond this app. Lots of things need that com.acme.core.CampaignContext
interface, but we have a little wiggle room because we are using our own implementation of the interface. What does this mean? It means we can change it with no (okay, very little) impact!
package com.acme.app.context; public abstract class CampaignContext<T extends com.acme.core.Campaign> implements com.acme.core.CampaignContext { private T campaign; @Override public T getCampaign() { return this.campaign; } public void setCampaign(T campaign) { this.campaign = campaign; } }
Note that all references to com.acme.core.Campaign
have vanished except for in the class-level type parameter constraint. This effectively forces any subclass of this CampaignContext
to provide this type parameter, and the type specified must be an instance of core.Campaign
.
Now, when we use this CampaignContext
, we will leverage the compiler's ability to ensure that the type parameter constraint is enforced, and therefore calling code will always get the expected type (and not a core.Campaign
)!
package com.acme.app.servlet; import com.acme.tracking.Campaign; import com.acme.app.context.CampaignContext; public class TrackingContext extends CampaignContext<Campaign> { public void contrivedAf() { // Look, ma, no casts! Campaign campaign = getCampaign(); // Notice this is a tracking.Campaign! // do some tracking specific thing with the tracking.Campaign } }
We have introduced a mostly* backward-compatible change to the abstract CampaignContext
class. All we need to do is update every CampaignContext
subclass to specify which subclass of core.Campaign
it needs. In our application, we had a limited number of CampaignContext
subclasses so we felt comfortable updating each. Code that called those contexts didn't need to change, except we could now remove the runtime casts!
Finito
This was a long post, but hopefully I've been able to convey my ideas in a digestible way. Let me know why you think our decisions were right or wrong, I'd love to hear counter-opinions.
* For what it's worth, you can avoid the BC break entirely by leaving the old CampaignContext
implementation in place and using the new one on an opt-in basis as you make changes to other parts of the application.
Comments
Nov '17
02
Packages 3.2 released!
After a somewhat long wait, I'm pleased to announce the latest version of Packages which acts as a mini-CI service and allows you to host a private Composer repository. Packages has been in service for many years now and this latest version brings many improvements!
Packages 3.2
Probably the most noticeable features are the design tweaks on the landing page, as well as the new Available Packages page that lists all the projects and versions provided by the repository. In addition, many usability issues were worked out and a bunch of stuff was reworded to be much more clear.
Another big feature is the ability to fully secure Satis-related files behind a login. When Composer tries to fetch the packages.json file, it will prompt you for the username and password-- the same as configured to access the backend management. If security is enabled (by specifying secure_satis: true
in config.yml), the Available Packages page will also be secured behind the login.
And a few more:
- Added ability to create dist archives (
archive: true
in config.yml) - Added ability to customize company and contact information
- Added example docker-compose.yml to get you up and started in a snap!
- Various internal upgrades, like the ability for plugins to bind HTTP handlers and compiler passes.
Screenshot of the 'Available Packages' page.
Get your hands on it
Download the latest release on GitHub. Let me know what you think!
Comments
Sep '17
21
Introducing the MOTKI CLI
I've been playing EVE again lately-- started a corp, it's getting really serious. So naturally, I decided to build an online presence for my corporation and tooling to help me optimize my carebear experience.
What started out as a web application has now become a pretty neat technical experiment on building a mid-sized project with Go. I used protobuf to create a client/server layer with code being shared between. I was able to leverage the client side of things to create a command line tool that interacts with a remote motkid installation.
Ner..I mean, neat!
The goal for the application was to create a set of customized tools tailored to meet my needs. Especially, I wanted something that could tell me what I was missing to produce a certain item.
This first pre-release of the MOTKI command-line interface includes a production chain feature that allows a user to set up arbitrary production chains, including the ability to either buy or manufacture the required materials. It pulls down average market prices for the products to produce a basic cost analysis.
For example, here's the output for a Guardian:
Guardian Domain # Material Name Cost/ea Qty Req Cost/unit 1 Tritanium 5.78 550,265 3,180,531.82 2 Pyerite 6.01 110,053 661,418.56 3 Mexallon 76.29 31,746 2,421,902.37 4 Isogen 52.35 8,254 432,096.89 5 Nocxium 386.97 1,375 532,083.75 6 Zydrine 1,108.45 741 821,361.41 7 Megacyte 1,320.42 128 169,013.77 8 Construction Blocks 12,501.06 71 887,575.23 9 Morphite 10,805.95 86 929,311.72 10 Fusion Thruster M 43,626.36 71 3,097,471.50 11 Radar Sensor Cluster M 29,516.26 250 7,379,065.73 12 Nanoelectrical Microprocess M 60,705.62 1,714 104,049,431.69 13 Tungsten Carbide Armor Plat M 11,342.42 2,143 24,306,798.33 14 Antimatter Reactor Unit M 148,411.69 22 3,265,057.08 15 Tesseract Capacitor Unit M 59,157.90 714 42,238,737.15 16 Linear Shield Emitter M 43,560.71 143 6,229,181.63 Per unit Revenue 215,500,816.00 5% ME Cost 200,601,038.61 Profit 14,899,777.39 Margin % 6.91 * 'M' indicates the component will be produced in-house.
Additionally, it looks at corporation inventory for blueprint copies of the necessary items.
Here's the inventory for the above Guardian:
Materials Inventory Missing Blueprints 11532 Fusion Thruster Available Blueprints Best/Worst Total Name Type ID ME% TE% Runs Guardian 11987 3/ 3 2/ 2 6 Radar Sensor Cluster 11537 10/10 20/20 11000 Nanoelectrical Microprocessor 11539 10/10 20/20 9000 Tungsten Carbide Armor Plate 11543 10/10 20/20 32000 Antimatter Reactor Unit 11549 10/10 20/20 9000 Tesseract Capacitor Unit 11554 10/10 20/20 13000 Linear Shield Emitter 11557 10/10 20/20 3000
Pretty cool, huh?
Check out the source on GitHub.
You'll probably also want to create an account on the Moritake Industries website and link your character to enable the production chain functionality in the CLI.
Comments
Search
Archive
- March 2018
- Refactoring, Now With Generics!
- November 2017
- Packages 3.2 released!
- September 2017
- Introducing the MOTKI CLI
- July 2017
- Decoupling Yourself From Dependencies
- May 2017
- Model Rocketry Update
- April 2017
- Dynamic DNS with homedns
- March 2017
- The Story I Heard
- February 2017
- New squIRCy2 Website
- Ad-Hoc Polymorphism in Scala
- July 2016
- Packing Data Into Byte Arrays