Home
Home-custom
Cancel

Next Steps

"We hold that Roe and Casey must be overruled," Justice Alito writes in an initial majority draft circulated inside the court.

[1]

When I read those words my heart dropped into my stomach. I knew this was coming, even in this term, and still I felt my entire body chill.

The news is terrifying right now. Knowing that it is coming and seeing it in reality are always different things, and seeing the text of this opinion I thought of all of the people who will be hurt and how many lives with be affected when this decision goes into effect.

Despair is a natural response, but I wanted to compile some thoughts from various sources on what people can actually do in this moment. What will be effective to do right now. It’s part of my way of coping, of understanding where I need to put my feet next and to understand how the situation has evolved over time.

I don’t claim to have the answers, and I write this as much for myself as anyone. What I will say, however, is that the paths forward all involve organizing.

Being Effective

…stop engaging in parades, and start disrupting the operation of power.

[2]

Note first: Almost nothing here is effective unless you have done the ground level organizing, and that should almost always start with people who are already doing it, if they exist (and they often do).

Breaking the Law

Determine if you are willing to break the law as part of your action, in what ways, and on what timeline. Think very careflly about this.

If you haven’t already then talk this position over with your family and be prepared to make some hard choices in the future here. Think about it, think about what you are willing to do, and think about the consequences you are willing to endure. Especially if you come from a place of privilege, meaning that for you such thinking is unusual, spend some serious time with the question of where you land on this.

Remember:

You law abiding citizens, come listen to this song
Laws were made by people, and people can be wrong
Once unions were against the law, but slavery was fine
Women were denied the vote and children worked the mine
The more you study history the less you can deny it
A rotten law stays on the books til folks like us defy it

[3]

I do not, here, mean “talk about what other people should do.” This is about your position. Then talk to others who have already been working on this problem.

Understand Where You Live

With the repeal of Roe, we will get to a hodgepodge of laws through the US (At least until the Republicans gain control of the House, Senate, and Presidency again).

For example, in Colorado there is the Reproductive Health Equity Act in force, a blue general assembly, a blue governor.

Broadly, States fall into one of four categories (borrowing from the Center for Reproductive Rights):

  1. Welcoming. Places with strong protections (constitutional or statutory) and few restrictions.
  2. Protected. They lack the stronger protections or maybe have some additional restrictions, but safe abortions are readily available and likely to remain so.
  3. Not protected. Places where it is only grudgingly legal, but which lack enjoined laws or so-called “trigger laws.”
  4. Hostile. Active antagonism with trigger laws or enjoined laws that will go into force almost immediately.

But the details matter and there’s tremendous state level variance even within each of the categories. As a result you will often find different groups will rate places differently at the edges depending on how much, e.g., they look at TRAP laws or whether they consider a court decision binding.

Fundamentally, there are four states (and DC) that protect abortion throughout the pregnancy. There are sixteen states that protect it up to the point of viability. Some of those are statutory protections, some of them are constitutional protections, and sometimes they have other restrictions (like TRAP laws) even though it is generally legal.

Other states are a mixed bag. Some are outright hostile in a lot of ways, but it will remain at least somewhat available so long as the current balance of power in their local government remains where it is.

Know where you are. Know the status of your neighboring states. Know the status of where your friends live. The laws here are a hodgepodge and knowing where your state falls and the specifics of where it falls is important. Some states aren’t likely to restrict it, but also won’t necessarily be good states to drive a friend to either.

Good sources here are the Center for Reproductive Rights, the Guttmacher institute, and Rewire news group.

Know which is which. Especially as you start working with groups that have been doing this already, it will help give you a baseline understanding of what you are up against.

Organize for direct action

I don’t mean parades (what people often call protests).

Organize your workplace, your community, your apartment complex. Get them radicalized and start thinking about what direct action would look like for your group. Start building resources for mutual aid.

What you fight for and how you fight with this depends on what you have organized, but some examples:

  • Get “will not discriminate on the basis of having had an abortion” or equivalent language added to your union contract or tenant agreement.
  • Push your company and your union to vocally support abortion access.
  • Get your company to support paying travel expenses for those who have to travel for abortions.
  • Shut down businesses that do discriminate here. Your local coffee shop fired someone for getting an abortion? Sit in. Fill every table, lock hands and keep people from entering, turn them into a local pariah.
  • Push companies to stop supporting republican politicians. Make companies that don’t do this miserable.
  • Make life miserable for your local legislators (or the US congress or SCOTUS, I won’t judge). Keep pressure up on them.
  • Use your mutual aid funds to help people get abortions or organize driving/flying people between states.
  • Come up with other goals that you can reach with various forms of direct action. Get creative and talk it over with your peers, because what you can do is far more expansive than you likely realize.

On that note: You can only organize where you are involved. You can’t organize effectively from the outside, so don’t try to “organize the south” unless you live there.

Most of these require that you organize to be effective, so start there. Focus on your apartment complex, your school, your job, your communities. If you are working with organizers of other groups, your job is to listen to them and give them what they ask for.

One thing I was told very early in union organizing is that it doesn’t matter if your coworkers are conservative, if they think they are anti-union, whatever else. They can still be organized.

Ultimately remember though: Direct action is a tool, and as Sara Nelson says, our power comes from solidarity. [4] The Black Panthers are known for escorting people to register to vote with guns, but they also made breakfast for children, which is also a form of direct action. Conversely, the Provisional IRA had massive support from the community, for example in the rattling of trash bin lids to signal raids. [5]

Try really hard to not reinvent the wheel.

When you are looking to do something, look for people who are already doing that job and help them. Don’t try to create things de novo, focus instead on supporting what already exists.

Usually when people in dominant cultures first become aware of a problem there’s a group that has been working on that exact problem for over 100 years. This is no different.

The first step there is learn who is already working on these problems. Then you can listen to what they need and give it to them.

Political Approaches

Political approaches fall under the category of “don’t put faith in our systems, our systems are at best broken and at worst working exceptionally well at being terrilbe,” but political approaches can create a lot of buffer. Fighting a group of incompetent Democrats who can’t agree is night-and-day from fighting a group of fascist Republicans. Fighting to protect your rights when you have a state-level constitutional amendment in hand is very different than fighting to protect your rights when you don’t have such passed.

So these are effective approaches in some ways, but don’t depend on the government or the political parties to save us. Only we can do that, and many of these are–like the elements above–only effective with good organizing.

Push Local Government Around

The difference between living in Colorado and living in Texas on these sorts of issues is night and day. So get your local government to pass protections.

Call your city council, your mayor, your local legislator, your local governor.

Make them pledge to pass laws to protect abortion in your state or to provide some degree of sanctuary in your city. Primary them if they won’t do it. If they are all about StAtEs RiGhTs tell them “great, you now have the right to make it legal.” Push them to resist national legislation banning it.

Referendums and Citizen Initiatives

Go through whatever your state’s public referendum or citizen’s initiative process is for adding or challenging a constitutional amendment or a law. Learn what your state does for this and what it requires and get on it, and use it to either strike down legislative overreach or to enshrine protections.

Remember that abortion protections up to at least some point are popular in the US, so even if you can’t get your legislators on board you can still get protections passed locally.

If you already have it protected, try to amplify those protections. Get it as a constitutional amendment if it isn’t already protected there, make it as explicit an amendment as possible.

Push your national legislators

This doesn’t mean yelling “DO SOMETHING” on twitter. Call them. Every day. Fax things to them. Show up at townhalls and yell at them. Chase them into bathrooms. Camp out in front of their office or their house. Confront them in restaurants. Make your displeasure clear.

They are there to serve you. Remind them of that fact.

Tell them to pass the House’s protection. Tell them to expand the SCOTUS. Tell them to push the President to send abortion providers to federal lands and other suggestions that people like Elie Mystal and Imani Gandy have been talking about.

Do this even if they are conservatives. Make them so miserable that they leave.

Vote. Blue.

Vote and vote blue. Every time. Regardless if they have earned it. Make it so that Manchin is the most conservative member of a 60 member senate. Make sure that the Democrats keep the Presidency. Take your local governorship. Take your local legislator. Take your local dogcatcher and make it blue.

If you don’t want a particular Democrat in the role then primary them, then vote for the Democrat who wins the primary.

Voting blue is insufficient. It is woefully inadequate. Electoralism will not save us. But it belongs on this list because you don’t want to be trying to do any of these with a GOP congress and a GOP president, we want to replace Supreme Court members with people who are sympathetic in the long run, not fascists, and a hodgepodge is better than a nationwide ban passed by and enforced by fascists.

The republicans are fascists. Keep them out of power.

On thate note, it is also important to do the work to improve access to voting. Make sure everyone is registered to vote. Make sure that they can vote. Protect them in trying to vote. Help them make sure they remain registered to vote and are eligible to vote. Improve turnout in disadvantaged communities with whatever that takes.

I do, incidentally, mean “whatever that takes.”

Don’t Reinvent the Wheel, Don’t Give Up

White leftists in particular have a long habit of noticing something and coming to three conclusions:

  1. That because they haven’t heard of it previously, no one else has either.
  2. That any other groups that have been working on this have not succeeded due to failures of vision, ethics, or because they haven’t had the white person’s unique insight.
  3. That because something turns out to be hard, it is intractable.

It took over a century in the US to stop some of the worst excesses of child labor and we’re still not done nearly a century after that. Women’s suffrage was similarly over 80 years in the making. Let alone the distance between 1619 and 1865.

There are often groups already doing a lot of the things that people think of, saw this coming, and have deep knowledge of the terrain. Connect with them! Help them as much as possible. Remember that we have to be in this for the long haul.

Conclusions: Consider Where You Are, and Where You Need To Be

In every case, think in terms of where you are, what you want to accomplish and drawing a line to what it will take to accomplish that, and what your escalation is if you fail. As the quote from Gen. Robert H. Barrow goes:

Amateurs talk about strategy and tactics. Professionals talk about logistics and sustainability in warfare.

This means that you can’t just go “we’re going to have a general strike!” then later go “well, no one showed up, so our solution is ADVERTISE HARDER.”

Where you are: Be realistic. Do you have the solidarity and support for a strike? For a general strike? For a sit-in? If not, what will it take to get to that point? How are you going to manage this without burning everyone out?

We must maintain hope and certainty that we will win, and we also must maintain a clear-eyed realization of what our position is.

What you are trying to accomplish and what it will take to accomplish that: If you want to, say, raise the minimum wage for everyone, having a walkout at your local whole foods is unlikely to accomplish that. Your escalation if you can’t secure a 15 dollar minimum wage is not “ask for 25 dollars instead” and your escalation path if you can’t get your local store to strike is not “we’ll have a general strike instead!”

Your escalation path should be clear and you should have one, but that means being realistic about where you are and what you can do right now with the resources that you have. It means understanding where you are logistically, and how that situation will evolve.

Learn constantly. Be prepared to fail and to learn from your failures. Be prepared for this to take not just years but generations.

After all, it took generations to get here.

References

[1]   J. Gerstein and A. Ward, “Exclusive: Supreme Court has voted to overturn abortion rights, draft opinion shows,” POLITICO. May 2, 2022. [Online; accessed on 5 May 2022]

[2]   E. C. McLaughlin, “A parade is not a protest and other lessons for the looming collapse of our rights,” The Newsletter with ECM. May 7, 2022. [Online; accessed on 9 May 2022]

[3]   A. Feeney, “Have you Been To Jail For Justice?,” Original Recordings. 1969. [Online; accessed on 9 May 2022]

[4]   S. Nelson, “Preface,” in Strike! 50th Anniversary Edition, J. Brecher, Ed. PO Box 23912, Oakland, CA 94623: PM Press, 2020.

[5]   E. Owicki, “Rattle Away at Your Bin: Women, Community, and Bin Lids in Northern Irish Drama,” Theatre Symposium, vol. 18, pp. 55–56, doi: 10.1353/tsy.2010.0011. [Online; accessed on 9 May 2022]

Project Root Canal: A Story About Fixing a System

Years ago I was working on a project that had a sort of “service locator” pattern in it. This is a memory about how it got replaced with dependency injection.

I put “service locator” in quotes because a normal service locator pattern looks something like this:

ClassAServiceLocatorlocate(ServiceKey<T>) : Service<T extends Service<T>>ServiceT extends Service<T>initialize()serviceState(): ServiceStateService01Service01Service02Service02May be a service or just an unencapsulated class.1*

Then in order to locate a service, you just pass the service key to the locator:

ClassAService LocatorServicealt[Not Ready]locate(ServiceKey<Service>)checkIfReadycreate()new(Service)initialize()ready()ServiceClassAService LocatorService

When services depend on each other you try to keep it out of the initialize step, or at a minimum keep it as a directed acyclic graph inside the initialize step so that they won’t deadlock.

This is almost, but not quite, entirely unlike what this system did.

The System’s Design

While the system was clearly inspired by a service locator pattern, in an effort to make it “type safe” it had made certain allowances in its design:

ClassAClassBClassCRootclassA(): List<ClassA>classB(): ClassBclassC(): ClassC10..*1111

Initialization happened entirely in the constructor and with interdependencies between the classes:

ServerRootClassAClassBClassCcons()cons()classAcons()classA()classAdoSomething()classBcons()classA()classAdoSomething()classB()classBdoSomethingElse()doAnotherThing()finishAnotherThingresultsFromSomethingElseclassCrootServerRootClassAClassBClassC

Services could (and did) refer to other services outside of their constructors, which may or may not be initialized. These methods would be called by other services inside their constructors.

Among other consequences:

  1. It meant that the order that the services were created in the constructor was extremely fragile.
  2. You had no ability to know, when you added a new call to an item, if it had any dependencies that were unresolved.
  3. It was completely untestable and it made new code difficult to test. This was made worse by the constructors engaging in behavior like File IO or starting threads.

This company at the time was very anti-testing for a variety of reasons, but part of it was that writing tests with a system like this led to brittle tests that were difficult to write and provided minimal value.

Purpose of the Design

The (unfulfilled) idea was that you could have various types of system all living together not just in one code base, but one binary and spin up a server with radically different characteristics through configuration alone.

This was never followed through with, but it did mean that a lot of Root code would end up in sometimes bizarre tools that really didn’t need to be running, say, the production user management system.

This was not something that anyone really wanted to keep: it was a vision that never came to fruition, and no one really thought it would ever come to be, but a lot of the design choices were predicated on this assumption.

So I Set Out To Fix It

Fixing this “service locator” became a major focus for me. I wanted to convert us to dependency injection model and had chosen guice as a framework. Guice had several advantages over other systems:

  1. It required no mucking about with the build, which at that point was something incredibly perilous to change (another team was working on this problem).
  2. It had support for a lot of weird edge cases, such as multibinding, circular dependencies, and things of that nature. These aren’t necessarily desireable, but they were already extant in the code and we didn’t know where all of them were.
  3. It worked off the standard JSR injection patterns, which facilitate doing manual injection and make testing easier.

I got a few people together who were on board, got buy-in from different parts of the org, and off we went.

Can we start from the bottom up?

The original idea was to start at the bottom and work my way up:

Find a point with no dependencies (that aren't injected)
Replace it with the injector
Initialize complex behavior in an initialize() method

Where possible, it would also help to bring pieces into a test harness, even if we couldn’t write any tests for them yet.

This approach, which focused on the degree of coupling, had several advantages:

  • Minimally Invasive By starting with the lowest pieces of the puzzle and working up meant that, especially at first, the components did not need excess dependencies and the modules could be kept relatively simple. Also it meant that each individual code change would be small and would be unlikely to impact anyone who wasn’t working on that exact class.
  • Fast Benefit It could be done in small pieces—two hours here, a day there—rather than requiring a large amount of work up front.
  • Isolated Most of the classes that fit this description were not in the critical path per se, or were in the critical path but their instantiation and teardown were not in the crtitical path. This meant that we could make the changes without radically altering the performance or memory characteristics of the system so that we could prove that we wouldn’t impact things too badly before moving to the more critical path components.

The limitations, however, quickly became apparant as well:

  1. Because of the mass of interrelated dependencies, we ended up needing multiple injectors that we would build on in later steps.
  2. It turns out that the criteria for this actually came out to very few cases. Adding additional cases got more and more difficult, particularly as we started encountering classes with 15+ dependencies.
  3. It was actually fairly difficult to map how many dependencies a given class had. Because of the problem of A would call B during the instantiation process of C, and because there was a lot of code that would do: root.getA().getB().getC().doSomething() finding which class to pick next was relatively challenging.

But the real kicker was this:

People who were doing development would often reach for a tool that was on Root but then get confused on “how to add it the right way.” They would add dependencies that were not ready to be brought in, and this meant that the entire class would need to be revisited.

This also meant that rather than doing dependency injection, a lot of classes would end up with a hodgepodge of dependency injection and service lookups… and we were inadvertantly encouraging them to make more of a hodgepodge going forward rather than encouraging other developers to clean things up.

We had created a situation where there were pretty much two people who could actually improve the health of the code and drive the project forward.

This was, suffice it to say, undesireable.

Enter Project Root Canal

Instead of going at it from the bottom up, what if we went at it from the top down? Focusing on the order of initialization rather than the degree of coupling.

Basically:

Find a point with no (uninjected) initialized dependencies
Convert it entirely to dependency injection
Initialize complex behavior in an initialize() method

This also gave us an opportunity, because most of these needed to be initialized, to wrap a lot of the tooling in Guava services, which gave us better error handling and lifecycle management.

Dependencies that were not ready to be converted to DI wholesale could still inject Root and use that object, giving a stopgap measure to keep us from having to convert the entire system in one throw.

Basically:

  1. Rather than start at the bottom and work our way up, we were starting at the top and working our way down.
  2. Rather than attempting to eliminate the Root object per se, we were hollowing it out and turning it into a wrapper for the initialization.

The outer structure of Root would remain exactly the same, but the inner structure of it became something simpler.

Having decided on a course of action, the initial work to get the basic framework in place took around two weeks. This kicked off the iterative process–which took months–of moving every component over to the new system.

Making it Readable

Guice has a reputation for making code hard to read. This is especially true when it is used in highly complex ways, with architectures that involve lots of options in their module configurations (what is Foo bound to? who can say!). We adopted several principles in this design to make sure that we didn’t end up in a worse position than where we started:

  1. Every package should contain exactly one (1) public module. No bindings should reach outside of their module if it is possible to avoid it (no impl packages). There was some flexibility on this point for multibindings. By convention this public module was named <Package>Module.
  2. Every package was responsible, within its public module, for injecting the modules of child packages. There were a handful of exceptions to this, but we tried to make them as explicit as possible. What this meant was that, combined with (1), even if the classes had dependencies all over the place, the modules would form a tree that was identical to the package hierarchy.
  3. Modules should contain no conditional logic, with only a handful of exceptions.
  4. Everything should be as explicit as possible: no implicit bindings, always use @Inject on constructors, and try to follow the Law of Demeter [1].
  5. All binding logic should be inside of the modules and their associated Providers. No using @ImplementedBy or @ProvidedBy.

The goal here was to simplify what we could in guice and make it as simple as possible to find and diagnose problems. You should never have to wonder “where was this bound.” If you wanted to know the implementation for com.example.foo.Bar you looked for com.example.foo.FooModule as a starting point, and you knew it had to be bound somewhere within a Module inside of com.example.foo. This limited the search scope to a handful of classes, usually only one or two.

Using these rules, you could pretty much always find the implementation of a given object with grep or by hand, if need be.

It also simplified reasoning about the module layout if you always knew that com.example.foo.FooModule would always inject com.example.foo.bar.BarModule and com.example.foo.baz.BazModule.

Getting Others On Board

There’s a lot more to making a change of this scale and scope than just the technical work of migrating the code. A lot of other things had to start happening simultaneously, and ultimately the success of a project like this depends on more than a small group working on it. We needed to get everyone to join in on the process.

Many of the developers had started their careers with this system and only had peripheral knowledge of dependency injection as a set of principles. This necessitated a lot of different approaches to try to make sure that the ideas were widely disseminated:

  1. Holding small group workshops with practice and discussion elements.
  2. Larger sessions that were more theory focused.
  3. Providing resources in the form of books, articles, and the like.
  4. One-on-one work with individuals, especially with help debugging or tracking down problems.
  5. Copious code reviews.

It also required clearing up some misconceptions:

  1. That “dependency injection” is a framework. Essentially thinking that DI = Spring, Guice, Dagger, etc.
  2. That merely being instantiated with guice would significantly alter the runtime performance characteristics.
  3. That this was the end of the journey and after this we would be in a magical place.

Now for the Test Harness

Meanwhile, a senior SDET had been working on trying to get the entire thing into an integration test harness that could run against a known database.

To do this, we made sure that there were tools in place that allowed them to swap out the injector in test and we prioritized those pieces that did things that were really undesireable in a testing situation. Things like interacting with a database or opening a network socket inside of a constructor. As much as possile we tried to move these into guava services that we could initialize separately from their construction.

Now whenever someone would instantiate the service, a series of steps took place that most programmers never had to worry about or deal with:

Build the set of modules
we were going to use
Create the guice injector
Inject the ServiceManager
Start the ServiceManager
Wait for the ServiceManager to
reach a healthy state

With that done we could add a few static methods that allowed us to swap out the modules and we were good to go.

Well, Almost

Turns out that people have a strong reflex when they see a class that looks like this:

1
2
3
Foo(Root root) {
  this.root = root;
}

Become this:

1
2
3
4
5
6
7
8
9
10
@Inject
Foo(
  DependencyA a,
  DependencyB b,
  DependencyC c,
  DependencyD d,
  //12 more dependencies
  DependencyQ q) {
    // ...
}

They have some understandable twitch reactions and second thoughts.

The thing was that, by and large, those dependencies were preexisting. It’s just that previously they happened deeper in the code when root.getQ() was called. It wasn’t adding dependencies, instead it was revealing the dependencies that already existed.

Refinements and Iterations

Through this we had several missteps or learning experiences along the way that caused reevaluation and backtracking or doing things differently.

Multiple Injectors

Our early efforts involved building multiple, chained injectors. This was done because fo the early design philosophy of starting at the bottom and working our way up, as we often had to deal with situations where a (non-injected) object needed to use the result of injection before something else (that was injected) could be built.

CreateSomeObjects
OriginalInjector
createInjector
firstServiceStart
CreateMoreObjects
SecondInjector
createChildInjector1
randomServiceStart
CreateAnotherObjectGroup
ThirdInjector
createChildInjector2
otherServiceStart
CreateEvenMoreObjects
finalServiceGroup

This proved to be moderately disastrous as an approach:

  1. This made it extremely difficult to test. The construction process become fragile and any change to ordering or system construction would cause the test harness to break.
  2. Because some steps depended on the initialization of previous steps, it meant that you almost couldn’t use a ServiceManager and were instead stuck with initializing every service manually.
  3. It became very difficult for engineers who weren’t extremely familiar with the ins and outs of guice to modify the system in any way, which was the opposite of what was desired.

PrivateModule

When we first started, we used PrivateModules to strictly segment the code areas. This way the packages very rarely impacted each other. Over time, it was recognized that this was not ideal.

  1. PrivateModule did not work with a lot of the tooling that makes it easier to debug, visualize, or test guice.
  2. It ended up making it harder to share certain key, central resources that were already shared. It became challenging to know whether it was more appropriate to declare a resource in the private module or in a higher level module, or to find resources that might be useful that had already been bound somewhere.
  3. It added another layer of conceptual complexity to an already complex system.

In the end, we did away with virtually all of the private modules, moving instead to binding annotations/qualifiers instead.

Testing Moving Ahead of Refactoring

In several cases testing moved ahead of refactoring, requiring us to break and fix a large body of tests that had been written to try to solve the incredibly weird problem. This was especially true when we had started from the bottom up, but it remained a problem until we had finalized the changes.

Luckily, the testing group was completey on board with our work, so it made it an easy sell in a lot of ways, but it still meant there was some avoidable churn in how we went about it.

Is That a Singleton?

Guice likes to deemphasize the use of singletons—for good reasons—when they aren’t absolutely required. This had… odd interfacing with some of the elements of the system that depended on something being a singleton and never documented it.

This was particularly challenging for sets of objects that depended on shared locks (yes, this was a thing), where the object had some slow memory leak that hadn’t been previously detected, or where a singleton was being used as a memory control. In these cases there was no obvious reason why a singleton was the right choice–and sometimes it was in fact the wrong choice–but Bad Things™ would happen from removing that conceit from the system.

This was also challenging because a lot of bugs could crop up that were very subtle as a result of these sorts of conceits. To catch these we had to do a lot of work with load testing, because the problems would only manifest under load.

Conclusion

Ultimately the project was what I’d term successful: we got the system into a stable, testable, state. It was in many way

So that’s me getting the story of how we tore apart the central conceit of a system out of my system.

References

[1]   K. J. Lieberherr, I. Holland, and A. J. Riel, “Object-Oriented Programming: An Objective Sense of Style,” SIGPLAN Notices, vol. 23, no. 11, pp. 323–334, Jan. 1988, doi: 10.1145/62084.62113. [Online; accessed on 25 April 2022]